diff --git a/src/debug/debugController.ts b/src/debug/debugController.ts index b3946ba80d..e0b0d97195 100644 --- a/src/debug/debugController.ts +++ b/src/debug/debugController.ts @@ -18,26 +18,37 @@ import { Writable } from 'stream'; import { BrowserContextBase } from '../browserContext'; import { Events } from '../events'; import * as frames from '../frames'; +import * as js from '../javascript'; import { Page } from '../page'; import { RecorderController } from './recorderController'; +import DebugScript from './injected/debugScript'; export class DebugController { + private _options: { recorderOutput?: Writable | undefined }; + 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: !!options.recorderOutput }); - } catch (e) { - } - }; + this._options = options; 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); + this.ensureInstalledInFrame(frame); + page.on(Events.Page.FrameNavigated, frame => this.ensureInstalledInFrame(frame)); }); } + + private async ensureInstalledInFrame(frame: frames.Frame): Promise | undefined> { + try { + const mainContext = await frame._mainContext(); + return await mainContext.createDebugScript({ console: true, record: !!this._options.recorderOutput }); + } catch (e) { + } + } + + async ensureInstalledInFrameForTest(frame: frames.Frame): Promise { + const handle = await this.ensureInstalledInFrame(frame); + await handle!.evaluate(debugScript => debugScript.recorder!.refreshListeners()); + } } diff --git a/src/debug/injected/debugScript.ts b/src/debug/injected/debugScript.ts index 7609389e1b..3d8da14a9e 100644 --- a/src/debug/injected/debugScript.ts +++ b/src/debug/injected/debugScript.ts @@ -22,9 +22,6 @@ export default class DebugScript { consoleAPI: ConsoleAPI | undefined; recorder: Recorder | undefined; - constructor() { - } - initialize(injectedScript: InjectedScript, options: { console?: boolean, record?: boolean }) { if (options.console) this.consoleAPI = new ConsoleAPI(injectedScript); diff --git a/src/debug/injected/recorder.ts b/src/debug/injected/recorder.ts index 4bf177b789..4b4bedd8d1 100644 --- a/src/debug/injected/recorder.ts +++ b/src/debug/injected/recorder.ts @@ -28,13 +28,27 @@ declare global { export class Recorder { private _injectedScript: InjectedScript; private _performingAction = false; + readonly refreshListeners: () => void; constructor(injectedScript: InjectedScript) { this._injectedScript = injectedScript; - document.addEventListener('click', event => this._onClick(event), true); - document.addEventListener('input', event => this._onInput(event), true); - document.addEventListener('keydown', event => this._onKeyDown(event), true); + const onClick = this._onClick.bind(this); + const onInput = this._onInput.bind(this); + const onKeyDown = this._onKeyDown.bind(this); + this.refreshListeners = () => { + document.removeEventListener('click', onClick, true); + document.removeEventListener('input', onInput, true); + document.removeEventListener('keydown', onKeyDown, true); + document.addEventListener('click', onClick, true); + document.addEventListener('input', onInput, true); + document.addEventListener('keydown', onKeyDown, true); + }; + this.refreshListeners(); + // Document listeners are cleared upon document.open, + // so we refresh them periodically in a best-effort manner. + // Note: keep in sync with the same constant in the test. + setInterval(this.refreshListeners, 1000); } private async _onClick(event: MouseEvent) { diff --git a/test/recorder.spec.js b/test/recorder.spec.js index 15849d658b..3b8da261ae 100644 --- a/test/recorder.spec.js +++ b/test/recorder.spec.js @@ -14,7 +14,7 @@ * limitations under the License. */ -const {USES_HOOKS} = require('./utils').testOptions(browserType); +const {FFOX, CHROMIUM, WEBKIT, USES_HOOKS} = require('./utils').testOptions(browserType); class WritableBuffer { constructor() { @@ -56,16 +56,20 @@ describe.skip(USES_HOOKS)('Recorder', function() { state.context = await state.browser.newContext(); state.output = new WritableBuffer(); const debugController = state.context._initDebugModeForTest({ recorderOutput: state.output }); + state.page = await state.context.newPage(); + state.setContent = async (content) => { + await state.page.setContent(content); + await debugController.ensureInstalledInFrameForTest(state.page.mainFrame()); + }; }); - + afterEach(async state => { await state.context.close(); }); - - it('should click', async function({context, output, server}) { - const page = await context.newPage(); + + it('should click', async function({page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent(``); + await setContent(``); const [message] = await Promise.all([ page.waitForEvent('console'), output.waitFor('click'), @@ -77,10 +81,30 @@ describe.skip(USES_HOOKS)('Recorder', function() { expect(message.text()).toBe('click'); }); - it('should fill', async function({context, output, server}) { - const page = await context.newPage(); + it('should click after document.open', async function({page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent(``); + await setContent(``); + await page.evaluate(() => { + document.open(); + document.write(``); + document.close(); + // Give it time to refresh. See Recorder for details. + return new Promise(f => setTimeout(f, 1000)); + }); + 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({page, output, setContent, server}) { + await page.goto(server.EMPTY_PAGE); + await setContent(``); const [message] = await Promise.all([ page.waitForEvent('console'), output.waitFor('fill'), @@ -92,10 +116,9 @@ describe.skip(USES_HOOKS)('Recorder', function() { expect(message.text()).toBe('John'); }); - it('should press', async function({context, output, server}) { - const page = await context.newPage(); + it('should press', async function({page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent(``); + await setContent(``); const [message] = await Promise.all([ page.waitForEvent('console'), output.waitFor('press'), @@ -107,10 +130,9 @@ describe.skip(USES_HOOKS)('Recorder', function() { expect(message.text()).toBe('press'); }); - it('should check', async function({context, output, server}) { - const page = await context.newPage(); + it('should check', async function({page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent(``); + await setContent(``); const [message] = await Promise.all([ page.waitForEvent('console'), output.waitFor('check'), @@ -123,10 +145,9 @@ describe.skip(USES_HOOKS)('Recorder', function() { expect(message.text()).toBe("true"); }); - it('should uncheck', async function({context, output, server}) { - const page = await context.newPage(); + it('should uncheck', async function({page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent(``); + await setContent(``); const [message] = await Promise.all([ page.waitForEvent('console'), output.waitFor('uncheck'), @@ -138,10 +159,9 @@ describe.skip(USES_HOOKS)('Recorder', function() { expect(message.text()).toBe("false"); }); - it('should select', async function({context, output, server}) { - const page = await context.newPage(); + it('should select', async function({page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent(''); + await setContent(''); const [message] = await Promise.all([ page.waitForEvent('console'), output.waitFor('select'), @@ -153,10 +173,9 @@ describe.skip(USES_HOOKS)('Recorder', function() { expect(message.text()).toBe("2"); }); - it('should await popup', async function({context, output, server}) { - const page = await context.newPage(); + it('should await popup', async function({context, page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent('link'); + await setContent('link'); const [popup] = await Promise.all([ context.waitForEvent('page'), output.waitFor('waitForEvent'), @@ -171,10 +190,9 @@ describe.skip(USES_HOOKS)('Recorder', function() { expect(popup.url()).toBe(`${server.PREFIX}/popup/popup.html`); }); - it('should await navigation', async function({context, output, server}) { - const page = await context.newPage(); + it('should await navigation', async function({page, output, setContent, server}) { await page.goto(server.EMPTY_PAGE); - await page.setContent(`link`); + await setContent(`link`); await Promise.all([ page.waitForNavigation(), output.waitFor('waitForNavigation'),