diff --git a/packages/playwright-core/src/cli/driver.ts b/packages/playwright-core/src/cli/driver.ts index 372802470c..9af655c792 100644 --- a/packages/playwright-core/src/cli/driver.ts +++ b/packages/playwright-core/src/cli/driver.ts @@ -100,7 +100,7 @@ class ProtocolHandler { } async navigate(params: { url: string }) { - await this._controller.navigateAll(params.url); + await this._controller.navigate(params.url); } async setMode(params: { mode: Mode, language?: string, file?: string }) { @@ -112,11 +112,11 @@ class ProtocolHandler { } async highlight(params: { selector: string }) { - await this._controller.highlightAll(params.selector); + await this._controller.highlight(params.selector); } async hideHighlight() { - await this._controller.hideHighlightAll(); + await this._controller.hideHighlight(); } async closeAllBrowsers() { diff --git a/packages/playwright-core/src/client/playwright.ts b/packages/playwright-core/src/client/playwright.ts index 874eb1dbe2..6c8224693f 100644 --- a/packages/playwright-core/src/client/playwright.ts +++ b/packages/playwright-core/src/client/playwright.ts @@ -73,10 +73,6 @@ export class Playwright extends ChannelOwner { (global as any)._playwrightInstance = this; } - async _hideHighlight() { - await this._channel.hideHighlight(); - } - _setSelectors(selectors: Selectors) { const selectorsOwner = SelectorsOwner.from(this._initializer.selectors); this.selectors._removeChannel(selectorsOwner); diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 1bb5bf0214..79981d7fc0 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -316,8 +316,6 @@ scheme.PlaywrightNewRequestParams = tObject({ scheme.PlaywrightNewRequestResult = tObject({ request: tChannel(['APIRequestContext']), }); -scheme.PlaywrightHideHighlightParams = tOptional(tObject({})); -scheme.PlaywrightHideHighlightResult = tOptional(tObject({})); scheme.RecorderSource = tObject({ isRecorded: tBoolean, id: tString, @@ -355,22 +353,22 @@ scheme.DebugControllerSetReportStateChangedParams = tObject({ scheme.DebugControllerSetReportStateChangedResult = tOptional(tObject({})); scheme.DebugControllerResetForReuseParams = tOptional(tObject({})); scheme.DebugControllerResetForReuseResult = tOptional(tObject({})); -scheme.DebugControllerNavigateAllParams = tObject({ +scheme.DebugControllerNavigateParams = tObject({ url: tString, }); -scheme.DebugControllerNavigateAllResult = tOptional(tObject({})); +scheme.DebugControllerNavigateResult = tOptional(tObject({})); scheme.DebugControllerSetRecorderModeParams = tObject({ mode: tEnum(['inspecting', 'recording', 'none']), language: tOptional(tString), file: tOptional(tString), }); scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({})); -scheme.DebugControllerHighlightAllParams = tObject({ +scheme.DebugControllerHighlightParams = tObject({ selector: tString, }); -scheme.DebugControllerHighlightAllResult = tOptional(tObject({})); -scheme.DebugControllerHideHighlightAllParams = tOptional(tObject({})); -scheme.DebugControllerHideHighlightAllResult = tOptional(tObject({})); +scheme.DebugControllerHighlightResult = tOptional(tObject({})); +scheme.DebugControllerHideHighlightParams = tOptional(tObject({})); +scheme.DebugControllerHideHighlightResult = tOptional(tObject({})); scheme.DebugControllerKillParams = tOptional(tObject({})); scheme.DebugControllerKillResult = tOptional(tObject({})); scheme.DebugControllerCloseAllBrowsersParams = tOptional(tObject({})); diff --git a/packages/playwright-core/src/remote/playwrightConnection.ts b/packages/playwright-core/src/remote/playwrightConnection.ts index 1096ecabdc..8ec0a64f34 100644 --- a/packages/playwright-core/src/remote/playwrightConnection.ts +++ b/packages/playwright-core/src/remote/playwrightConnection.ts @@ -142,7 +142,6 @@ export class PlaywrightConnection { private _initDebugControllerMode(): DebugControllerDispatcher { this._debugLog(`engaged reuse controller mode`); const playwright = this._preLaunched.playwright!; - this._cleanups.push(() => gracefullyCloseAll()); // Always create new instance based on the reused Playwright instance. return new DebugControllerDispatcher(this._dispatcherConnection, playwright.debugController); } @@ -169,7 +168,7 @@ export class PlaywrightConnection { if (!browser) { browser = await playwright[(this._options.browserName || 'chromium') as 'chromium'].launch(serverSideCallMetadata(), { ...this._options.launchOptions, - headless: false, + headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS, }); browser.on(Browser.Events.Disconnected, () => { // Underlying browser did close for some reason - force disconnect the client. diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 38178350df..320b0e2f8a 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -670,4 +670,5 @@ const defaultNewContextParamValues: channels.BrowserNewContextForReuseParams = { acceptDownloads: true, strictSelectors: false, serviceWorkers: 'allow', + locale: 'en-US', }; diff --git a/packages/playwright-core/src/server/debugController.ts b/packages/playwright-core/src/server/debugController.ts index 570ee2716c..87b7af7766 100644 --- a/packages/playwright-core/src/server/debugController.ts +++ b/packages/playwright-core/src/server/debugController.ts @@ -42,7 +42,6 @@ export class DebugController extends SdkObject { private _autoCloseAllowed = false; private _trackHierarchyListener: InstrumentationListener | undefined; private _playwright: Playwright; - private _reuseBrowser = false; constructor(playwright: Playwright) { super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'DebugController'); @@ -62,7 +61,6 @@ export class DebugController extends SdkObject { if (enabled && !this._trackHierarchyListener) { this._trackHierarchyListener = { onPageOpen: () => this._emitSnapshot(), - onPageNavigated: () => this._emitSnapshot(), onPageClose: () => this._emitSnapshot(), }; this._playwright.instrumentation.addListener(this._trackHierarchyListener, null); @@ -80,7 +78,7 @@ export class DebugController extends SdkObject { await context.resetForReuse(internalMetadata, null); } - async navigateAll(url: string) { + async navigate(url: string) { for (const p of this._playwright.allPages()) await p.mainFrame().goto(internalMetadata, url); } @@ -98,7 +96,7 @@ export class DebugController extends SdkObject { } if (!this._playwright.allBrowsers().length) - await this._playwright.chromium.launch(internalMetadata, { headless: false }); + await this._playwright.chromium.launch(internalMetadata, { headless: !!process.env.PW_DEBUG_CONTROLLER_HEADLESS }); // Create page if none. const pages = this._playwright.allPages(); if (!pages.length) { @@ -132,12 +130,16 @@ export class DebugController extends SdkObject { this._autoCloseTimer = setTimeout(heartBeat, 30000); } - async highlightAll(selector: string) { + async highlight(selector: string) { for (const recorder of await this._allRecorders()) recorder.setHighlightedSelector(selector); } - async hideHighlightAll() { + async hideHighlight() { + // Hide all active recorder highlights. + for (const recorder of await this._allRecorders()) + recorder.setHighlightedSelector(''); + // Hide all locator.highlight highlights. await this._playwright.hideHighlight(); } diff --git a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts index 555ec810a2..b2ebcc99c2 100644 --- a/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/debugControllerDispatcher.ts @@ -44,20 +44,20 @@ export class DebugControllerDispatcher extends Dispatcher { - await this._object.hideHighlight(); - } - async cleanup() { // Cleanup contexts upon disconnect. await this._browserDispatcher?.cleanupContexts(); diff --git a/packages/playwright-core/src/server/frames.ts b/packages/playwright-core/src/server/frames.ts index f39df5ab84..20520c0168 100644 --- a/packages/playwright-core/src/server/frames.ts +++ b/packages/playwright-core/src/server/frames.ts @@ -462,8 +462,6 @@ export class FrameManager { private _fireInternalFrameNavigation(frame: Frame, event: NavigationEvent) { frame.emit(Frame.Events.InternalNavigation, event); - if (event.isPublic && !frame.parentFrame()) - frame.instrumentation.onPageNavigated(frame._page, event.url); } } diff --git a/packages/playwright-core/src/server/instrumentation.ts b/packages/playwright-core/src/server/instrumentation.ts index 2fbb3d1143..3f0f8b9b53 100644 --- a/packages/playwright-core/src/server/instrumentation.ts +++ b/packages/playwright-core/src/server/instrumentation.ts @@ -63,7 +63,6 @@ export interface Instrumentation { onAfterCall(sdkObject: SdkObject, metadata: CallMetadata): Promise; onEvent(sdkObject: SdkObject, metadata: CallMetadata): void; onPageOpen(page: Page): void; - onPageNavigated(page: Page, url: string): void; onPageClose(page: Page): void; onBrowserOpen(browser: Browser): void; onBrowserClose(browser: Browser): void; @@ -76,7 +75,6 @@ export interface InstrumentationListener { onAfterCall?(sdkObject: SdkObject, metadata: CallMetadata): Promise; onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void; onPageOpen?(page: Page): void; - onPageNavigated?(page: Page, url: string): void; onPageClose?(page: Page): void; onBrowserOpen?(browser: Browser): void; onBrowserClose?(browser: Browser): void; diff --git a/packages/playwright-core/src/server/page.ts b/packages/playwright-core/src/server/page.ts index 75d2e13e59..b8c7ebbe3e 100644 --- a/packages/playwright-core/src/server/page.ts +++ b/packages/playwright-core/src/server/page.ts @@ -192,7 +192,6 @@ export class Page extends SdkObject { this.pdf = delegate.pdf.bind(delegate); this.coverage = delegate.coverage ? delegate.coverage() : null; this.selectors = browserContext.selectors(); - this.instrumentation.onPageOpen(this); } async initOpener(opener: PageDelegate | null) { @@ -218,6 +217,7 @@ export class Page extends SdkObject { // corresponding Close event after it is reported on the context. if (this.isClosed()) this.emit(Page.Events.Close); + this.instrumentation.onPageOpen(this); } initializedOrUndefined() { @@ -261,25 +261,24 @@ export class Page extends SdkObject { } _didClose() { - this.instrumentation.onPageClose(this); this._frameManager.dispose(); this._frameThrottler.dispose(); assert(this._closedState !== 'closed', 'Page closed twice'); this._closedState = 'closed'; this.emit(Page.Events.Close); this._closedPromise.resolve(); + this.instrumentation.onPageClose(this); } _didCrash() { - this.instrumentation.onPageClose(this); this._frameManager.dispose(); this._frameThrottler.dispose(); this.emit(Page.Events.Crash); this._crashedPromise.resolve(new Error('Page crashed')); + this.instrumentation.onPageClose(this); } _didDisconnect() { - this.instrumentation.onPageClose(this); this._frameManager.dispose(); this._frameThrottler.dispose(); assert(!this._disconnected, 'Page disconnected twice'); diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index 4f404b8335..97d3ef5e0b 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -519,7 +519,6 @@ export interface PlaywrightEventTarget { export interface PlaywrightChannel extends PlaywrightEventTarget, Channel { _type_Playwright: boolean; newRequest(params: PlaywrightNewRequestParams, metadata?: Metadata): Promise; - hideHighlight(params?: PlaywrightHideHighlightParams, metadata?: Metadata): Promise; } export type PlaywrightNewRequestParams = { baseURL?: string, @@ -568,9 +567,6 @@ export type PlaywrightNewRequestOptions = { export type PlaywrightNewRequestResult = { request: APIRequestContextChannel, }; -export type PlaywrightHideHighlightParams = {}; -export type PlaywrightHideHighlightOptions = {}; -export type PlaywrightHideHighlightResult = void; export interface PlaywrightEvents { } @@ -601,10 +597,10 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan _type_DebugController: boolean; setReportStateChanged(params: DebugControllerSetReportStateChangedParams, metadata?: Metadata): Promise; resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise; - navigateAll(params: DebugControllerNavigateAllParams, metadata?: Metadata): Promise; + navigate(params: DebugControllerNavigateParams, metadata?: Metadata): Promise; setRecorderMode(params: DebugControllerSetRecorderModeParams, metadata?: Metadata): Promise; - highlightAll(params: DebugControllerHighlightAllParams, metadata?: Metadata): Promise; - hideHighlightAll(params?: DebugControllerHideHighlightAllParams, metadata?: Metadata): Promise; + highlight(params: DebugControllerHighlightParams, metadata?: Metadata): Promise; + hideHighlight(params?: DebugControllerHideHighlightParams, metadata?: Metadata): Promise; kill(params?: DebugControllerKillParams, metadata?: Metadata): Promise; closeAllBrowsers(params?: DebugControllerCloseAllBrowsersParams, metadata?: Metadata): Promise; } @@ -635,13 +631,13 @@ export type DebugControllerSetReportStateChangedResult = void; export type DebugControllerResetForReuseParams = {}; export type DebugControllerResetForReuseOptions = {}; export type DebugControllerResetForReuseResult = void; -export type DebugControllerNavigateAllParams = { +export type DebugControllerNavigateParams = { url: string, }; -export type DebugControllerNavigateAllOptions = { +export type DebugControllerNavigateOptions = { }; -export type DebugControllerNavigateAllResult = void; +export type DebugControllerNavigateResult = void; export type DebugControllerSetRecorderModeParams = { mode: 'inspecting' | 'recording' | 'none', language?: string, @@ -652,16 +648,16 @@ export type DebugControllerSetRecorderModeOptions = { file?: string, }; export type DebugControllerSetRecorderModeResult = void; -export type DebugControllerHighlightAllParams = { +export type DebugControllerHighlightParams = { selector: string, }; -export type DebugControllerHighlightAllOptions = { +export type DebugControllerHighlightOptions = { }; -export type DebugControllerHighlightAllResult = void; -export type DebugControllerHideHighlightAllParams = {}; -export type DebugControllerHideHighlightAllOptions = {}; -export type DebugControllerHideHighlightAllResult = void; +export type DebugControllerHighlightResult = void; +export type DebugControllerHideHighlightParams = {}; +export type DebugControllerHideHighlightOptions = {}; +export type DebugControllerHideHighlightResult = void; export type DebugControllerKillParams = {}; export type DebugControllerKillOptions = {}; export type DebugControllerKillResult = void; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 28207028b6..de0c075bf9 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -636,8 +636,6 @@ Playwright: returns: request: APIRequestContext - hideHighlight: - RecorderSource: type: object properties: @@ -666,7 +664,7 @@ DebugController: resetForReuse: - navigateAll: + navigate: parameters: url: string @@ -681,11 +679,11 @@ DebugController: language: string? file: string? - highlightAll: + highlight: parameters: selector: string - hideHighlightAll: + hideHighlight: kill: diff --git a/tests/config/debugControllerBackend.ts b/tests/config/debugControllerBackend.ts new file mode 100644 index 0000000000..7fc51f89e9 --- /dev/null +++ b/tests/config/debugControllerBackend.ts @@ -0,0 +1,176 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import WebSocket from 'ws'; +import { EventEmitter } from 'events'; + +export type ProtocolRequest = { + id: number; + method: string; + params: any; +}; + +export type ProtocolResponse = { + id?: number; + method?: string; + error?: { message: string; data: any; }; + params?: any; + result?: any; +}; + +export interface ConnectionTransport { + send(s: ProtocolRequest): void; + close(): void; // Note: calling close is expected to issue onclose at some point. + isClosed(): boolean, + onmessage?: (message: ProtocolResponse) => void, + onclose?: () => void, +} + +class WebSocketTransport implements ConnectionTransport { + private _ws: WebSocket; + + onmessage?: (message: ProtocolResponse) => void; + onclose?: () => void; + readonly wsEndpoint: string; + + static async connect(url: string, headers: Record = {}): Promise { + const transport = new WebSocketTransport(url, headers); + await new Promise((fulfill, reject) => { + transport._ws.addEventListener('open', async () => { + fulfill(transport); + }); + transport._ws.addEventListener('error', event => { + reject(new Error('WebSocket error: ' + event.message)); + transport._ws.close(); + }); + }); + return transport; + } + + constructor(url: string, headers: Record = {}) { + this.wsEndpoint = url; + this._ws = new WebSocket(url, [], { + perMessageDeflate: false, + maxPayload: 256 * 1024 * 1024, // 256Mb, + handshakeTimeout: 30000, + headers + }); + + this._ws.addEventListener('message', event => { + try { + if (this.onmessage) + this.onmessage.call(null, JSON.parse(event.data.toString())); + } catch (e) { + this._ws.close(); + } + }); + + this._ws.addEventListener('close', event => { + if (this.onclose) + this.onclose.call(null); + }); + // Prevent Error: read ECONNRESET. + this._ws.addEventListener('error', () => {}); + } + + isClosed() { + return this._ws.readyState === WebSocket.CLOSING || this._ws.readyState === WebSocket.CLOSED; + } + + send(message: ProtocolRequest) { + this._ws.send(JSON.stringify(message)); + } + + close() { + this._ws.close(); + } + + async closeAndWait() { + const promise = new Promise(f => this._ws.once('close', f)); + this.close(); + await promise; // Make sure to await the actual disconnect. + } +} + +export class Backend extends EventEmitter { + private static _lastId = 0; + private _callbacks = new Map void, reject: (e: Error) => void }>(); + private _transport!: WebSocketTransport; + + constructor() { + super(); + } + + async connect(wsEndpoint: string) { + this._transport = await WebSocketTransport.connect(wsEndpoint, { + 'x-playwright-debug-controller': 'true' + }); + this._transport.onmessage = (message: any) => { + if (!message.id) { + this.emit(message.method, message.params); + return; + } + const pair = this._callbacks.get(message.id); + if (!pair) + return; + this._callbacks.delete(message.id); + if (message.error) { + const error = new Error(message.error.error?.message || message.error.value); + error.stack = message.error.error?.stack; + pair.reject(error); + } else { + pair.fulfill(message.result); + } + }; + } + + async resetForReuse() { + await this._send('resetForReuse'); + } + + async navigate(params: { url: string }) { + await this._send('navigate', params); + } + + async setMode(params: { mode: 'none' | 'inspecting' | 'recording', language?: string, file?: string }) { + await this._send('setRecorderMode', params); + } + + async setReportStateChanged(params: { enabled: boolean }) { + await this._send('setReportStateChanged', params); + } + + async highlight(params: { selector: string }) { + await this._send('highlight', params); + } + + async hideHighlight() { + await this._send('hideHighlight'); + } + + async kill() { + this._send('kill'); + } + + private _send(method: string, params: any = {}): Promise { + return new Promise((fulfill, reject) => { + const id = ++Backend._lastId; + const command = { id, guid: 'DebugController', method, params, metadata: {} }; + this._transport.send(command as any); + this._callbacks.set(id, { fulfill, reject }); + }); + } +} diff --git a/tests/library/debug-controller.spec.ts b/tests/library/debug-controller.spec.ts new file mode 100644 index 0000000000..22caeb4246 --- /dev/null +++ b/tests/library/debug-controller.spec.ts @@ -0,0 +1,155 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, playwrightTest as baseTest } from '../config/browserTest'; +import { PlaywrightServer } from '../../packages/playwright-core/lib/remote/playwrightServer'; +import { createGuid } from '../../packages/playwright-core/lib/utils'; +import { Backend } from '../config/debugControllerBackend'; +import type { Browser, BrowserContext } from '@playwright/test'; + +type Fixtures = { + wsEndpoint: string; + backend: Backend; + connectedBrowser: Browser & { _newContextForReuse: () => Promise }; +}; + +const test = baseTest.extend({ + wsEndpoint: async ({ }, use) => { + process.env.PW_DEBUG_CONTROLLER_HEADLESS = '1'; + const server = new PlaywrightServer({ path: '/' + createGuid(), maxConnections: Number.MAX_VALUE, enableSocksProxy: false }); + const wsEndpoint = await server.listen(); + await use(wsEndpoint); + await server.close(); + }, + backend: async ({ wsEndpoint }, use) => { + const backend = new Backend(); + await backend.connect(wsEndpoint); + await use(backend); + }, + connectedBrowser: async ({ playwright, wsEndpoint }, use) => { + (playwright.chromium as any)._defaultConnectOptions = { + wsEndpoint, + headers: { 'x-playwright-reuse-context': '1', }, + }; + const browser = await playwright.chromium.launch(); + await use(browser as any); + await browser.close(); + }, +}); + +test.slow(true, 'All controller tests are slow'); + +test('should pick element', async ({ backend, connectedBrowser }) => { + const events = []; + backend.on('inspectRequested', event => events.push(event)); + + await backend.setMode({ mode: 'inspecting' }); + + const context = await connectedBrowser._newContextForReuse(); + const [page] = context.pages(); + + await page.locator('body').click(); + await page.locator('body').click(); + + expect(events).toEqual([ + { + selector: 'body', + locators: [ + { name: 'javascript', value: 'locator(\'body\')' }, + { name: 'python', value: 'locator("body")' }, + { name: 'java', value: 'locator("body")' }, + { name: 'csharp', value: 'Locator("body")' } + ] + }, { + selector: 'body', + locators: [ + { name: 'javascript', value: 'locator(\'body\')' }, + { name: 'python', value: 'locator("body")' }, + { name: 'java', value: 'locator("body")' }, + { name: 'csharp', value: 'Locator("body")' } + ] + }, + ]); + + // No events after mode disabled + await backend.setMode({ mode: 'none' }); + await page.locator('body').click(); + expect(events).toHaveLength(2); +}); + +test('should report pages', async ({ backend, connectedBrowser }) => { + const events = []; + backend.on('stateChanged', event => events.push(event)); + await backend.setReportStateChanged({ enabled: true }); + + const context = await connectedBrowser._newContextForReuse(); + const page1 = await context.newPage(); + const page2 = await context.newPage(); + await page1.close(); + await page2.close(); + + await backend.setReportStateChanged({ enabled: false }); + const page3 = await context.newPage(); + await page3.close(); + + expect(events).toEqual([ + { + pageCount: 1, + }, { + pageCount: 2, + }, { + pageCount: 1, + }, { + pageCount: 0, + } + ]); +}); + +test('should navigate all', async ({ backend, connectedBrowser }) => { + const context = await connectedBrowser._newContextForReuse(); + const page1 = await context.newPage(); + const page2 = await context.newPage(); + + await backend.navigate({ url: 'data:text/plain,Hello world' }); + + expect(await page1.evaluate(() => window.location.href)).toBe('data:text/plain,Hello world'); + expect(await page2.evaluate(() => window.location.href)).toBe('data:text/plain,Hello world'); +}); + +test('should reset for reuse', async ({ backend, connectedBrowser }) => { + const context = await connectedBrowser._newContextForReuse(); + const page1 = await context.newPage(); + const page2 = await context.newPage(); + await backend.navigate({ url: 'data:text/plain,Hello world' }); + + const context2 = await connectedBrowser._newContextForReuse(); + expect(await context2.pages()[0].evaluate(() => window.location.href)).toBe('about:blank'); + expect(await page1.evaluate(() => window.location.href)).toBe('about:blank'); + expect(await page2.evaluate(() => window.location.href).catch(e => e.message)).toContain('Target page, context or browser has been closed'); +}); + +test('should highlight all', async ({ backend, connectedBrowser }) => { + const context = await connectedBrowser._newContextForReuse(); + const page1 = await context.newPage(); + const page2 = await context.newPage(); + await backend.navigate({ url: 'data:text/html,' }); + await backend.highlight({ selector: 'button' }); + await expect(page1.getByText('locator(\'button\')')).toBeVisible(); + await expect(page2.getByText('locator(\'button\')')).toBeVisible(); + await backend.hideHighlight(); + await expect(page1.getByText('locator(\'button\')')).toBeHidden({ timeout: 1000000 }); + await expect(page2.getByText('locator(\'button\')')).toBeHidden(); +});