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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ EOF
)"
```

Never add Co-Authored-By agents in commit message.
Branch naming for issue fixes: `fix-<issue-number>`

## Development Guides
Expand Down
83 changes: 0 additions & 83 deletions docs/src/api/class-inspector.md

This file was deleted.

60 changes: 41 additions & 19 deletions docs/src/api/class-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -763,6 +763,13 @@ System prompt for the agent's loop.

Brings page to front (activates tab).

## async method: Page.cancelPickLocator
* since: v1.59
* langs: js

Cancels an ongoing [`method: Page.pickLocator`] call by deactivating pick locator mode.
If no pick locator mode is active, this method is a no-op.

## async method: Page.check
* since: v1.8
* discouraged: Use locator-based [`method: Locator.check`] instead. Read more about [locators](../locators.md).
Expand Down Expand Up @@ -2600,25 +2607,6 @@ Throws for non-input elements. However, if the element is inside the `<label>` e
### option: Page.inputValue.timeout = %%-input-timeout-js-%%
* since: v1.13

## method: Page.inspector
* since: v1.59
* langs: js
- returns: <[Inspector]>

Returns the [Inspector] object associated with this page.

**Usage**

```js
const inspector = page.inspector();
inspector.on('screencastFrame', data => {
console.log('received frame, jpeg size:', data.length);
});
await inspector.startScreencast();
// ... perform actions ...
await inspector.stopScreencast();
```

## async method: Page.isChecked
* since: v1.8
* discouraged: Use locator-based [`method: Locator.isChecked`] instead. Read more about [locators](../locators.md).
Expand Down Expand Up @@ -3077,6 +3065,20 @@ Whether or not to generate tagged (accessible) PDF. Defaults to `false`.

Whether or not to embed the document outline into the PDF. Defaults to `false`.

## async method: Page.pickLocator
* since: v1.59
* langs: js
- returns: <[Locator]>

Enters pick locator mode where hovering over page elements highlights them and shows the corresponding locator.
Once the user clicks an element, the mode is deactivated and the [Locator] for the picked element is returned.

**Usage**

```js
const locator = await page.pickLocator();
console.log(locator);
```

## async method: Page.press
* since: v1.8
Expand Down Expand Up @@ -3886,6 +3888,26 @@ Handler function to route the WebSocket.
Handler function to route the WebSocket.


## method: Page.screencast
* since: v1.59
* langs: js
- returns: <[Screencast]>

Returns the [Screencast] object associated with this page.

**Usage**

```js
const screencast = page.screencast();
screencast.on('screencastFrame', data => {
console.log('received frame, jpeg size:', data.length);
});
await screencast.start();
// ... perform actions ...
await screencast.stop();
```


## async method: Page.screenshot
* since: v1.8
- returns: <[Buffer]>
Expand Down
63 changes: 63 additions & 0 deletions docs/src/api/class-screencast.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# class: Screencast
* since: v1.59
* langs: js

Interface for capturing screencast frames from a page.

## event: Screencast.screencastFrame
* since: v1.59
- argument: <[Object]>
- `data` <[Buffer]> JPEG-encoded frame data.

Emitted for each captured JPEG screencast frame while the screencast is running.

**Usage**

```js
const screencast = page.screencast();
screencast.on('screencastframe', ({ data, width, height }) => {
console.log(`frame ${width}x${height}, jpeg size: ${data.length}`);
require('fs').writeFileSync('frame.jpg', data);
});
await screencast.start({ maxSize: { width: 1200, height: 800 } });
// ... perform actions ...
await screencast.stop();
```

## async method: Screencast.start
* since: v1.59

Starts capturing screencast frames. Frames are emitted as [`event: Screencast.screencastFrame`] events.

**Usage**

```js
const screencast = page.screencast();
screencast.on('screencastframe', ({ data, width, height }) => {
console.log(`frame ${width}x${height}, size: ${data.length}`);
});
await screencast.start({ maxSize: { width: 800, height: 600 } });
// ... perform actions ...
await screencast.stop();
```

### option: Screencast.start.maxSize
* since: v1.59
- `maxSize` ?<[Object]>
- `width` <[int]> Max frame width in pixels.
- `height` <[int]> Max frame height in pixels.

Maximum screencast frame dimensions. The output frame may be smaller to preserve the page aspect ratio. Defaults to 800×800.

## async method: Screencast.stop
* since: v1.59

Stops the screencast started with [`method: Screencast.start`].

**Usage**

```js
await screencast.start();
// ... perform actions ...
await screencast.stop();
```
39 changes: 39 additions & 0 deletions docs/src/chrome-extensions-js-python.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,45 @@ with sync_playwright() as playwright:
run(playwright)
```

## Service worker idle suspension (MV3)

Chrome MV3 service workers are automatically suspended after ~30 seconds of inactivity and restarted
on demand. When this happens, Playwright keeps the **same [Worker] object alive** — no new
`'serviceworker'` event is emitted. New `evaluate()` calls issued during the restart window are
stalled until the new context is ready and then resume automatically:

```js
const sw = await context.waitForEvent('serviceworker');

// ... SW suspends after 30 s of inactivity and is restarted by the browser ...

// The existing handle is transparent across the restart.
await sw.evaluate(() => sendMessage({ type: 'ping' })); // just works
```

```python async
sw = await context.wait_for_event('serviceworker')

# ... SW suspends after 30 s of inactivity and is restarted by the browser ...

# The existing handle is transparent across the restart.
await sw.evaluate("sendMessage({ type: 'ping' })") # just works
```

```python sync
sw = context.wait_for_event('serviceworker')

# ... SW suspends after 30 s of inactivity and is restarted by the browser ...

# The existing handle is transparent across the restart.
sw.evaluate("sendMessage({ type: 'ping' })") # just works
```

:::note
`evaluate()` calls that were already in-flight at the exact moment of suspension will throw
with `"Service worker restarted"`, matching the behaviour of page navigations mid-flight.
:::

## Testing

To have the extension loaded when running tests you can use a test fixture to set the context. You can also dynamically retrieve the extension id and use it to load and test the popup page for example.
Expand Down
8 changes: 4 additions & 4 deletions packages/devtools/src/grid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
const workspaceGroups = React.useMemo(() => {
const groups = new Map<string, SessionStatus[]>();
for (const session of sessions) {
const key = session.browserDescriptor.workspaceDir || 'Global';
const key = session.workspaceDir || 'Global';
let list = groups.get(key);
if (!list) {
list = [];
Expand All @@ -53,7 +53,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
list.push(session);
}
for (const list of groups.values())
list.sort((a, b) => a.browserDescriptor.title.localeCompare(b.browserDescriptor.title));
list.sort((a, b) => a.title.localeCompare(b.title));

// Current workspace first, then alphabetical.
const entries = [...groups.entries()];
Expand Down Expand Up @@ -91,7 +91,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
</div>
{isExpanded && (
<div className='session-chips'>
{entries.map(session => <SessionChip key={session.browserDescriptor.guid} descriptor={session.browserDescriptor} wsUrl={session.wsUrl} visible={isExpanded} model={model} />)}
{entries.map(session => <SessionChip key={session.browser.guid} descriptor={session} wsUrl={session.wsUrl} visible={isExpanded} model={model} />)}
</div>
)}
</div>
Expand All @@ -103,7 +103,7 @@ export const Grid: React.FC<{ model: SessionModel }> = ({ model }) => {
};

const SessionChip: React.FC<{ descriptor: BrowserDescriptor; wsUrl: string | undefined; visible: boolean; model: SessionModel }> = ({ descriptor, wsUrl, visible, model }) => {
const href = '#session=' + encodeURIComponent(descriptor.guid);
const href = '#session=' + encodeURIComponent(descriptor.browser.guid);

const channel = React.useMemo(() => {
if (!wsUrl || !visible)
Expand Down
10 changes: 4 additions & 6 deletions packages/devtools/src/sessionModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,10 @@
import type { ClientInfo } from '../../playwright-core/src/cli/client/registry';
import type { BrowserDescriptor } from '../../playwright-core/src/serverRegistry';

export type SessionStatus = {
browserDescriptor: BrowserDescriptor;
export type SessionStatus = BrowserDescriptor & {
wsUrl?: string;
};


type Listener = () => void;

export class SessionModel {
Expand Down Expand Up @@ -67,7 +65,7 @@ export class SessionModel {
}

sessionByGuid(guid: string): SessionStatus | undefined {
return this.sessions.find(s => s.browserDescriptor.guid === guid);
return this.sessions.find(s => s.browser.guid === guid);
}

private async _fetchSessions() {
Expand Down Expand Up @@ -103,7 +101,7 @@ export class SessionModel {
await fetch('/api/sessions/close', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionGuid: descriptor.guid }),
body: JSON.stringify({ guid: descriptor.browser.guid }),
});
await this._fetchSessions();
}
Expand All @@ -112,7 +110,7 @@ export class SessionModel {
await fetch('/api/sessions/delete-data', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionGuid: descriptor.guid }),
body: JSON.stringify({ guid: descriptor.browser.guid }),
});
await this._fetchSessions();
}
Expand Down
Loading
Loading