Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions docs/src/intro-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,42 @@ Get started by installing Playwright and running the example test to see it in a

Install the [Pytest plugin](https://pypi.org/project/pytest-playwright/):

<Tabs
groupId="package-managers"
defaultValue="pip"
values={[
{label: 'Pip', value: 'pip'},
{label: 'Poetry', value: 'poetry'},
{label: 'uv', value: 'uv'}
]
}>

<TabItem value="pip">

```bash
pip install pytest-playwright
```

</TabItem>

<TabItem value="poetry">

```bash
poetry add pytest-playwright
```

</TabItem>

<TabItem value="uv">

```bash
uv add pytest-playwright
```

</TabItem>

</Tabs>

Install the required browsers:

```bash
Expand Down Expand Up @@ -69,10 +101,38 @@ pytest

To update Playwright to the latest version run the following command:

<Tabs
groupId="package-managers"
defaultValue="pip"
values={[
{label: 'pip', value: 'pip'},
{label: 'Poetry', value: 'poetry'},
{label: 'uv', value: 'uv'}
]
}>
<TabItem value="pip">

```bash
pip install pytest-playwright playwright -U
```

</TabItem>
<TabItem value="poetry">

```bash
poetry update pytest-playwright playwright
```

</TabItem>
<TabItem value="uv">

```bash
uv add --upgrade pytest-playwright playwright
```

</TabItem>
</Tabs>

## System requirements

- Python 3.8 or higher.
Expand Down
36 changes: 36 additions & 0 deletions docs/src/library-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,48 @@ title: "Getting started - Library"

[<img src="https://badge.fury.io/py/playwright.svg" alt="PyPI version" width="132" height="20" />](https://pypi.python.org/pypi/playwright/)

<Tabs
groupId="package-managers"
defaultValue="pip"
values={[
{label: 'Pip', value: 'pip'},
{label: 'Poetry', value: 'poetry'},
{label: 'uv', value: 'uv'}
]
}>

<TabItem value="pip">

```bash
pip install --upgrade pip
pip install playwright
playwright install
```

</TabItem>

<TabItem value="poetry">

```bash
poetry self update
poetry add playwright
playwright install
```

</TabItem>

<TabItem value="uv">

```bash
uv self update
uv add playwright
playwright install
```

</TabItem>

</Tabs>

These commands download the Playwright package and install browser binaries for Chromium, Firefox and WebKit. To modify this behavior see [installation parameters](./browsers.md#install-browsers).

## Usage
Expand Down
2 changes: 1 addition & 1 deletion packages/playwright-core/browsers.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
},
{
"name": "webkit",
"revision": "2309",
"revision": "2310",
"installByDefault": true,
"revisionOverrides": {
"mac14": "2251",
Expand Down
7 changes: 7 additions & 0 deletions packages/playwright-core/src/server/har/harRecorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export class HarRecorder implements HarTracerDelegate {
this._fs.writeFile(path.join(this._resourcesDir, sha1), buffer, true /* skipIfExists */);
}

onContentBlobAppend(sha1: string, text: string) {
if (!this._writtenContentEntries.size)
this._fs.mkdir(this._resourcesDir);
this._writtenContentEntries.add(sha1);
this._fs.appendFile(path.join(this._resourcesDir, sha1), text);
}

private async _flush() {
if (this._isFlushed)
return;
Expand Down
60 changes: 21 additions & 39 deletions packages/playwright-core/src/server/har/harTracer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { base64ByteLength } from '@isomorphic/base64';
import { ManualPromise } from '@isomorphic/manualPromise';
import { eventsHelper } from '@utils/eventsHelper';
import { assert } from '@isomorphic/assert';
import { calculateSha1 } from '@utils/crypto';
import { calculateSha1, createGuid } from '@utils/crypto';
import { monotonicTime } from '@isomorphic/time';
import { isTextualMimeType } from '@isomorphic/mimeType';
import { urlMatches } from '@isomorphic/urlMatch';
Expand All @@ -45,6 +45,7 @@ export interface HarTracerDelegate {
onEntryStarted(entry: har.Entry): void;
onEntryFinished(entry: har.Entry): void;
onContentBlob(sha1: string, buffer: Buffer): void;
onContentBlobAppend(sha1: string, text: string): void;
}

type HarTracerOptions = {
Expand Down Expand Up @@ -75,7 +76,6 @@ export class HarTracer {
private _pageEntrySymbol: symbol;
private _baseURL: string | undefined;
private _page: Page | null;
private _saveOpenWebSocketMessagesFunctions = new Set<() => void>();

constructor(context: BrowserContext | APIRequestContext, page: Page | null, delegate: HarTracerDelegate, options: HarTracerOptions) {
this._context = context;
Expand Down Expand Up @@ -440,27 +440,25 @@ export class HarTracer {
const harEntry = createHarEntry(pageEntry?.id, method, url, page.mainFrame().guid, this._options, webSocket.wallTimeMs());
harEntry._resourceType = 'websocket';

const messages: har.WebSocketMessage[] = [];
if (this._options.content === 'embed')
harEntry._webSocketMessages = messages;

let saveMessages: (() => void) | undefined;
if (this._options.content === 'attach') {
saveMessages = () => {
if (!messages.length)
return;
let sha1: string | undefined = undefined;
const recordMessage = (type: 'send' | 'receive', opcode: number, data: string, wallTimeMs: number) => {
const message = { type, time: this._options.omitTiming ? -1 : wallTimeMs, opcode, data };
if (this._options.content === 'embed') {
harEntry._webSocketMessages ??= [];
harEntry._webSocketMessages.push(message);
} else if (this._options.content === 'attach') {
if (!sha1) {
sha1 = createGuid() + '.jsonl';
if (this._options.includeTraceInfo)
harEntry.response.content._sha1 = sha1;
else
harEntry.response.content._file = sha1;
}

const buffer = Buffer.from(JSON.stringify(messages));
const sha1 = calculateSha1(buffer) + '.json';
if (this._options.includeTraceInfo)
harEntry.response.content._sha1 = sha1;
else
harEntry.response.content._file = sha1;
if (this._started)
this._delegate.onContentBlob(sha1, buffer);
};
this._saveOpenWebSocketMessagesFunctions.add(saveMessages);
}
this._delegate.onContentBlobAppend(sha1, JSON.stringify(message) + '\n');
}
};

let oldestWallTimeMs = Infinity;
let newestWallTimeMs = -Infinity;
Expand Down Expand Up @@ -497,15 +495,11 @@ export class HarTracer {
}
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameSent, ({ opcode, data, wallTimeMs }: { opcode: number, data: string, wallTimeMs: number }) => {
if (this._options.content !== 'omit')
messages.push({ type: 'send', time: this._options.omitTiming ? -1 : wallTimeMs, opcode, data });

recordMessage('send', opcode, data, wallTimeMs);
updateTime(wallTimeMs);
}),
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.FrameReceived, ({ opcode, data, wallTimeMs }: { opcode: number, data: string, wallTimeMs: number }) => {
if (this._options.content !== 'omit')
messages.push({ type: 'receive', time: this._options.omitTiming ? -1 : wallTimeMs, opcode, data });

recordMessage('receive', opcode, data, wallTimeMs);
updateTime(wallTimeMs);

if (!this._options.omitSizes) {
Expand All @@ -530,11 +524,6 @@ export class HarTracer {
eventsHelper.addEventListener(webSocket, network.WebSocket.Events.Close, () => {
eventsHelper.removeEventListeners(eventListeners);

if (saveMessages) {
this._saveOpenWebSocketMessagesFunctions.delete(saveMessages);
saveMessages();
}

if (this._started)
this._delegate.onEntryFinished(harEntry);
}),
Expand Down Expand Up @@ -665,12 +654,6 @@ export class HarTracer {
}

stop() {
// Unlike other requests that have a single response, a WebSocket can receive multiple frames.
// As such, we don't finish the entry until the WebSocket is closed, which delays when the captured frames are saved.
// Make sure to save at least what has been captured so far.
for (const saveOpenWebSocketMessages of this._saveOpenWebSocketMessagesFunctions)
saveOpenWebSocketMessages();

this._started = false;
eventsHelper.removeEventListeners(this._eventListeners);
this._barrierPromises.clear();
Expand Down Expand Up @@ -703,7 +686,6 @@ export class HarTracer {
}
}
this._pageEntries = [];
this._saveOpenWebSocketMessagesFunctions.clear();
return log;
}

Expand Down
6 changes: 6 additions & 0 deletions packages/playwright-core/src/server/trace/recorder/tracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,12 @@ export class Tracing extends SdkObject implements InstrumentationListener, Snaps
this._appendResource(sha1, buffer);
}

onContentBlobAppend(sha1: string, text: string) {
if (!this._allResources.has(sha1))
this._allResources.add(sha1);
this._fs.appendFile(path.join(this._state!.resourcesDir, sha1), text, this._state!.options.live /* flush */);
}

onSnapshotterBlob(blob: SnapshotterBlob): void {
this._appendResource(blob.sha1, blob.buffer);
}
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/reporters/html.ts
Original file line number Diff line number Diff line change
Expand Up @@ -464,11 +464,11 @@ class HtmlBuilder {

private async _writeReportData(filePath: string) {
fs.appendFileSync(filePath, '<template id="playwrightReportBase64">data:application/zip;base64,');
await new Promise(f => {
await new Promise<void>((resolve, reject) => {
this._dataZipFile.end(undefined, () => {
this._dataZipFile.outputStream
.pipe(new Base64Encoder())
.pipe(fs.createWriteStream(filePath, { flags: 'a' })).on('close', f);
.pipe(fs.createWriteStream(filePath, { flags: 'a' })).on('close', resolve).on('error', reject);
});
});
fs.appendFileSync(filePath, '</template>');
Expand Down
4 changes: 2 additions & 2 deletions packages/playwright/src/worker/testTracing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,9 +238,9 @@ export class TestTracing {
const traceContent = Buffer.from(this._traceEvents.map(e => JSON.stringify(e)).join('\n'));
zipFile.addBuffer(traceContent, testTraceEntryName);

await new Promise(f => {
await new Promise<void>((resolve, reject) => {
zipFile.end(undefined, () => {
zipFile.outputStream.pipe(fs.createWriteStream(this._generateNextTraceRecordingPath())).on('close', f);
zipFile.outputStream.pipe(fs.createWriteStream(this._generateNextTraceRecordingPath())).on('close', resolve).on('error', reject);
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/trace-viewer/src/ui/networkFilters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import './networkFilters.css';

const resourceTypes = ['Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image'] as const;
const resourceTypes = ['Fetch', 'HTML', 'JS', 'CSS', 'Font', 'Image', 'WS'] as const;
export type ResourceType = typeof resourceTypes[number];

export type FilterState = {
Expand Down
Loading
Loading