From 59d0f8728d4809b39785d68d7a146f06f0dbe2e6 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Tue, 16 Jun 2020 10:15:08 -0700 Subject: [PATCH] test(recorder): add recorder sanity tests (#2582) --- src/browserContext.ts | 14 ++- src/cli/index.ts | 4 +- src/debug/debugController.ts | 15 +-- src/debug/injected/recorder.ts | 8 +- src/debug/recorderActions.ts | 26 +++-- src/debug/recorderController.ts | 83 +++++++++----- src/debug/terminalOutput.ts | 103 ++++++++++------- src/dom.ts | 3 - src/helper.ts | 13 ++- test/recorder.spec.js | 196 ++++++++++++++++++++++++++++++++ test/test.config.js | 1 + 11 files changed, 362 insertions(+), 104 deletions(-) create mode 100644 test/recorder.spec.js diff --git a/src/browserContext.ts b/src/browserContext.ts index bc3f3642c8..8c217e0b29 100644 --- a/src/browserContext.ts +++ b/src/browserContext.ts @@ -15,6 +15,7 @@ * limitations under the License. */ +import { Writable } from 'stream'; import { helper } from './helper'; import * as network from './network'; import { Page, PageBinding } from './page'; @@ -89,6 +90,7 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser readonly _downloads = new Set(); readonly _browserBase: BrowserBase; readonly _logger: InnerLogger; + private _debugController: DebugController | undefined; constructor(browserBase: BrowserBase, options: BrowserContextOptions) { super(); @@ -99,8 +101,16 @@ export abstract class BrowserContextBase extends EventEmitter implements Browser } async _initialize() { - if (helper.isDebugMode()) - new DebugController(this); + if (helper.isDebugMode() || helper.isRecordMode()) { + this._debugController = new DebugController(this, { + recorderOutput: helper.isRecordMode() ? process.stdout : undefined + }); + } + } + + _initDebugModeForTest(options: { recorderOutput: Writable }): DebugController { + this._debugController = new DebugController(this, options); + return this._debugController; } async waitForEvent(event: string, optionsOrPredicate: types.WaitForEventOptions = {}): Promise { diff --git a/src/cli/index.ts b/src/cli/index.ts index e3abc9a63d..efd6729bff 100755 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -23,7 +23,6 @@ import { Playwright } from '../server/playwright'; import { BrowserType, LaunchOptions } from '../server/browserType'; import { DeviceDescriptors } from '../deviceDescriptors'; import { BrowserContextOptions } from '../browserContext'; -import { setRecorderMode } from '../debug/debugController'; import { helper } from '../helper'; const playwright = new Playwright(__dirname, require('../../browsers.json')['browsers']); @@ -104,8 +103,7 @@ async function open(options: Options, url: string | undefined) { } async function record(options: Options, url: string | undefined) { - helper.setDebugMode(); - setRecorderMode(); + helper.setRecordMode(true); return await open(options, url); } diff --git a/src/debug/debugController.ts b/src/debug/debugController.ts index 64c5bc8ba1..b3946ba80d 100644 --- a/src/debug/debugController.ts +++ b/src/debug/debugController.ts @@ -14,33 +14,30 @@ * limitations under the License. */ +import { Writable } from 'stream'; import { BrowserContextBase } from '../browserContext'; import { Events } from '../events'; import * as frames from '../frames'; import { Page } from '../page'; import { RecorderController } from './recorderController'; -let isRecorderMode = false; - -export function setRecorderMode(): void { - isRecorderMode = true; -} - export class DebugController { - constructor(context: BrowserContextBase) { + constructor(context: BrowserContextBase, options: { recorderOutput?: Writable | undefined }) { const installInFrame = async (frame: frames.Frame) => { try { const mainContext = await frame._mainContext(); - await mainContext.createDebugScript({ console: true, record: isRecorderMode }); + await mainContext.createDebugScript({ console: true, record: !!options.recorderOutput }); } catch (e) { } }; + if (options.recorderOutput) + new RecorderController(context, options.recorderOutput); + context.on(Events.BrowserContext.Page, (page: Page) => { for (const frame of page.frames()) installInFrame(frame); page.on(Events.Page.FrameNavigated, installInFrame); - new RecorderController(page); }); } } diff --git a/src/debug/injected/recorder.ts b/src/debug/injected/recorder.ts index 4d8ac9ee8a..4bf177b789 100644 --- a/src/debug/injected/recorder.ts +++ b/src/debug/injected/recorder.ts @@ -40,6 +40,11 @@ export class Recorder { private async _onClick(event: MouseEvent) { if ((event.target as Element).nodeName === 'SELECT') return; + if ((event.target as Element).nodeName === 'INPUT') { + // Check/uncheck are handled in input. + if (((event.target as HTMLInputElement).type || '').toLowerCase() === 'checkbox') + return; + } // Perform action consumes this event and asks Playwright to perform it. this._performAction(event, { @@ -76,8 +81,7 @@ export class Recorder { } if ((event.target as Element).nodeName === 'SELECT') { const selectElement = event.target as HTMLSelectElement; - // TODO: move this to this._performAction - window.recordPlaywrightAction({ + this._performAction(event, { name: 'select', selector, options: [...selectElement.selectedOptions].map(option => option.value), diff --git a/src/debug/recorderActions.ts b/src/debug/recorderActions.ts index e2c85e3759..6a1d72f3fa 100644 --- a/src/debug/recorderActions.ts +++ b/src/debug/recorderActions.ts @@ -22,7 +22,6 @@ export type ActionName = export type ActionBase = { signals: Signal[], - frameUrl?: string, committed?: boolean, } @@ -78,30 +77,35 @@ export type NavigationSignal = { type: 'assert' | 'await', }; -export type Signal = NavigationSignal; +export type PopupSignal = { + name: 'popup', + popupAlias: string, +}; + +export type Signal = NavigationSignal | PopupSignal; export function actionTitle(action: Action): string { switch (action.name) { case 'check': - return 'Check'; + return `Check ${action.selector}`; case 'uncheck': - return 'Uncheck'; + return `Uncheck ${action.selector}`; case 'click': { if (action.clickCount === 1) - return 'Click'; + return `Click ${action.selector}`; if (action.clickCount === 2) - return 'Double click'; + return `Double click ${action.selector}`; if (action.clickCount === 3) - return 'Triple click'; + return `Triple click ${action.selector}`; return `${action.clickCount}× click`; } case 'fill': - return 'Fill'; + return `Fill ${action.selector}`; case 'navigate': - return 'Go to'; + return `Go to ${action.url}`; case 'press': - return 'Press'; + return `Press ${action.key}` + (action.modifiers ? ' with modifiers' : ''); case 'select': - return 'Select'; + return `Select ${action.selector}`; } } diff --git a/src/debug/recorderController.ts b/src/debug/recorderController.ts index da5fba9aff..fe6cd58a64 100644 --- a/src/debug/recorderController.ts +++ b/src/debug/recorderController.ts @@ -14,37 +14,46 @@ * limitations under the License. */ -import * as actions from './recorderActions'; +import { Writable } from 'stream'; +import { BrowserContextBase } from '../browserContext'; +import * as dom from '../dom'; +import { Events } from '../events'; import * as frames from '../frames'; import { Page } from '../page'; -import { Events } from '../events'; +import * as actions from './recorderActions'; import { TerminalOutput } from './terminalOutput'; -import * as dom from '../dom'; export class RecorderController { - private _page: Page; - private _output = new TerminalOutput(); + private _output: TerminalOutput; private _performingAction = false; + private _pageAliases = new Map(); + private _lastPopupOrdinal = 0; - constructor(page: Page) { - this._page = page; + constructor(context: BrowserContextBase, output: Writable) { + this._output = new TerminalOutput(output || process.stdout); + context.on(Events.BrowserContext.Page, (page: Page) => { + // First page is called page, others are called popup1, popup2, etc. + const pageName = this._pageAliases.size ? 'popup' + ++this._lastPopupOrdinal : 'page'; + this._pageAliases.set(page, pageName); + page.on(Events.Page.Close, () => this._pageAliases.delete(page)); - // Input actions that potentially lead to navigation are intercepted on the page and are - // performed by the Playwright. - this._page.exposeBinding('performPlaywrightAction', - (source, action: actions.Action) => this._performAction(source.frame, action)); - // Other non-essential actions are simply being recorded. - this._page.exposeBinding('recordPlaywrightAction', - (source, action: actions.Action) => this._recordAction(source.frame, action)); + // Input actions that potentially lead to navigation are intercepted on the page and are + // performed by the Playwright. + page.exposeBinding('performPlaywrightAction', + (source, action: actions.Action) => this._performAction(source.frame, action)).catch(e => {}); - this._page.on(Events.Page.FrameNavigated, (frame: frames.Frame) => this._onFrameNavigated(frame)); + // Other non-essential actions are simply being recorded. + page.exposeBinding('recordPlaywrightAction', + (source, action: actions.Action) => this._recordAction(source.frame, action)).catch(e => {}); + + page.on(Events.Page.FrameNavigated, (frame: frames.Frame) => this._onFrameNavigated(frame)); + page.on(Events.Page.Popup, (popup: Page) => this._onPopup(page, popup)); + }); } private async _performAction(frame: frames.Frame, action: actions.Action) { - if (frame !== this._page.mainFrame()) - action.frameUrl = frame.url(); this._performingAction = true; - this._output.addAction(action); + this._recordAction(frame, action); if (action.name === 'click') { const { options } = toClickOptions(action); await frame.click(action.selector, options); @@ -58,36 +67,48 @@ export class RecorderController { await frame.check(action.selector); if (action.name === 'uncheck') await frame.uncheck(action.selector); + if (action.name === 'select') + await frame.selectOption(action.selector, action.options); this._performingAction = false; - setTimeout(() => action.committed = true, 2000); + setTimeout(() => action.committed = true, 5000); } private async _recordAction(frame: frames.Frame, action: actions.Action) { - if (frame !== this._page.mainFrame()) - action.frameUrl = frame.url(); - this._output.addAction(action); + this._output.addAction(this._pageAliases.get(frame._page)!, frame, action); } private _onFrameNavigated(frame: frames.Frame) { if (frame.parentFrame()) return; + const pageAlias = this._pageAliases.get(frame._page); + const action = this._output.lastAction(); + // We only augment actions that have not been committed. + if (action && !action.committed && action.name !== 'navigate') { + // If we hit a navigation while action is executed, we assert it. Otherwise, we await it. + this._output.signal(pageAlias!, frame, { name: 'navigation', url: frame.url(), type: this._performingAction ? 'assert' : 'await' }); + } else if (!action || action.committed) { + // If navigation happens out of the blue, we just log it. + this._output.addAction( + pageAlias!, frame, { + name: 'navigate', + url: frame.url(), + signals: [], + }); + } + } + + private _onPopup(page: Page, popup: Page) { + const pageAlias = this._pageAliases.get(page)!; + const popupAlias = this._pageAliases.get(popup)!; const action = this._output.lastAction(); // We only augment actions that have not been committed. if (action && !action.committed) { // If we hit a navigation while action is executed, we assert it. Otherwise, we await it. - this._output.signal({ name: 'navigation', url: frame.url(), type: this._performingAction ? 'assert' : 'await' }); - } else { - // If navigation happens out of the blue, we just log it. - this._output.addAction({ - name: 'navigate', - url: this._page.url(), - signals: [], - }); + this._output.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias }); } } } - export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: dom.ClickOptions } { let method: 'click' | 'dblclick' = 'click'; if (action.clickCount === 2) diff --git a/src/debug/terminalOutput.ts b/src/debug/terminalOutput.ts index 7bf7ece7ac..78f526a692 100644 --- a/src/debug/terminalOutput.ts +++ b/src/debug/terminalOutput.ts @@ -14,9 +14,11 @@ * limitations under the License. */ +import { Writable } from 'stream'; import * as dom from '../dom'; -import { Formatter, formatColors } from '../utils/formatter'; -import { Action, NavigationSignal, actionTitle } from './recorderActions'; +import { Frame } from '../frames'; +import { formatColors, Formatter } from '../utils/formatter'; +import { Action, actionTitle, NavigationSignal, PopupSignal, Signal } from './recorderActions'; import { toModifiers } from './recorderController'; const { cst, cmt, fnc, kwd, prp, str } = formatColors; @@ -24,8 +26,10 @@ const { cst, cmt, fnc, kwd, prp, str } = formatColors; export class TerminalOutput { private _lastAction: Action | undefined; private _lastActionText: string | undefined; + private _out: Writable; - constructor() { + constructor(out: Writable) { + this._out = out; const formatter = new Formatter(); formatter.add(` @@ -36,11 +40,10 @@ export class TerminalOutput { ${kwd('const')} ${cst('browser')} = ${kwd('await')} ${cst(`chromium`)}.${fnc('launch')}(); ${kwd('const')} ${cst('page')} = ${kwd('await')} ${cst('browser')}.${fnc('newPage')}(); `); - process.stdout.write(formatter.format()); - process.stdout.write(`\n})();`); + this._out.write(formatter.format() + '\n`})();`\n'); } - addAction(action: Action) { + addAction(pageAlias: string, frame: Frame, action: Action) { // We augment last action based on the type. let eraseLastAction = false; if (this._lastAction && action.name === 'fill' && this._lastAction.name === 'fill') { @@ -57,55 +60,85 @@ export class TerminalOutput { eraseLastAction = true; } } - this._printAction(action, eraseLastAction); + this._printAction(pageAlias, frame, action, eraseLastAction); } - _printAction(action: Action, eraseLastAction: boolean) { + _printAction(pageAlias: string, frame: Frame, action: Action, eraseLastAction: boolean) { // We erase terminating `})();` at all times. let eraseLines = 1; if (eraseLastAction && this._lastActionText) eraseLines += this._lastActionText.split('\n').length; // And we erase the last action too if augmenting. for (let i = 0; i < eraseLines; ++i) - process.stdout.write('\u001B[F\u001B[2K'); + this._out.write('\u001B[1A\u001B[2K'); this._lastAction = action; - this._lastActionText = this._generateAction(action); - console.log(this._lastActionText); // eslint-disable-line no-console - console.log(`})();`); // eslint-disable-line no-console + this._lastActionText = this._generateAction(pageAlias, frame, action); + this._out.write(this._lastActionText + '\n})();\n'); } lastAction(): Action | undefined { return this._lastAction; } - signal(signal: NavigationSignal) { + signal(pageAlias: string, frame: Frame, signal: Signal) { if (this._lastAction) { this._lastAction.signals.push(signal); - this._printAction(this._lastAction, true); + this._printAction(pageAlias, frame, this._lastAction, true); } } - private _generateAction(action: Action): string { + private _generateAction(pageAlias: string, frame: Frame, action: Action): string { const formatter = new Formatter(2); formatter.newLine(); formatter.add(cmt(actionTitle(action))); + + const subject = frame === frame._page.mainFrame() ? cst(pageAlias) : + `${cst(pageAlias)}.${fnc('frame')}(${formatObject({ url: frame.url() })})`; + let navigationSignal: NavigationSignal | undefined; - if (action.name !== 'navigate' && action.signals && action.signals.length) - navigationSignal = action.signals[action.signals.length - 1]; + let popupSignal: PopupSignal | undefined; + for (const signal of action.signals) { + if (signal.name === 'navigation') + navigationSignal = signal; + if (signal.name === 'popup') + popupSignal = signal; + } const waitForNavigation = navigationSignal && navigationSignal.type === 'await'; const assertNavigation = navigationSignal && navigationSignal.type === 'assert'; - if (waitForNavigation) { - formatter.add(`${kwd('await')} ${cst('Promise')}.${fnc('all')}([ - ${cst('page')}.${fnc('waitForNavigation')}({ ${prp('url')}: ${str(navigationSignal!.url)} }),`); + + const emitPromiseAll = waitForNavigation || popupSignal; + if (emitPromiseAll) { + // Generate either await Promise.all([]) or + // const [popup1] = await Promise.all([]). + let leftHandSide = ''; + if (popupSignal) + leftHandSide = `${kwd('const')} [${cst(popupSignal.popupAlias)}] = `; + formatter.add(`${leftHandSide}${kwd('await')} ${cst('Promise')}.${fnc('all')}([`); } - const subject = action.frameUrl ? - `${cst('page')}.${fnc('frame')}(${formatObject({ url: action.frameUrl })})` : cst('page'); + // Popup signals. + if (popupSignal) + formatter.add(`${cst(pageAlias)}.${fnc('waitForEvent')}(${str('popup')}),`); + + // Navigation signal. + if (waitForNavigation) + formatter.add(`${cst(pageAlias)}.${fnc('waitForNavigation')}({ ${prp('url')}: ${str(navigationSignal!.url)} }),`); const prefix = waitForNavigation ? '' : kwd('await') + ' '; + const actionCall = this._generateActionCall(action); const suffix = waitForNavigation ? '' : ';'; + formatter.add(`${prefix}${subject}.${actionCall}${suffix}`); + + if (emitPromiseAll) + formatter.add(`]);`); + else if (assertNavigation) + formatter.add(` ${cst('assert')}.${fnc('equal')}(${cst(pageAlias)}.${fnc('url')}(), ${str(navigationSignal!.url)});`); + return formatter.format(); + } + + private _generateActionCall(action: Action): string { switch (action.name) { case 'click': { let method = 'click'; @@ -120,36 +153,24 @@ export class TerminalOutput { if (action.clickCount > 2) options.clickCount = action.clickCount; const optionsString = formatOptions(options); - formatter.add(`${prefix}${subject}.${fnc(method)}(${str(action.selector)}${optionsString})${suffix}`); - break; + return `${fnc(method)}(${str(action.selector)}${optionsString})`; } case 'check': - formatter.add(`${prefix}${subject}.${fnc('check')}(${str(action.selector)})${suffix}`); - break; + return `${fnc('check')}(${str(action.selector)})`; case 'uncheck': - formatter.add(`${prefix}${subject}.${fnc('uncheck')}(${str(action.selector)})${suffix}`); - break; + return `${fnc('uncheck')}(${str(action.selector)})`; case 'fill': - formatter.add(`${prefix}${subject}.${fnc('fill')}(${str(action.selector)}, ${str(action.text)})${suffix}`); - break; + return `${fnc('fill')}(${str(action.selector)}, ${str(action.text)})`; case 'press': { const modifiers = toModifiers(action.modifiers); const shortcut = [...modifiers, action.key].join('+'); - formatter.add(`${prefix}${subject}.${fnc('press')}(${str(action.selector)}, ${str(shortcut)})${suffix}`); - break; + return `${fnc('press')}(${str(action.selector)}, ${str(shortcut)})`; } case 'navigate': - formatter.add(`${prefix}${subject}.${fnc('goto')}(${str(action.url)})${suffix}`); - break; + return `${fnc('goto')}(${str(action.url)})`; case 'select': - formatter.add(`${prefix}${subject}.${fnc('select')}(${str(action.selector)}, ${formatObject(action.options.length > 1 ? action.options : action.options[0])})${suffix}`); - break; + return `${fnc('selectOption')}(${str(action.selector)}, ${formatObject(action.options.length > 1 ? action.options : action.options[0])})`; } - if (waitForNavigation) - formatter.add(`]);`); - else if (assertNavigation) - formatter.add(` ${cst('assert')}.${fnc('equal')}(${cst('page')}.${fnc('url')}(), ${str(navigationSignal!.url)});`); - return formatter.format(); } } diff --git a/src/dom.ts b/src/dom.ts index e97f9d19fa..67900d864c 100644 --- a/src/dom.ts +++ b/src/dom.ts @@ -95,9 +95,6 @@ export class FrameExecutionContext extends js.ExecutionContext { } createDebugScript(options: { record?: boolean, console?: boolean }): Promise | undefined> { - if (!helper.isDebugMode()) - return Promise.resolve(undefined); - if (!this._debugScriptPromise) { const source = `new (${debugScriptSource.source})()`; this._debugScriptPromise = this._delegate.rawEvaluate(source).then(objectId => new js.JSHandle(this, 'object', objectId)).then(async debugScript => { diff --git a/src/helper.ts b/src/helper.ts index fe31411e21..acd19c9fe3 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -34,6 +34,7 @@ export type RegisteredListener = { export type Listener = (...args: any[]) => void; let isInDebugMode = !!getFromENV('PWDEBUG'); +let isInRecordMode = false; class Helper { static evaluationString(fun: Function | string, ...args: any[]): string { @@ -306,8 +307,16 @@ class Helper { return isInDebugMode; } - static setDebugMode() { - isInDebugMode = true; + static setDebugMode(enabled: boolean) { + isInDebugMode = enabled; + } + + static isRecordMode(): boolean { + return isInRecordMode; + } + + static setRecordMode(enabled: boolean) { + isInRecordMode = enabled; } } diff --git a/test/recorder.spec.js b/test/recorder.spec.js new file mode 100644 index 0000000000..e721d9620e --- /dev/null +++ b/test/recorder.spec.js @@ -0,0 +1,196 @@ +/** + * 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. + */ + +const { Writable } = require('stream'); +const {FFOX, CHROMIUM, WEBKIT} = require('./utils').testOptions(browserType); + +const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))' +].join('|') +class WritableBuffer { + constructor() { + this.lines = []; + } + + write(chunk) { + if (chunk === '\u001B[F\u001B[2K') { + this.lines.pop(); + return; + } + this.lines.push(...chunk.split('\n')); + if (this._callback && chunk.includes(this._text)) + this._callback(); + } + + waitFor(text) { + if (this.lines.join('\n').includes(text)) + return Promise.resolve(); + this._text = text; + return new Promise(f => this._callback = f); + } + + data() { + return this.lines.join('\n'); + } + + text() { + const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))' + ].join('|'); + return this.data().replace(new RegExp(pattern, 'g'), ''); + } +} + +describe('Recorder', function() { + beforeEach(async state => { + state.context = await state.browser.newContext(); + state.output = new WritableBuffer(); + const debugController = state.context._initDebugModeForTest({ recorderOutput: state.output }); + }); + + afterEach(async state => { + await state.context.close(); + }); + + it('should click', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent(``); + const [message] = await Promise.all([ + page.waitForEvent('console'), + output.waitFor('click'), + page.dispatchEvent('button', 'click', { detail: 1 }) + ]); + expect(output.text()).toContain(` + // Click text="Submit" + await page.click('text="Submit"');`); + expect(message.text()).toBe('click'); + }); + + it('should fill', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent(``); + const [message] = await Promise.all([ + page.waitForEvent('console'), + output.waitFor('fill'), + page.fill('input', 'John') + ]); + expect(output.text()).toContain(` + // Fill input[name=name] + await page.fill('input[name=name]', 'John');`); + expect(message.text()).toBe('John'); + }); + + it('should press', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent(``); + const [message] = await Promise.all([ + page.waitForEvent('console'), + output.waitFor('press'), + page.press('input', 'Shift+Enter') + ]); + expect(output.text()).toContain(` + // Press Enter with modifiers + await page.press('input[name=name]', 'Shift+Enter');`); + expect(message.text()).toBe('press'); + }); + + it('should check', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent(``); + const [message] = await Promise.all([ + page.waitForEvent('console'), + output.waitFor('check'), + page.dispatchEvent('input', 'click', { detail: 1 }) + ]); + await output.waitFor('check'); + expect(output.text()).toContain(` + // Check input[name=accept] + await page.check('input[name=accept]');`); + expect(message.text()).toBe("true"); + }); + + it('should uncheck', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent(``); + const [message] = await Promise.all([ + page.waitForEvent('console'), + output.waitFor('uncheck'), + page.dispatchEvent('input', 'click', { detail: 1 }) + ]); + expect(output.text()).toContain(` + // Uncheck input[name=accept] + await page.uncheck('input[name=accept]');`); + expect(message.text()).toBe("false"); + }); + + it('should select', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent(''); + const [message] = await Promise.all([ + page.waitForEvent('console'), + output.waitFor('select'), + page.selectOption('select', '2') + ]); + expect(output.text()).toContain(` + // Select select[id=age] + await page.selectOption('select[id=age]', '2');`); + expect(message.text()).toBe("2"); + }); + + it('should await popup', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent('link'); + const [popup] = await Promise.all([ + context.waitForEvent('page'), + output.waitFor('waitForEvent'), + page.dispatchEvent('a', 'click', { detail: 1 }) + ]); + expect(output.text()).toContain(` + // Click text="link" + const [popup1] = await Promise.all([ + page.waitForEvent('popup'), + await page.click('text="link"'); + ]);`); + expect(popup.url()).toBe(`${server.PREFIX}/popup/popup.html`); + }); + + it('should await navigation', async function({context, output, server}) { + const page = await context.newPage(); + await page.goto(server.EMPTY_PAGE); + await page.setContent(`link`); + await Promise.all([ + page.waitForNavigation(), + output.waitFor('waitForNavigation'), + page.dispatchEvent('a', 'click', { detail: 1 }) + ]); + expect(output.text()).toContain(` + // Click text="link" + await Promise.all([ + page.waitForNavigation({ url: '${server.PREFIX}/popup/popup.html' }), + page.click('text="link"') + ]);`); + expect(page.url()).toContain('/popup/popup.html'); + }); +}); diff --git a/test/test.config.js b/test/test.config.js index 53da76cdf7..924d44f7d1 100644 --- a/test/test.config.js +++ b/test/test.config.js @@ -216,6 +216,7 @@ module.exports = { './browsercontext.spec.js', './ignorehttpserrors.spec.js', './popup.spec.js', + './recorder.spec.js', ], environments: [customEnvironment, 'browser'], },