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
6 changes: 6 additions & 0 deletions docs/src/api/class-browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,12 @@ await page.GotoAsync("https://www.bing.com");
await browser.CloseAsync();
```

## event: Browser.context
* since: v1.60
- argument: <[BrowserContext]>

Emitted when a new browser context is created.

## event: Browser.disconnected
* since: v1.8
- argument: <[Browser]>
Expand Down
30 changes: 30 additions & 0 deletions packages/playwright-client/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9974,6 +9974,11 @@ export interface Browser {
*/
behavior?: 'wait'|'ignoreErrors'|'default'
}): Promise<void>;
/**
* Emitted when a new browser context is created.
*/
on(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the
* following:
Expand All @@ -9982,11 +9987,21 @@ export interface Browser {
*/
on(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
once(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
once(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Emitted when a new browser context is created.
*/
addListener(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the
* following:
Expand All @@ -9995,16 +10010,31 @@ export interface Browser {
*/
addListener(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Emitted when a new browser context is created.
*/
prependListener(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the
* following:
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/client/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ export class Browser extends ChannelOwner<channels.BrowserChannel> implements ap
// and will be configured later in `_connectToBrowserType`.
if (this._browserType)
this._setupBrowserContext(context);
this.emit(Events.Browser.Context, context);
}

private _setupBrowserContext(context: BrowserContext) {
Expand Down
1 change: 1 addition & 0 deletions packages/playwright-core/src/client/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export const Events = {
},

Browser: {
Context: 'context',
Disconnected: 'disconnected'
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,17 @@ class BrowserTracker {
readonly browser: api.Browser;
private _callbacks: BrowserTrackerCallbacks;
private _contextListeners = new Map<api.BrowserContext, Disposable[]>();
private _browserListeners: Disposable[] = [];

static async create(descriptor: BrowserDescriptor, callbacks: BrowserTrackerCallbacks): Promise<BrowserTracker | undefined> {
try {
const browser = await connectToBrowserAcrossVersions(descriptor);
const slot = new BrowserTracker(descriptor, browser, callbacks);
for (const context of browser.contexts())
slot._wireContext(context);
slot._browserListeners.push(eventsHelper.addEventListener(browser, 'context', (context: api.BrowserContext) => {
slot._wireContext(context);
}));
return slot;
} catch {
return undefined;
Expand All @@ -65,6 +69,8 @@ class BrowserTracker {
}

dispose() {
this._browserListeners.forEach(d => d.dispose());
this._browserListeners = [];
for (const listeners of this._contextListeners.values())
listeners.forEach(d => d.dispose());
this._contextListeners.clear();
Expand Down
30 changes: 30 additions & 0 deletions packages/playwright-core/types/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9974,6 +9974,11 @@ export interface Browser {
*/
behavior?: 'wait'|'ignoreErrors'|'default'
}): Promise<void>;
/**
* Emitted when a new browser context is created.
*/
on(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the
* following:
Expand All @@ -9982,11 +9987,21 @@ export interface Browser {
*/
on(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
once(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Adds an event listener that will be automatically removed after it is triggered once. See `addListener` for more information about this event.
*/
once(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Emitted when a new browser context is created.
*/
addListener(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the
* following:
Expand All @@ -9995,16 +10010,31 @@ export interface Browser {
*/
addListener(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
removeListener(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Removes an event listener added by `on` or `addListener`.
*/
off(event: 'disconnected', listener: (browser: Browser) => any): this;

/**
* Emitted when a new browser context is created.
*/
prependListener(event: 'context', listener: (browserContext: BrowserContext) => any): this;

/**
* Emitted when Browser gets disconnected from the browser application. This might happen because of one of the
* following:
Expand Down
7 changes: 7 additions & 0 deletions tests/library/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,13 @@ test('should dispatch page.on(close) upon browser.close and reject evaluate', as
expect(error.message).toContain(kTargetClosedErrorMessage);
});

test('should fire context event on newContext', async ({ browser }) => {
const events = [];
browser.on('context', ctx => events.push(ctx));
const context = await browser.newContext();
expect(events).toEqual([context]);
});

test('newContext should not leave a context upon failure', async ({ browser, toImpl }) => {
const error = await browser.newContext({
__testHookBeforeSetStorageState: () => Promise.reject(new Error('Oh my')),
Expand Down
15 changes: 14 additions & 1 deletion tests/library/multiclient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { kTargetClosedErrorMessage } from '../config/errors';
import { expect, playwrightTest } from '../config/browserTest';
import type { Browser, BrowserServer, ConnectOptions, Page } from 'playwright-core';
import type { Browser, BrowserContext, BrowserServer, ConnectOptions, Page } from 'playwright-core';

type ExtraFixtures = {
remoteServer: BrowserServer;
Expand Down Expand Up @@ -93,6 +93,19 @@ test('should connect two clients', async ({ connect, remoteServer, server }) =>
await expect(pageB2).toHaveURL(server.PREFIX + '/frames/frame.html');
});

test('should fire context event on remote newContext', async ({ connect, remoteServer }) => {
const browserA = await connect(remoteServer.wsEndpoint());
const events: BrowserContext[] = [];
browserA.on('context', ctx => events.push(ctx));

const browserB = await connect(remoteServer.wsEndpoint());
const contextB = await browserB.newContext();

await expect.poll(() => events).toHaveLength(1);
expect(browserA.contexts()).toEqual([events[0]]);
expect(events[0]).not.toBe(contextB);
});

test('should have separate default timeouts', async ({ twoPages }) => {
const { pageA, pageB } = twoPages;
pageA.setDefaultTimeout(500);
Expand Down
20 changes: 16 additions & 4 deletions tests/mcp/cli-fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,22 @@ export const test = baseTest.extend<{
for (const pid of allPids)
killProcessGroup(pid);

const daemonDir = path.join(test.info().outputDir, 'daemon');
const userDataDirs = await fs.promises.readdir(daemonDir).catch(() => []);
for (const dir of userDataDirs.filter(f => f.startsWith('ud-')))
await fs.promises.rm(path.join(daemonDir, dir), { recursive: true, force: true }).catch(() => {});
const daemonDir = test.info().outputPath('daemon');
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change seems entirely unrelated!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

absolutely, but it was useful utility for having copilot find the bug this fixes by itself.

for (const dir of await fs.promises.readdir(daemonDir).catch<string[]>(() => [])) {
if (dir.startsWith('ud-')) {
await fs.promises.rm(path.join(daemonDir, dir), { recursive: true, force: true }).catch(() => {});
continue;
}
const workspacePath = path.join(daemonDir, dir);
for (const entry of await fs.promises.readdir(workspacePath).catch<string[]>(() => [])) {
if (!entry.endsWith('.err'))
continue;
const errPath = path.join(workspacePath, entry);
if ((await fs.promises.stat(errPath)).size === 0)
continue;
await test.info().attach(entry, { path: errPath, contentType: 'text/plain' });
}
}
},
boundBrowser: async ({ mcpBrowser, playwright }, use) => {
const browserName = (mcpBrowser === 'chrome' || mcpBrowser === 'msedge') ? 'chromium' : mcpBrowser;
Expand Down
11 changes: 6 additions & 5 deletions tests/mcp/dashboard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,14 @@ test('should show one row per context for a single browser', async ({ boundBrows
const contextA = await boundBrowser.newContext();
const pageA = await contextA.newPage();
await pageA.goto(server.EMPTY_PAGE);
const contextB = await boundBrowser.newContext();
const pageB = await contextB.newPage();
await pageB.goto(server.EMPTY_PAGE);

const dashboard = await startDashboardServer();
const chips = dashboard.locator('.session-chip');
await expect(chips).toHaveCount(1);

const contextB = await boundBrowser.newContext();
const pageB = await contextB.newPage();
await pageB.goto(server.EMPTY_PAGE);
await expect(chips).toHaveCount(2);
});

Expand Down Expand Up @@ -97,8 +99,7 @@ test('should show current workspace sessions first', async ({ cli, server, start
});
});

test('should activate session when show is called with -s', async ({ cli, server, startDashboardServer, mcpBrowser }) => {
test.fixme(mcpBrowser === 'firefox', 'race condition gonna be fixed through https://github.com/microsoft/playwright/pull/40315');
test('should activate session when show is called with -s', async ({ cli, server, startDashboardServer }) => {
await cli('-s=sessA', 'open', server.EMPTY_PAGE);
await cli('-s=sessB', 'open', server.EMPTY_PAGE);

Expand Down
Loading