mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
feat(innerloop): allow reusing browsers over the remote connection (#16065)
This commit is contained in:
parent
557db4c35e
commit
5a79054544
@ -41,7 +41,7 @@
|
||||
"roll": "node utils/roll_browser.js",
|
||||
"check-deps": "node utils/check_deps.js",
|
||||
"build-android-driver": "./utils/build_android_driver.sh",
|
||||
"innerloop": "playwright launch-server --browser=chromium --config=utils/innerloop-server.config.json"
|
||||
"innerloop": "playwright run-server --reuse-browser"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
@ -54,7 +54,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher {
|
||||
path = options.wsPath.startsWith('/') ? options.wsPath : `/${options.wsPath}`;
|
||||
|
||||
// 2. Start the server
|
||||
const server = new PlaywrightServer(path, Infinity, false, browser);
|
||||
const server = new PlaywrightServer('use-pre-launched-browser', { path, maxClients: Infinity, enableSocksProxy: false, preLaunchedBrowser: browser });
|
||||
const wsEndpoint = await server.listen(options.port);
|
||||
|
||||
// 3. Return the BrowserServer interface
|
||||
|
@ -267,12 +267,13 @@ program
|
||||
|
||||
program
|
||||
.command('run-server', { hidden: true })
|
||||
.option('--reuse-browser', 'Whether to reuse the browser instance')
|
||||
.option('--port <port>', 'Server port')
|
||||
.option('--path <path>', 'Endpoint Path', '/')
|
||||
.option('--max-clients <maxClients>', 'Maximum clients')
|
||||
.option('--no-socks-proxy', 'Disable Socks Proxy')
|
||||
.action(function(options) {
|
||||
runServer(options.port ? +options.port : undefined, options.path, options.maxClients ? +options.maxClients : Infinity, options.socksProxy).catch(logErrorAndExit);
|
||||
runServer(options.port ? +options.port : undefined, options.path, options.maxClients ? +options.maxClients : Infinity, options.socksProxy, options.reuseBrowser).catch(logErrorAndExit);
|
||||
});
|
||||
|
||||
program
|
||||
|
@ -50,8 +50,8 @@ export function runDriver() {
|
||||
};
|
||||
}
|
||||
|
||||
export async function runServer(port: number | undefined, path: string = '/', maxClients: number = Infinity, enableSocksProxy: boolean = true) {
|
||||
const server = await PlaywrightServer.startDefault({ path, maxClients, enableSocksProxy });
|
||||
export async function runServer(port: number | undefined, path = '/', maxClients = Infinity, enableSocksProxy = true, reuseBrowser = false) {
|
||||
const server = new PlaywrightServer(reuseBrowser ? 'reuse-browser' : 'auto', { path, maxClients, enableSocksProxy });
|
||||
const wsEndpoint = await server.listen(port);
|
||||
process.on('exit', () => server.close().catch(console.error));
|
||||
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
|
||||
|
@ -23,7 +23,7 @@ function launchGridBrowserWorker(gridURL: string, agentId: string, workerId: str
|
||||
const log = debug(`pw:grid:worker:${workerId}`);
|
||||
log('created');
|
||||
const ws = new WebSocket(gridURL.replace('http://', 'ws://') + `/registerWorker?agentId=${agentId}&workerId=${workerId}`);
|
||||
new PlaywrightConnection(ws, true, browserAlias, true, undefined, log, async () => {
|
||||
new PlaywrightConnection('auto', ws, { enableSocksProxy: true, browserAlias, headless: true }, { playwright: null, browser: null }, log, async () => {
|
||||
log('exiting process');
|
||||
setTimeout(() => process.exit(0), 30000);
|
||||
// Meanwhile, try to gracefully close all browsers.
|
||||
|
@ -1,7 +1,7 @@
|
||||
[*]
|
||||
../client/
|
||||
../common/
|
||||
../dispatchers/
|
||||
../server/
|
||||
../server/dispatchers/
|
||||
../utils/
|
||||
../utilsBundle.ts
|
||||
|
@ -15,13 +15,26 @@
|
||||
*/
|
||||
|
||||
import type { WebSocket } from '../utilsBundle';
|
||||
import type { Playwright, DispatcherScope } from '../server';
|
||||
import type { Playwright, DispatcherScope, Executable } from '../server';
|
||||
import { createPlaywright, DispatcherConnection, Root, PlaywrightDispatcher } from '../server';
|
||||
import { Browser } from '../server/browser';
|
||||
import { serverSideCallMetadata } from '../server/instrumentation';
|
||||
import { gracefullyCloseAll } from '../utils/processLauncher';
|
||||
import { registry } from '../server';
|
||||
import { SocksProxy } from '../common/socksProxy';
|
||||
import type { Mode } from './playwrightServer';
|
||||
import { assert } from '../utils';
|
||||
|
||||
type Options = {
|
||||
enableSocksProxy: boolean,
|
||||
browserAlias: string | null,
|
||||
headless: boolean,
|
||||
};
|
||||
|
||||
type PreLaunched = {
|
||||
playwright: Playwright | null;
|
||||
browser: Browser | null;
|
||||
};
|
||||
|
||||
export class PlaywrightConnection {
|
||||
private _ws: WebSocket;
|
||||
@ -30,9 +43,18 @@ export class PlaywrightConnection {
|
||||
private _cleanups: (() => Promise<void>)[] = [];
|
||||
private _debugLog: (m: string) => void;
|
||||
private _disconnected = false;
|
||||
private _preLaunched: PreLaunched;
|
||||
private _options: Options;
|
||||
private _root: Root;
|
||||
|
||||
constructor(ws: WebSocket, enableSocksProxy: boolean, browserAlias: string | undefined, headless: boolean, browser: Browser | undefined, log: (m: string) => void, onClose: () => void) {
|
||||
constructor(mode: Mode, ws: WebSocket, options: Options, preLaunched: PreLaunched, log: (m: string) => void, onClose: () => void) {
|
||||
this._ws = ws;
|
||||
this._preLaunched = preLaunched;
|
||||
this._options = options;
|
||||
if (mode === 'reuse-browser' || mode === 'use-pre-launched-browser')
|
||||
assert(preLaunched.playwright);
|
||||
if (mode === 'use-pre-launched-browser')
|
||||
assert(preLaunched.browser);
|
||||
this._onClose = onClose;
|
||||
this._debugLog = log;
|
||||
|
||||
@ -48,36 +70,36 @@ export class PlaywrightConnection {
|
||||
ws.on('close', () => this._onDisconnect());
|
||||
ws.on('error', error => this._onDisconnect(error));
|
||||
|
||||
new Root(this._dispatcherConnection, async scope => {
|
||||
if (browser)
|
||||
return await this._initPreLaunchedBrowserMode(scope, browser);
|
||||
if (!browserAlias)
|
||||
return await this._initPlaywrightConnectMode(scope, enableSocksProxy);
|
||||
return await this._initLaunchBrowserMode(scope, enableSocksProxy, browserAlias, headless);
|
||||
this._root = new Root(this._dispatcherConnection, async scope => {
|
||||
if (mode === 'reuse-browser')
|
||||
return await this._initReuseBrowsersMode(scope);
|
||||
if (mode === 'use-pre-launched-browser')
|
||||
return await this._initPreLaunchedBrowserMode(scope);
|
||||
if (!options.browserAlias)
|
||||
return await this._initPlaywrightConnectMode(scope);
|
||||
return await this._initLaunchBrowserMode(scope);
|
||||
});
|
||||
}
|
||||
|
||||
private async _initPlaywrightConnectMode(scope: DispatcherScope, enableSocksProxy: boolean) {
|
||||
private async _initPlaywrightConnectMode(scope: DispatcherScope) {
|
||||
this._debugLog(`engaged playwright.connect mode`);
|
||||
const playwright = createPlaywright('javascript');
|
||||
// Close all launched browsers on disconnect.
|
||||
this._cleanups.push(() => gracefullyCloseAll());
|
||||
|
||||
const socksProxy = enableSocksProxy ? await this._enableSocksProxy(playwright) : undefined;
|
||||
const socksProxy = this._options.enableSocksProxy ? await this._enableSocksProxy(playwright) : undefined;
|
||||
return new PlaywrightDispatcher(scope, playwright, socksProxy);
|
||||
}
|
||||
|
||||
private async _initLaunchBrowserMode(scope: DispatcherScope, enableSocksProxy: boolean, browserAlias: string, headless: boolean) {
|
||||
this._debugLog(`engaged launch mode for "${browserAlias}"`);
|
||||
const executable = registry.findExecutable(browserAlias);
|
||||
if (!executable || !executable.browserName)
|
||||
throw new Error(`Unsupported browser "${browserAlias}`);
|
||||
private async _initLaunchBrowserMode(scope: DispatcherScope) {
|
||||
this._debugLog(`engaged launch mode for "${this._options.browserAlias}"`);
|
||||
const executable = this._executableForBrowerAlias(this._options.browserAlias!);
|
||||
|
||||
const playwright = createPlaywright('javascript');
|
||||
const socksProxy = enableSocksProxy ? await this._enableSocksProxy(playwright) : undefined;
|
||||
const browser = await playwright[executable.browserName].launch(serverSideCallMetadata(), {
|
||||
const socksProxy = this._options.enableSocksProxy ? await this._enableSocksProxy(playwright) : undefined;
|
||||
const browser = await playwright[executable.browserName!].launch(serverSideCallMetadata(), {
|
||||
channel: executable.type === 'browser' ? undefined : executable.name,
|
||||
headless,
|
||||
headless: this._options.headless,
|
||||
});
|
||||
|
||||
// Close the browser on disconnect.
|
||||
@ -91,13 +113,14 @@ export class PlaywrightConnection {
|
||||
return new PlaywrightDispatcher(scope, playwright, socksProxy, browser);
|
||||
}
|
||||
|
||||
private async _initPreLaunchedBrowserMode(scope: DispatcherScope, browser: Browser) {
|
||||
private async _initPreLaunchedBrowserMode(scope: DispatcherScope) {
|
||||
this._debugLog(`engaged pre-launched mode`);
|
||||
const playwright = this._preLaunched.playwright!;
|
||||
const browser = this._preLaunched.browser!;
|
||||
browser.on(Browser.Events.Disconnected, () => {
|
||||
// Underlying browser did close for some reason - force disconnect the client.
|
||||
this.close({ code: 1001, reason: 'Browser closed' });
|
||||
});
|
||||
const playwright = browser.options.rootSdkObject as Playwright;
|
||||
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, undefined, browser);
|
||||
// In pre-launched mode, keep the browser and just cleanup new contexts.
|
||||
// TODO: it is technically possible to launch more browsers over protocol.
|
||||
@ -105,6 +128,31 @@ export class PlaywrightConnection {
|
||||
return playwrightDispatcher;
|
||||
}
|
||||
|
||||
private async _initReuseBrowsersMode(scope: DispatcherScope) {
|
||||
this._debugLog(`engaged reuse browsers mode for ${this._options.browserAlias}`);
|
||||
const executable = this._executableForBrowerAlias(this._options.browserAlias!);
|
||||
const playwright = this._preLaunched.playwright!;
|
||||
|
||||
let browser = playwright.allBrowsers().find(b => b.options.name === executable.browserName);
|
||||
const remaining = playwright.allBrowsers().filter(b => b !== browser);
|
||||
for (const r of remaining)
|
||||
await r.close();
|
||||
|
||||
if (!browser) {
|
||||
browser = await playwright[executable.browserName!].launch(serverSideCallMetadata(), {
|
||||
channel: executable.type === 'browser' ? undefined : executable.name,
|
||||
headless: false,
|
||||
});
|
||||
browser.on(Browser.Events.Disconnected, () => {
|
||||
// Underlying browser did close for some reason - force disconnect the client.
|
||||
this.close({ code: 1001, reason: 'Browser closed' });
|
||||
});
|
||||
}
|
||||
|
||||
const playwrightDispatcher = new PlaywrightDispatcher(scope, playwright, undefined, browser);
|
||||
return playwrightDispatcher;
|
||||
}
|
||||
|
||||
private async _enableSocksProxy(playwright: Playwright) {
|
||||
const socksProxy = new SocksProxy();
|
||||
playwright.options.socksProxyPort = await socksProxy.listen(0);
|
||||
@ -116,8 +164,7 @@ export class PlaywrightConnection {
|
||||
private async _onDisconnect(error?: Error) {
|
||||
this._disconnected = true;
|
||||
this._debugLog(`disconnected. error: ${error}`);
|
||||
// Avoid sending any more messages over closed socket.
|
||||
this._dispatcherConnection.onmessage = () => {};
|
||||
this._root._dispose();
|
||||
this._debugLog(`starting cleanup`);
|
||||
for (const cleanup of this._cleanups)
|
||||
await cleanup().catch(() => {});
|
||||
@ -134,4 +181,11 @@ export class PlaywrightConnection {
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
|
||||
private _executableForBrowerAlias(browserAlias: string): Executable {
|
||||
const executable = registry.findExecutable(browserAlias);
|
||||
if (!executable || !executable.browserName)
|
||||
throw new Error(`Unsupported browser "${browserAlias}`);
|
||||
return executable;
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,12 @@
|
||||
|
||||
import { debug, wsServer } from '../utilsBundle';
|
||||
import type { WebSocketServer } from '../utilsBundle';
|
||||
import * as http from 'http';
|
||||
import http from 'http';
|
||||
import type { Browser } from '../server/browser';
|
||||
import type { Playwright } from '../server/playwright';
|
||||
import { createPlaywright } from '../server/playwright';
|
||||
import { PlaywrightConnection } from './playwrightConnection';
|
||||
import { assert } from '../utils';
|
||||
|
||||
const debugLog = debug('pw:server');
|
||||
|
||||
@ -30,24 +33,31 @@ function newLogger() {
|
||||
return (message: string) => debugLog(`[id=${id}] ${message}`);
|
||||
}
|
||||
|
||||
export type Mode = 'use-pre-launched-browser' | 'reuse-browser' | 'auto';
|
||||
|
||||
type ServerOptions = {
|
||||
path: string;
|
||||
maxClients: number;
|
||||
enableSocksProxy: boolean;
|
||||
preLaunchedBrowser?: Browser
|
||||
};
|
||||
|
||||
export class PlaywrightServer {
|
||||
private _path: string;
|
||||
private _maxClients: number;
|
||||
private _enableSocksProxy: boolean;
|
||||
private _browser: Browser | undefined;
|
||||
private _preLaunchedPlaywright: Playwright | null = null;
|
||||
private _wsServer: WebSocketServer | undefined;
|
||||
private _clientsCount = 0;
|
||||
private _mode: Mode;
|
||||
private _options: ServerOptions;
|
||||
|
||||
static async startDefault(options: { path?: string, maxClients?: number, enableSocksProxy?: boolean } = {}): Promise<PlaywrightServer> {
|
||||
const { path = '/ws', maxClients = 1, enableSocksProxy = true } = options;
|
||||
return new PlaywrightServer(path, maxClients, enableSocksProxy);
|
||||
}
|
||||
|
||||
constructor(path: string, maxClients: number, enableSocksProxy: boolean, browser?: Browser) {
|
||||
this._path = path;
|
||||
this._maxClients = maxClients;
|
||||
this._enableSocksProxy = enableSocksProxy;
|
||||
this._browser = browser;
|
||||
constructor(mode: Mode, options: ServerOptions) {
|
||||
this._mode = mode;
|
||||
this._options = options;
|
||||
if (mode === 'use-pre-launched-browser') {
|
||||
assert(options.preLaunchedBrowser);
|
||||
this._preLaunchedPlaywright = options.preLaunchedBrowser.options.rootSdkObject as Playwright;
|
||||
}
|
||||
if (mode === 'reuse-browser')
|
||||
this._preLaunchedPlaywright = createPlaywright('javascript');
|
||||
}
|
||||
|
||||
async listen(port: number = 0): Promise<string> {
|
||||
@ -63,33 +73,37 @@ export class PlaywrightServer {
|
||||
reject(new Error('Could not bind server socket'));
|
||||
return;
|
||||
}
|
||||
const wsEndpoint = typeof address === 'string' ? `${address}${this._path}` : `ws://127.0.0.1:${address.port}${this._path}`;
|
||||
const wsEndpoint = typeof address === 'string' ? `${address}${this._options.path}` : `ws://127.0.0.1:${address.port}${this._options.path}`;
|
||||
resolve(wsEndpoint);
|
||||
}).on('error', reject);
|
||||
});
|
||||
|
||||
debugLog('Listening at ' + wsEndpoint);
|
||||
|
||||
this._wsServer = new wsServer({ server, path: this._path });
|
||||
this._wsServer = new wsServer({ server, path: this._options.path });
|
||||
const originalShouldHandle = this._wsServer.shouldHandle.bind(this._wsServer);
|
||||
this._wsServer.shouldHandle = request => originalShouldHandle(request) && this._clientsCount < this._maxClients;
|
||||
this._wsServer.shouldHandle = request => originalShouldHandle(request) && this._clientsCount < this._options.maxClients;
|
||||
this._wsServer.on('connection', async (ws, request) => {
|
||||
if (this._clientsCount >= this._maxClients) {
|
||||
if (this._clientsCount >= this._options.maxClients) {
|
||||
ws.close(1013, 'Playwright Server is busy');
|
||||
return;
|
||||
}
|
||||
const url = new URL('http://localhost' + (request.url || ''));
|
||||
const browserHeader = request.headers['x-playwright-browser'];
|
||||
const browserAlias = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader);
|
||||
const browserAlias = url.searchParams.get('browser') || (Array.isArray(browserHeader) ? browserHeader[0] : browserHeader) || null;
|
||||
const headlessHeader = request.headers['x-playwright-headless'];
|
||||
const headlessValue = url.searchParams.get('headless') || (Array.isArray(headlessHeader) ? headlessHeader[0] : headlessHeader);
|
||||
const proxyHeader = request.headers['x-playwright-proxy'];
|
||||
const proxyValue = url.searchParams.get('proxy') || (Array.isArray(proxyHeader) ? proxyHeader[0] : proxyHeader);
|
||||
const enableSocksProxy = this._enableSocksProxy && proxyValue === '*';
|
||||
const enableSocksProxy = this._options.enableSocksProxy && proxyValue === '*';
|
||||
this._clientsCount++;
|
||||
const log = newLogger();
|
||||
log(`serving connection: ${request.url}`);
|
||||
const connection = new PlaywrightConnection(ws, enableSocksProxy, browserAlias, headlessValue !== '0', this._browser, log, () => this._clientsCount--);
|
||||
const connection = new PlaywrightConnection(
|
||||
this._mode, ws,
|
||||
{ enableSocksProxy, browserAlias, headless: headlessValue !== '0' },
|
||||
{ playwright: this._preLaunchedPlaywright, browser: this._options.preLaunchedBrowser || null },
|
||||
log, () => this._clientsCount--);
|
||||
(ws as any)[kConnectionSymbol] = connection;
|
||||
});
|
||||
|
||||
|
@ -75,6 +75,7 @@ export abstract class Browser extends SdkObject {
|
||||
super(options.rootSdkObject, 'browser');
|
||||
this.attribution.browser = this;
|
||||
this.options = options;
|
||||
this.instrumentation.onBrowserOpen(this);
|
||||
}
|
||||
|
||||
abstract doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext>;
|
||||
@ -146,6 +147,7 @@ export abstract class Browser extends SdkObject {
|
||||
if (this._defaultContext)
|
||||
this._defaultContext._browserClosed();
|
||||
this.emit(Browser.Events.Disconnected);
|
||||
this.instrumentation.onBrowserClose(this);
|
||||
}
|
||||
|
||||
async close() {
|
||||
|
@ -159,9 +159,15 @@ export abstract class BrowserContext extends SdkObject {
|
||||
|
||||
await this._cancelAllRoutesInFlight();
|
||||
|
||||
const [page, ...otherPages] = this.pages();
|
||||
for (const page of otherPages)
|
||||
let page: Page | undefined = this.pages()[0];
|
||||
const [, ...otherPages] = this.pages();
|
||||
for (const p of otherPages)
|
||||
await p.close(metadata);
|
||||
if (page && page._crashedPromise.isDone()) {
|
||||
await page.close(metadata);
|
||||
page = undefined;
|
||||
}
|
||||
|
||||
// Unless I do this early, setting extra http headers below does not respond.
|
||||
await page?._frameManager.closeOpenDialogs();
|
||||
await page?.mainFrame().goto(metadata, 'about:blank', { timeout: 0 });
|
||||
|
@ -145,7 +145,8 @@ async function newContextForReuse(browser: Browser, scope: DispatcherScope, para
|
||||
const { context, needsReset } = await browser.newContextForReuse(params, metadata);
|
||||
if (needsReset) {
|
||||
const oldContextDispatcher = existingDispatcher<BrowserContextDispatcher>(context);
|
||||
oldContextDispatcher._dispose();
|
||||
if (oldContextDispatcher)
|
||||
oldContextDispatcher._dispose();
|
||||
await context.resetForReuse(metadata, params);
|
||||
}
|
||||
const contextDispatcher = new BrowserContextDispatcher(scope, context);
|
||||
|
@ -64,6 +64,8 @@ export interface Instrumentation {
|
||||
onEvent(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onPageOpen(page: Page): void;
|
||||
onPageClose(page: Page): void;
|
||||
onBrowserOpen(browser: Browser): void;
|
||||
onBrowserClose(browser: Browser): void;
|
||||
}
|
||||
|
||||
export interface InstrumentationListener {
|
||||
@ -74,6 +76,8 @@ export interface InstrumentationListener {
|
||||
onEvent?(sdkObject: SdkObject, metadata: CallMetadata): void;
|
||||
onPageOpen?(page: Page): void;
|
||||
onPageClose?(page: Page): void;
|
||||
onBrowserOpen?(browser: Browser): void;
|
||||
onBrowserClose?(browser: Browser): void;
|
||||
}
|
||||
|
||||
export function createInstrumentation(): Instrumentation {
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import { Android } from './android/android';
|
||||
import { AdbBackend } from './android/backendAdb';
|
||||
import type { PlaywrightOptions } from './browser';
|
||||
import type { Browser, PlaywrightOptions } from './browser';
|
||||
import { Chromium } from './chromium/chromium';
|
||||
import { Electron } from './electron/electron';
|
||||
import { Firefox } from './firefox/firefox';
|
||||
@ -36,10 +36,13 @@ export class Playwright extends SdkObject {
|
||||
readonly webkit: WebKit;
|
||||
readonly options: PlaywrightOptions;
|
||||
private _allPages = new Set<Page>();
|
||||
private _allBrowsers = new Set<Browser>();
|
||||
|
||||
constructor(sdkLanguage: string, isInternalPlaywright: boolean) {
|
||||
super({ attribution: { isInternalPlaywright }, instrumentation: createInstrumentation() } as any, undefined, 'Playwright');
|
||||
this.instrumentation.addListener({
|
||||
onBrowserOpen: browser => this._allBrowsers.add(browser),
|
||||
onBrowserClose: browser => this._allBrowsers.delete(browser),
|
||||
onPageOpen: page => this._allPages.add(page),
|
||||
onPageClose: page => this._allPages.delete(page),
|
||||
onCallLog: (sdkObject: SdkObject, metadata: CallMetadata, logName: string, message: string) => {
|
||||
@ -62,6 +65,10 @@ export class Playwright extends SdkObject {
|
||||
async hideHighlight() {
|
||||
await Promise.all([...this._allPages].map(p => p.hideHighlight().catch(() => {})));
|
||||
}
|
||||
|
||||
allBrowsers(): Browser[] {
|
||||
return [...this._allBrowsers];
|
||||
}
|
||||
}
|
||||
|
||||
export function createPlaywright(sdkLanguage: string, isInternalPlaywright: boolean = false) {
|
||||
|
@ -70,7 +70,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||
headless: [ ({ launchOptions }, use) => use(launchOptions.headless ?? true), { scope: 'worker', option: true } ],
|
||||
channel: [ ({ launchOptions }, use) => use(launchOptions.channel), { scope: 'worker', option: true } ],
|
||||
launchOptions: [ {}, { scope: 'worker', option: true } ],
|
||||
connectOptions: [ undefined, { scope: 'worker', option: true } ],
|
||||
connectOptions: [ process.env.PW_TEST_CONNECT_WS_ENDPOINT ? { wsEndpoint: process.env.PW_TEST_CONNECT_WS_ENDPOINT } : undefined, { scope: 'worker', option: true } ],
|
||||
screenshot: [ 'off', { scope: 'worker', option: true } ],
|
||||
video: [ 'off', { scope: 'worker', option: true } ],
|
||||
trace: [ 'off', { scope: 'worker', option: true } ],
|
||||
@ -512,7 +512,7 @@ export const test = _baseTest.extend<TestFixtures, WorkerFixtures>({
|
||||
testInfo.errors.push({ message: prependToError });
|
||||
}, { scope: 'test', _title: 'context' } as any],
|
||||
|
||||
_contextReuseEnabled: !!process.env.PW_REUSE_CONTEXT,
|
||||
_contextReuseEnabled: !!process.env.PW_TEST_REUSE_CONTEXT,
|
||||
|
||||
_reuseContext: async ({ video, trace, _contextReuseEnabled }, use, testInfo) => {
|
||||
const reuse = _contextReuseEnabled && !shouldCaptureVideo(normalizeVideoMode(video), testInfo) && !shouldCaptureTrace(normalizeTraceMode(trace), testInfo);
|
||||
|
@ -22,12 +22,16 @@ export type TestModeWorkerOptions = {
|
||||
mode: TestModeName;
|
||||
};
|
||||
|
||||
export type TestModeWorkerFixtures = {
|
||||
playwright: typeof import('@playwright/test');
|
||||
export type TestModeTestFixtures = {
|
||||
toImpl: (rpcObject?: any) => any;
|
||||
};
|
||||
|
||||
export const testModeTest = test.extend<{}, TestModeWorkerOptions & TestModeWorkerFixtures>({
|
||||
export type TestModeWorkerFixtures = {
|
||||
toImplInWorkerScope: (rpcObject?: any) => any;
|
||||
playwright: typeof import('@playwright/test');
|
||||
};
|
||||
|
||||
export const testModeTest = test.extend<TestModeTestFixtures, TestModeWorkerOptions & TestModeWorkerFixtures>({
|
||||
mode: [ 'default', { scope: 'worker', option: true } ],
|
||||
playwright: [ async ({ mode }, run) => {
|
||||
const testMode = {
|
||||
@ -42,5 +46,13 @@ export const testModeTest = test.extend<{}, TestModeWorkerOptions & TestModeWork
|
||||
await testMode.teardown();
|
||||
}, { scope: 'worker' } ],
|
||||
|
||||
toImpl: [ async ({ playwright }, run) => run((playwright as any)._toImpl), { scope: 'worker' } ],
|
||||
toImplInWorkerScope: [async ({ playwright }, use) => {
|
||||
await use((playwright as any)._toImpl);
|
||||
}, { scope: 'worker' }],
|
||||
|
||||
toImpl: async ({ toImplInWorkerScope: toImplWorker, mode }, use, testInfo) => {
|
||||
if (mode !== 'default' || process.env.PW_TEST_REUSE_CONTEXT)
|
||||
testInfo.skip();
|
||||
await use(toImplWorker);
|
||||
},
|
||||
});
|
||||
|
@ -21,10 +21,10 @@ import { playwrightTest, expect } from '../config/browserTest';
|
||||
// Use something worker-scoped (e.g. expectScopeState) forces a new worker for this file.
|
||||
// Otherwise, a browser launched for other tests in this worker will affect the expectations.
|
||||
const it = playwrightTest.extend<{}, { expectScopeState: (object: any, golden: any) => void }>({
|
||||
expectScopeState: [ async ({ toImpl }, use) => {
|
||||
expectScopeState: [ async ({ toImplInWorkerScope }, use) => {
|
||||
await use((object, golden) => {
|
||||
golden = trimGuids(golden);
|
||||
const remoteRoot = toImpl();
|
||||
const remoteRoot = toImplInWorkerScope();
|
||||
const remoteState = trimGuids(remoteRoot._debugScopeState());
|
||||
const localRoot = object._connection._rootObject;
|
||||
const localState = trimGuids(localRoot._debugScopeState());
|
||||
|
@ -186,8 +186,6 @@ playwrightTest('should connect to an existing cdp session', async ({ browserType
|
||||
});
|
||||
|
||||
playwrightTest('should cleanup artifacts dir after connectOverCDP disconnects due to ws close', async ({ browserType, toImpl, mode }, testInfo) => {
|
||||
playwrightTest.skip(mode !== 'default');
|
||||
|
||||
const port = 9339 + testInfo.workerIndex;
|
||||
const browserServer = await browserType.launch({
|
||||
args: ['--remote-debugging-port=' + port]
|
||||
@ -207,8 +205,6 @@ playwrightTest('should cleanup artifacts dir after connectOverCDP disconnects du
|
||||
});
|
||||
|
||||
playwrightTest('should connectOverCDP and manage downloads in default context', async ({ browserType, toImpl, mode, server }, testInfo) => {
|
||||
playwrightTest.skip(mode !== 'default');
|
||||
|
||||
server.setRoute('/downloadWithFilename', (req, res) => {
|
||||
res.setHeader('Content-Type', 'application/octet-stream');
|
||||
res.setHeader('Content-Disposition', 'attachment; filename=file.txt');
|
||||
|
@ -18,8 +18,7 @@ import { contextTest, expect } from '../config/browserTest';
|
||||
import { InMemorySnapshotter } from '../../packages/playwright-core/lib/server/trace/test/inMemorySnapshotter';
|
||||
|
||||
const it = contextTest.extend<{ snapshotter: InMemorySnapshotter }>({
|
||||
snapshotter: async ({ mode, toImpl, context }, run, testInfo) => {
|
||||
testInfo.skip(mode !== 'default');
|
||||
snapshotter: async ({ toImpl, context }, run) => {
|
||||
const snapshotter = new InMemorySnapshotter(toImpl(context));
|
||||
await snapshotter.initialize();
|
||||
await run(snapshotter);
|
||||
|
@ -95,94 +95,6 @@ it('textContent should work', async ({ page, server }) => {
|
||||
expect(await page.textContent('#inner')).toBe('Text,\nmore text');
|
||||
});
|
||||
|
||||
it('textContent should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root, selector) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('textContent', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.textContent('textContent=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
|
||||
});
|
||||
|
||||
it('innerText should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root: HTMLElement, selector: string) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('innerText', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.innerText('innerText=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').innerText)).toBe('modified');
|
||||
});
|
||||
|
||||
it('innerHTML should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root, selector) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('innerHTML', createDummySelector);
|
||||
await page.setContent(`<div>Hello<span>world</span></div>`);
|
||||
const tc = await page.innerHTML('innerHTML=div');
|
||||
expect(tc).toBe('Hello<span>world</span>');
|
||||
expect(await page.evaluate(() => document.querySelector('div').innerHTML)).toBe('modified');
|
||||
});
|
||||
|
||||
it('getAttribute should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root: HTMLElement, selector: string) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.setAttribute('foo', 'modified'));
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => (e as HTMLElement).setAttribute('foo', 'modified'));
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('getAttribute', createDummySelector);
|
||||
await page.setContent(`<div foo=hello></div>`);
|
||||
const tc = await page.getAttribute('getAttribute=div', 'foo');
|
||||
expect(tc).toBe('hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').getAttribute('foo'))).toBe('modified');
|
||||
});
|
||||
|
||||
it('isVisible and isHidden should work', async ({ page }) => {
|
||||
await page.setContent(`<div>Hi</div><span></span>`);
|
||||
|
||||
@ -308,25 +220,3 @@ it('isChecked should work', async ({ page }) => {
|
||||
const error = await page.isChecked('div').catch(e => e);
|
||||
expect(error.message).toContain('Not a checkbox or radio button');
|
||||
});
|
||||
|
||||
it('isVisible should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root, selector) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.style.display = 'none');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => (e as HTMLElement).style.display = 'none');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('isVisible', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const result = await page.isVisible('isVisible=div');
|
||||
expect(result).toBe(true);
|
||||
expect(await page.evaluate(() => document.querySelector('div').style.display)).toBe('none');
|
||||
});
|
||||
|
@ -42,8 +42,7 @@ function expectContexts(pageImpl, count, browserName) {
|
||||
expect(pageImpl._delegate._contextIdToContext.size).toBe(count);
|
||||
}
|
||||
|
||||
it('should dispose context on navigation', async ({ page, server, toImpl, browserName, mode, isElectron }) => {
|
||||
it.skip(mode !== 'default');
|
||||
it('should dispose context on navigation', async ({ page, server, toImpl, browserName, isElectron }) => {
|
||||
it.skip(isElectron);
|
||||
|
||||
await page.goto(server.PREFIX + '/frames/one-frame.html');
|
||||
@ -53,8 +52,7 @@ it('should dispose context on navigation', async ({ page, server, toImpl, browse
|
||||
expectContexts(toImpl(page), 2, browserName);
|
||||
});
|
||||
|
||||
it('should dispose context on cross-origin navigation', async ({ page, server, toImpl, browserName, mode, isElectron }) => {
|
||||
it.skip(mode !== 'default');
|
||||
it('should dispose context on cross-origin navigation', async ({ page, server, toImpl, browserName, isElectron }) => {
|
||||
it.skip(isElectron);
|
||||
|
||||
await page.goto(server.PREFIX + '/frames/one-frame.html');
|
||||
|
@ -16,42 +16,38 @@
|
||||
|
||||
import { test, expect } from './pageTest';
|
||||
|
||||
test.describe('non-stalling evaluate', () => {
|
||||
test.skip(({ mode }) => mode !== 'default');
|
||||
|
||||
test('should work', async ({ page, server, toImpl }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const result = await toImpl(page.mainFrame()).nonStallingRawEvaluateInExistingMainContext('2+2');
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
|
||||
test('should throw while pending navigation', async ({ page, server, toImpl }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.evaluate(() => document.body.textContent = 'HELLO WORLD');
|
||||
let error;
|
||||
await page.route('**/empty.html', async (route, request) => {
|
||||
error = await toImpl(page.mainFrame()).nonStallingRawEvaluateInExistingMainContext('2+2').catch(e => e);
|
||||
route.abort();
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE).catch(() => {});
|
||||
expect(error.message).toContain('Frame is currently attempting a navigation');
|
||||
});
|
||||
|
||||
test('should throw when no main execution context', async ({ page, toImpl }) => {
|
||||
let errorPromise;
|
||||
page.on('frameattached', frame => {
|
||||
errorPromise = toImpl(frame).nonStallingRawEvaluateInExistingMainContext('2+2').catch(e => e);
|
||||
});
|
||||
await page.setContent('<iframe></iframe>');
|
||||
const error = await errorPromise;
|
||||
// bail out if we accidentally succeeded
|
||||
if (error === 4)
|
||||
return;
|
||||
// Testing this as a race.
|
||||
expect([
|
||||
'Frame does not yet have a main execution context',
|
||||
'Frame is currently attempting a navigation',
|
||||
'Navigation interrupted the evaluation',
|
||||
]).toContain(error.message);
|
||||
});
|
||||
test('should work', async ({ page, server, toImpl }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
const result = await toImpl(page.mainFrame()).nonStallingRawEvaluateInExistingMainContext('2+2');
|
||||
expect(result).toBe(4);
|
||||
});
|
||||
|
||||
test('should throw while pending navigation', async ({ page, server, toImpl }) => {
|
||||
await page.goto(server.EMPTY_PAGE);
|
||||
await page.evaluate(() => document.body.textContent = 'HELLO WORLD');
|
||||
let error;
|
||||
await page.route('**/empty.html', async (route, request) => {
|
||||
error = await toImpl(page.mainFrame()).nonStallingRawEvaluateInExistingMainContext('2+2').catch(e => e);
|
||||
route.abort();
|
||||
});
|
||||
await page.goto(server.EMPTY_PAGE).catch(() => {});
|
||||
expect(error.message).toContain('Frame is currently attempting a navigation');
|
||||
});
|
||||
|
||||
test('should throw when no main execution context', async ({ page, toImpl }) => {
|
||||
let errorPromise;
|
||||
page.on('frameattached', frame => {
|
||||
errorPromise = toImpl(frame).nonStallingRawEvaluateInExistingMainContext('2+2').catch(e => e);
|
||||
});
|
||||
await page.setContent('<iframe></iframe>');
|
||||
const error = await errorPromise;
|
||||
// bail out if we accidentally succeeded
|
||||
if (error === 4)
|
||||
return;
|
||||
// Testing this as a race.
|
||||
expect([
|
||||
'Frame does not yet have a main execution context',
|
||||
'Frame is currently attempting a navigation',
|
||||
'Navigation interrupted the evaluation',
|
||||
]).toContain(error.message);
|
||||
});
|
||||
|
@ -22,11 +22,9 @@ function crash({ page, toImpl, browserName, platform, mode }: any) {
|
||||
if (browserName === 'chromium') {
|
||||
page.goto('chrome://crash').catch(e => {});
|
||||
} else if (browserName === 'webkit') {
|
||||
it.skip(mode !== 'default');
|
||||
it.fixme(platform === 'darwin' && parseInt(os.release(), 10) >= 20, 'Timing out after roll on BigSur');
|
||||
toImpl(page)._delegate._session.send('Page.crash', {}).catch(e => {});
|
||||
} else if (browserName === 'firefox') {
|
||||
it.skip(mode !== 'default');
|
||||
toImpl(page)._delegate._session.send('Page.crash', {}).catch(e => {});
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,7 @@
|
||||
|
||||
import type { TestType } from '@playwright/test';
|
||||
import type { PlatformWorkerFixtures } from '../config/platformFixtures';
|
||||
import type { TestModeWorkerFixtures, TestModeWorkerOptions } from '../config/testModeFixtures';
|
||||
import type { TestModeTestFixtures, TestModeWorkerFixtures, TestModeWorkerOptions } from '../config/testModeFixtures';
|
||||
import { androidTest } from '../android/androidTest';
|
||||
import { browserTest } from '../config/browserTest';
|
||||
import { electronTest } from '../electron/electronTest';
|
||||
@ -24,7 +24,7 @@ import type { PageTestFixtures, PageWorkerFixtures } from './pageTestApi';
|
||||
import type { ServerFixtures, ServerWorkerOptions } from '../config/serverFixtures';
|
||||
export { expect } from '@playwright/test';
|
||||
|
||||
let impl: TestType<PageTestFixtures & ServerFixtures, PageWorkerFixtures & PlatformWorkerFixtures & TestModeWorkerFixtures & TestModeWorkerOptions & ServerWorkerOptions> = browserTest;
|
||||
let impl: TestType<PageTestFixtures & ServerFixtures & TestModeTestFixtures, PageWorkerFixtures & PlatformWorkerFixtures & TestModeWorkerFixtures & TestModeWorkerOptions & ServerWorkerOptions> = browserTest;
|
||||
|
||||
if (process.env.PWPAGE_IMPL === 'android')
|
||||
impl = androidTest;
|
||||
|
130
tests/page/selectors-register.spec.ts
Normal file
130
tests/page/selectors-register.spec.ts
Normal file
@ -0,0 +1,130 @@
|
||||
/**
|
||||
* Copyright 2018 Google Inc. All rights reserved.
|
||||
* Modifications 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 { test as it, expect } from './pageTest';
|
||||
|
||||
it.skip(!!process.env.PW_TEST_CONNECT_WS_ENDPOINT, 'selectors.register does not support reuse');
|
||||
|
||||
it('textContent should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root, selector) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('textContent', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.textContent('textContent=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').textContent)).toBe('modified');
|
||||
});
|
||||
|
||||
it('innerText should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root: HTMLElement, selector: string) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('innerText', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const tc = await page.innerText('innerText=div');
|
||||
expect(tc).toBe('Hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').innerText)).toBe('modified');
|
||||
});
|
||||
|
||||
it('innerHTML should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root, selector) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.textContent = 'modified');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => e.textContent = 'modified');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('innerHTML', createDummySelector);
|
||||
await page.setContent(`<div>Hello<span>world</span></div>`);
|
||||
const tc = await page.innerHTML('innerHTML=div');
|
||||
expect(tc).toBe('Hello<span>world</span>');
|
||||
expect(await page.evaluate(() => document.querySelector('div').innerHTML)).toBe('modified');
|
||||
});
|
||||
|
||||
it('getAttribute should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root: HTMLElement, selector: string) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.setAttribute('foo', 'modified'));
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => (e as HTMLElement).setAttribute('foo', 'modified'));
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('getAttribute', createDummySelector);
|
||||
await page.setContent(`<div foo=hello></div>`);
|
||||
const tc = await page.getAttribute('getAttribute=div', 'foo');
|
||||
expect(tc).toBe('hello');
|
||||
expect(await page.evaluate(() => document.querySelector('div').getAttribute('foo'))).toBe('modified');
|
||||
});
|
||||
|
||||
it('isVisible should be atomic', async ({ playwright, page }) => {
|
||||
const createDummySelector = () => ({
|
||||
query(root, selector) {
|
||||
const result = root.querySelector(selector);
|
||||
if (result)
|
||||
Promise.resolve().then(() => result.style.display = 'none');
|
||||
return result;
|
||||
},
|
||||
queryAll(root: HTMLElement, selector: string) {
|
||||
const result = Array.from(root.querySelectorAll(selector));
|
||||
for (const e of result)
|
||||
Promise.resolve().then(() => (e as HTMLElement).style.display = 'none');
|
||||
return result;
|
||||
}
|
||||
});
|
||||
await playwright.selectors.register('isVisible', createDummySelector);
|
||||
await page.setContent(`<div>Hello</div>`);
|
||||
const result = await page.isVisible('isVisible=div');
|
||||
expect(result).toBe(true);
|
||||
expect(await page.evaluate(() => document.querySelector('div').style.display)).toBe('none');
|
||||
});
|
@ -1,5 +0,0 @@
|
||||
{
|
||||
"port": 3333,
|
||||
"wsPath": "/",
|
||||
"headless": false
|
||||
}
|
Loading…
Reference in New Issue
Block a user