test(inspector): add some tests (#5461)

This commit is contained in:
Pavel Feldman 2021-02-15 08:32:13 -08:00 committed by GitHub
parent 1f3449c7da
commit 0c7da44465
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 175 additions and 51 deletions

View File

@ -325,7 +325,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
} }
async function open(options: Options, url: string | undefined, language: string) { async function open(options: Options, url: string | undefined, language: string) {
const { context, launchOptions, contextOptions } = await launchContext(options, false); const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWCLI_HEADLESS_FOR_TEST);
await context._enableRecorder({ await context._enableRecorder({
language, language,
launchOptions, launchOptions,
@ -339,7 +339,7 @@ async function open(options: Options, url: string | undefined, language: string)
} }
async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) { async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) {
const { context, launchOptions, contextOptions } = await launchContext(options, false); const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWCLI_HEADLESS_FOR_TEST);
if (process.env.PWTRACE) if (process.env.PWTRACE)
contextOptions._traceDir = path.join(process.cwd(), '.trace'); contextOptions._traceDir = path.join(process.cwd(), '.trace');
await context._enableRecorder({ await context._enableRecorder({

View File

@ -47,6 +47,7 @@ export type BrowserOptions = PlaywrightOptions & {
protocolLogger: types.ProtocolLogger, protocolLogger: types.ProtocolLogger,
browserLogsCollector: RecentLogsCollector, browserLogsCollector: RecentLogsCollector,
slowMo?: number; slowMo?: number;
wsEndpoint?: string; // Only there when connected over web socket.
}; };
export abstract class Browser extends SdkObject { export abstract class Browser extends SdkObject {

View File

@ -20,7 +20,7 @@ import path from 'path';
import * as util from 'util'; import * as util from 'util';
import { BrowserContext, normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; import { BrowserContext, normalizeProxySettings, validateBrowserContextOptions } from './browserContext';
import * as registry from '../utils/registry'; import * as registry from '../utils/registry';
import { ConnectionTransport } from './transport'; import { ConnectionTransport, WebSocketTransport } from './transport';
import { BrowserOptions, Browser, BrowserProcess, PlaywrightOptions } from './browser'; import { BrowserOptions, Browser, BrowserProcess, PlaywrightOptions } from './browser';
import { launchProcess, Env, envArrayToObject } from './processLauncher'; import { launchProcess, Env, envArrayToObject } from './processLauncher';
import { PipeTransport } from './pipeTransport'; import { PipeTransport } from './pipeTransport';
@ -112,6 +112,7 @@ export abstract class BrowserType extends SdkObject {
proxy: options.proxy, proxy: options.proxy,
protocolLogger, protocolLogger,
browserLogsCollector, browserLogsCollector,
wsEndpoint: options.useWebSocket ? (transport as WebSocketTransport).wsEndpoint : undefined,
}; };
if (persistent) if (persistent)
validateBrowserContextOptions(persistent, browserOptions); validateBrowserContextOptions(persistent, browserOptions);
@ -180,6 +181,8 @@ export abstract class BrowserType extends SdkObject {
await validateHostRequirements(this._registry, this._name); await validateHostRequirements(this._registry, this._name);
} }
let wsEndpointCallback: ((wsEndpoint: string) => void) | undefined;
const wsEndpoint = options.useWebSocket ? new Promise<string>(f => wsEndpointCallback = f) : undefined;
// Note: it is important to define these variables before launchProcess, so that we don't get // Note: it is important to define these variables before launchProcess, so that we don't get
// "Cannot access 'browserServer' before initialization" if something went wrong. // "Cannot access 'browserServer' before initialization" if something went wrong.
let transport: ConnectionTransport | undefined = undefined; let transport: ConnectionTransport | undefined = undefined;
@ -192,6 +195,11 @@ export abstract class BrowserType extends SdkObject {
handleSIGTERM, handleSIGTERM,
handleSIGHUP, handleSIGHUP,
log: (message: string) => { log: (message: string) => {
if (wsEndpointCallback) {
const match = message.match(/DevTools listening on (.*)/);
if (match)
wsEndpointCallback(match[1]);
}
progress.log(message); progress.log(message);
browserLogsCollector.log(message); browserLogsCollector.log(message);
}, },
@ -217,9 +225,12 @@ export abstract class BrowserType extends SdkObject {
kill kill
}; };
progress.cleanupWhenAborted(() => browserProcess && closeOrKill(browserProcess, progress.timeUntilDeadline())); progress.cleanupWhenAborted(() => browserProcess && closeOrKill(browserProcess, progress.timeUntilDeadline()));
if (options.useWebSocket) {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream]; transport = await WebSocketTransport.connect(progress, await wsEndpoint!);
transport = new PipeTransport(stdio[3], stdio[4]); } else {
const stdio = launchedProcess.stdio as unknown as [NodeJS.ReadableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.WritableStream, NodeJS.ReadableStream];
transport = new PipeTransport(stdio[3], stdio[4]);
}
return { browserProcess, downloadsPath, transport }; return { browserProcess, downloadsPath, transport };
} }
@ -242,7 +253,10 @@ function copyTestHooks(from: object, to: object) {
} }
function validateLaunchOptions<Options extends types.LaunchOptions>(options: Options): Options { function validateLaunchOptions<Options extends types.LaunchOptions>(options: Options): Options {
const { devtools = false, headless = !isDebugMode() && !devtools } = options; const { devtools = false } = options;
let { headless = !devtools } = options;
if (isDebugMode())
headless = false;
return { ...options, devtools, headless }; return { ...options, devtools, headless };
} }

View File

@ -119,7 +119,10 @@ export class Chromium extends BrowserType {
throw new Error('Arguments can not specify page to be opened'); throw new Error('Arguments can not specify page to be opened');
const chromeArguments = [...DEFAULT_ARGS]; const chromeArguments = [...DEFAULT_ARGS];
chromeArguments.push(`--user-data-dir=${userDataDir}`); chromeArguments.push(`--user-data-dir=${userDataDir}`);
chromeArguments.push('--remote-debugging-pipe'); if (options.useWebSocket)
chromeArguments.push('--remote-debugging-port=0');
else
chromeArguments.push('--remote-debugging-pipe');
if (options.devtools) if (options.devtools)
chromeArguments.push('--auto-open-devtools-for-tabs'); chromeArguments.push('--auto-open-devtools-for-tabs');
if (options.headless) { if (options.headless) {

View File

@ -21,16 +21,11 @@ import { CallMetadata, InstrumentationListener, SdkObject } from '../instrumenta
import { isDebugMode, isUnderTest } from '../../utils/utils'; import { isDebugMode, isUnderTest } from '../../utils/utils';
export class InspectorController implements InstrumentationListener { export class InspectorController implements InstrumentationListener {
private _recorders = new Map<BrowserContext, Promise<RecorderSupplement>>();
private _waitOperations = new Map<string, CallMetadata>(); private _waitOperations = new Map<string, CallMetadata>();
async onContextCreated(context: BrowserContext): Promise<void> { async onContextCreated(context: BrowserContext): Promise<void> {
if (isDebugMode()) if (isDebugMode())
this._recorders.set(context, RecorderSupplement.getOrCreate(context)); RecorderSupplement.getOrCreate(context);
}
async onContextDidDestroy(context: BrowserContext): Promise<void> {
this._recorders.delete(context);
} }
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
@ -61,10 +56,10 @@ export class InspectorController implements InstrumentationListener {
// Force create recorder on pause. // Force create recorder on pause.
if (!context._browser.options.headful && !isUnderTest()) if (!context._browser.options.headful && !isUnderTest())
return; return;
this._recorders.set(context, RecorderSupplement.getOrCreate(context)); RecorderSupplement.getOrCreate(context);
} }
const recorder = await this._recorders.get(context); const recorder = await RecorderSupplement.getNoCreate(context);
await recorder?.onBeforeCall(sdkObject, metadata); await recorder?.onBeforeCall(sdkObject, metadata);
} }
@ -87,14 +82,14 @@ export class InspectorController implements InstrumentationListener {
} }
} }
const recorder = await this._recorders.get(sdkObject.attribution.context); const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
await recorder?.onAfterCall(metadata); await recorder?.onAfterCall(metadata);
} }
async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onBeforeInputAction(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (!sdkObject.attribution.context) if (!sdkObject.attribution.context)
return; return;
const recorder = await this._recorders.get(sdkObject.attribution.context); const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
await recorder?.onBeforeInputAction(metadata); await recorder?.onBeforeInputAction(metadata);
} }
@ -102,7 +97,7 @@ export class InspectorController implements InstrumentationListener {
debugLogger.log(logName as any, message); debugLogger.log(logName as any, message);
if (!sdkObject.attribution.context) if (!sdkObject.attribution.context)
return; return;
const recorder = await this._recorders.get(sdkObject.attribution.context); const recorder = await RecorderSupplement.getNoCreate(sdkObject.attribution.context);
await recorder?.updateCallLog([metadata]); await recorder?.updateCallLog([metadata]);
} }
} }

View File

@ -26,7 +26,6 @@ import { internalCallMetadata } from '../../instrumentation';
import type { CallLog, EventData, Mode, Source } from './recorderTypes'; import type { CallLog, EventData, Mode, Source } from './recorderTypes';
import { BrowserContext } from '../../browserContext'; import { BrowserContext } from '../../browserContext';
import { isUnderTest } from '../../../utils/utils'; import { isUnderTest } from '../../../utils/utils';
import { RecentLogsCollector } from '../../../utils/debugLogger';
const readFileAsync = util.promisify(fs.readFile); const readFileAsync = util.promisify(fs.readFile);
@ -104,34 +103,20 @@ export class RecorderApp extends EventEmitter {
sdkLanguage: inspectedContext._options.sdkLanguage, sdkLanguage: inspectedContext._options.sdkLanguage,
args, args,
noDefaultViewport: true, noDefaultViewport: true,
headless: isUnderTest() && !inspectedContext._browser.options.headful headless: !!process.env.PWCLI_HEADLESS_FOR_TEST || (isUnderTest() && !inspectedContext._browser.options.headful),
useWebSocket: isUnderTest()
}); });
const wsEndpoint = isUnderTest() ? await this._parseWsEndpoint(context._browser.options.browserLogsCollector) : undefined;
const controller = new ProgressController(internalCallMetadata(), context._browser); const controller = new ProgressController(internalCallMetadata(), context._browser);
await controller.run(async progress => { await controller.run(async progress => {
await context._browser._defaultContext!._loadDefaultContextAsIs(progress); await context._browser._defaultContext!._loadDefaultContextAsIs(progress);
}); });
const [page] = context.pages(); const [page] = context.pages();
const result = new RecorderApp(page, wsEndpoint); const result = new RecorderApp(page, context._browser.options.wsEndpoint);
await result._init(); await result._init();
return result; return result;
} }
private static async _parseWsEndpoint(recentLogs: RecentLogsCollector): Promise<string> {
let callback: ((log: string) => void) | undefined;
const result = new Promise<string>(f => callback = f);
const check = (log: string) => {
const match = log.match(/DevTools listening on (.*)/);
if (match)
callback!(match[1]);
};
for (const log of recentLogs.recentLogs())
check(log);
recentLogs.on('log', check);
return result;
}
async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> { async setMode(mode: 'none' | 'recording' | 'inspecting'): Promise<void> {
await this._page.mainFrame()._evaluateExpression(((mode: Mode) => { await this._page.mainFrame()._evaluateExpression(((mode: Mode) => {
window.playwrightSetMode(mode); window.playwrightSetMode(mode);

View File

@ -67,6 +67,10 @@ export class RecorderSupplement {
return recorderPromise; return recorderPromise;
} }
static getNoCreate(context: BrowserContext): Promise<RecorderSupplement> | undefined {
return (context as any)[symbol] as Promise<RecorderSupplement> | undefined;
}
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) { constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
this._context = context; this._context = context;
this._params = params; this._params = params;
@ -325,6 +329,8 @@ export class RecorderSupplement {
} }
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
if (this._mode === 'recording')
return;
this._currentCallsMetadata.set(metadata, sdkObject); this._currentCallsMetadata.set(metadata, sdkObject);
this._updateUserSources(); this._updateUserSources();
this.updateCallLog([metadata]); this.updateCallLog([metadata]);
@ -333,6 +339,8 @@ export class RecorderSupplement {
} }
async onAfterCall(metadata: CallMetadata): Promise<void> { async onAfterCall(metadata: CallMetadata): Promise<void> {
if (this._mode === 'recording')
return;
if (!metadata.error) if (!metadata.error)
this._currentCallsMetadata.delete(metadata); this._currentCallsMetadata.delete(metadata);
this._pausedCallsMetadata.delete(metadata); this._pausedCallsMetadata.delete(metadata);
@ -372,16 +380,20 @@ export class RecorderSupplement {
} }
async onBeforeInputAction(metadata: CallMetadata): Promise<void> { async onBeforeInputAction(metadata: CallMetadata): Promise<void> {
if (this._mode === 'recording')
return;
if (this._pauseOnNextStatement) if (this._pauseOnNextStatement)
await this.pause(metadata); await this.pause(metadata);
} }
async updateCallLog(metadatas: CallMetadata[]): Promise<void> { async updateCallLog(metadatas: CallMetadata[]): Promise<void> {
if (this._mode === 'recording')
return;
const logs: CallLog[] = []; const logs: CallLog[] = [];
for (const metadata of metadatas) { for (const metadata of metadatas) {
if (!metadata.method) if (!metadata.method)
continue; continue;
const title = metadata.stack?.[0]?.function || metadata.method; const title = metadata.method;
let status: 'done' | 'in-progress' | 'paused' | 'error' = 'done'; let status: 'done' | 'in-progress' | 'paused' | 'error' = 'done';
if (this._currentCallsMetadata.has(metadata)) if (this._currentCallsMetadata.has(metadata))
status = 'in-progress'; status = 'in-progress';

View File

@ -50,6 +50,7 @@ export class WebSocketTransport implements ConnectionTransport {
onmessage?: (message: ProtocolResponse) => void; onmessage?: (message: ProtocolResponse) => void;
onclose?: () => void; onclose?: () => void;
readonly wsEndpoint: string;
static async connect(progress: Progress, url: string): Promise<WebSocketTransport> { static async connect(progress: Progress, url: string): Promise<WebSocketTransport> {
progress.log(`<ws connecting> ${url}`); progress.log(`<ws connecting> ${url}`);
@ -75,6 +76,7 @@ export class WebSocketTransport implements ConnectionTransport {
} }
constructor(progress: Progress, url: string) { constructor(progress: Progress, url: string) {
this.wsEndpoint = url;
this._ws = new WebSocket(url, [], { this._ws = new WebSocket(url, [], {
perMessageDeflate: false, perMessageDeflate: false,
maxPayload: 256 * 1024 * 1024, // 256Mb, maxPayload: 256 * 1024 * 1024, // 256Mb,

View File

@ -264,7 +264,8 @@ type LaunchOptionsBase = {
proxy?: ProxySettings, proxy?: ProxySettings,
downloadsPath?: string, downloadsPath?: string,
chromiumSandbox?: boolean, chromiumSandbox?: boolean,
slowMo?: number; slowMo?: number,
useWebSocket?: boolean,
}; };
export type LaunchOptions = LaunchOptionsBase & { export type LaunchOptions = LaunchOptionsBase & {
firefoxUserPrefs?: { [key: string]: string | number | boolean }, firefoxUserPrefs?: { [key: string]: string | number | boolean },

View File

@ -16,7 +16,6 @@
import debug from 'debug'; import debug from 'debug';
import fs from 'fs'; import fs from 'fs';
import { EventEmitter } from 'events';
const debugLoggerColorMap = { const debugLoggerColorMap = {
'api': 45, // cyan 'api': 45, // cyan
@ -64,11 +63,10 @@ class DebugLogger {
export const debugLogger = new DebugLogger(); export const debugLogger = new DebugLogger();
const kLogCount = 50; const kLogCount = 50;
export class RecentLogsCollector extends EventEmitter { export class RecentLogsCollector {
private _logs: string[] = []; private _logs: string[] = [];
log(message: string) { log(message: string) {
this.emit('log', message);
this._logs.push(message); this._logs.push(message);
if (this._logs.length === kLogCount * 2) if (this._logs.length === kLogCount * 2)
this._logs.splice(0, kLogCount); this._logs.splice(0, kLogCount);

View File

@ -18,7 +18,7 @@
import { folio } from './remoteServer.fixture'; import { folio } from './remoteServer.fixture';
const { it, expect, describe } = folio; const { it, expect, describe } = folio;
describe('lauch server', (suite, { mode }) => { describe('launch server', (suite, { mode }) => {
suite.skip(mode !== 'default'); suite.skip(mode !== 'default');
}, () => { }, () => {
it('should work', async ({browserType, browserOptions}) => { it('should work', async ({browserType, browserOptions}) => {

View File

@ -19,9 +19,9 @@ import * as http from 'http';
const { it, describe, expect } = folio; const { it, describe, expect } = folio;
describe('cli codegen', (suite, { mode, browserName, headful }) => { describe('cli codegen', (suite, { browserName, headful, mode }) => {
suite.fixme(browserName === 'firefox' && headful, 'Focus is off');
suite.skip(mode !== 'default'); suite.skip(mode !== 'default');
suite.fixme(browserName === 'firefox' && headful, 'Focus is off');
}, () => { }, () => {
it('should click', async ({ page, recorder }) => { it('should click', async ({ page, recorder }) => {
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`); await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);

View File

@ -20,8 +20,7 @@ import * as url from 'url';
const { it, describe, expect } = folio; const { it, describe, expect } = folio;
describe('cli codegen', (suite, { mode, browserName, headful }) => { describe('cli codegen', (suite, { mode }) => {
// suite.fixme(browserName === 'firefox' && headful, 'Focus is off');
suite.skip(mode !== 'default'); suite.skip(mode !== 'default');
}, () => { }, () => {
it('should contain open page', async ({ recorder }) => { it('should contain open page', async ({ recorder }) => {

View File

@ -146,10 +146,10 @@ class Recorder {
} }
} }
fixtures.runCLI.init(async ({ browserName }, runTest) => { fixtures.runCLI.init(async ({ browserName, headful }, runTest) => {
let cli: CLIMock; let cli: CLIMock;
const cliFactory = (args: string[]) => { const cliFactory = (args: string[]) => {
cli = new CLIMock(browserName, args); cli = new CLIMock(browserName, !headful, args);
return cli; return cli;
}; };
await runTest(cliFactory); await runTest(cliFactory);
@ -163,7 +163,7 @@ class CLIMock {
private waitForCallback: () => void; private waitForCallback: () => void;
exited: Promise<void>; exited: Promise<void>;
constructor(browserName, args: string[]) { constructor(browserName: string, headless: boolean, args: string[]) {
this.data = ''; this.data = '';
this.process = spawn('node', [ this.process = spawn('node', [
path.join(__dirname, '..', '..', 'lib', 'cli', 'cli.js'), path.join(__dirname, '..', '..', 'lib', 'cli', 'cli.js'),
@ -172,7 +172,8 @@ class CLIMock {
], { ], {
env: { env: {
...process.env, ...process.env,
PWCLI_EXIT_FOR_TEST: '1' PWCLI_EXIT_FOR_TEST: '1',
PWCLI_HEADLESS_FOR_TEST: headless ? '1' : undefined,
}, },
stdio: 'pipe' stdio: 'pipe'
}); });

View File

@ -15,6 +15,7 @@
*/ */
import { expect } from 'folio'; import { expect } from 'folio';
import { Page } from '..';
import { folio } from './recorder.fixtures'; import { folio } from './recorder.fixtures';
const { it, describe} = folio; const { it, describe} = folio;
@ -119,4 +120,116 @@ describe('pause', (suite, { mode }) => {
await recorderPage.click('[title="Step over"]'); await recorderPage.click('[title="Step over"]');
await scriptPromise; await scriptPromise;
}); });
it('should skip input when resuming', async ({page, recorderPageGetter}) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
await page.pause();
await page.click('button');
await page.pause(); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
await recorderPage.click('[title=Resume]');
await scriptPromise;
});
it('should populate log', async ({page, recorderPageGetter}) => {
await page.setContent('<button>Submit</button>');
const scriptPromise = (async () => {
await page.pause();
await page.click('button');
await page.pause(); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'pause',
'click',
'waiting for selector "button"',
'selector resolved to visible <button>Submit</button>',
'attempting click action',
'waiting for element to be visible, enabled and stable',
'element is visible, enabled and stable',
'scrolling into view if needed',
'done scrolling',
'checking that element receives pointer events at ()',
'element does receive pointer events',
'performing click action',
'click action done',
'waiting for scheduled navigations to finish',
'navigations have finished',
'pause',
]);
await recorderPage.click('[title="Resume"]');
await scriptPromise;
});
it('should populate log with waitForEvent', async ({page, recorderPageGetter}) => {
await page.setContent('<button onclick="console.log(1)">Submit</button>');
const scriptPromise = (async () => {
await page.pause();
await Promise.all([
page.waitForEvent('console'),
page.click('button'),
]);
await page.pause(); // 2
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.pause(); // 2")');
expect(await sanitizeLog(recorderPage)).toEqual([
'pause',
'waitForEvent()',
'waiting for event \"console\"',
'click',
'waiting for selector "button"',
'selector resolved to visible <button onclick=\"console.log()\">Submit</button>',
'attempting click action',
'waiting for element to be visible, enabled and stable',
'element is visible, enabled and stable',
'scrolling into view if needed',
'done scrolling',
'checking that element receives pointer events at ()',
'element does receive pointer events',
'performing click action',
'click action done',
'waiting for scheduled navigations to finish',
'navigations have finished',
'pause',
]);
await recorderPage.click('[title="Resume"]');
await scriptPromise;
});
it('should populate log with error', async ({page, recorderPageGetter}) => {
await page.setContent('<button onclick="console.log(1)">Submit</button>');
const scriptPromise = (async () => {
await page.pause();
await page.isChecked('button');
})().catch(e => e);
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Resume"]');
await recorderPage.waitForSelector('.source-line-error');
expect(await sanitizeLog(recorderPage)).toEqual([
'pause',
'isChecked',
'checking \"checked\" state of \"button\"',
'selector resolved to <button onclick=\"console.log()\">Submit</button>',
'Not a checkbox or radio button',
]);
const error = await scriptPromise;
expect(error.message).toContain('Not a checkbox or radio button');
});
}); });
async function sanitizeLog(recorderPage: Page): Promise<string[]> {
const text = await recorderPage.innerText('.recorder-log');
return text.split('\n').filter(l => {
return l !== 'element is not stable - waiting...';
}).map(l => {
return l.replace(/\(.*\)/, '()');
});
}