feat(trace): record goto, setContent, goBack, goForward and reload (#3883)

This commit is contained in:
Dmitry Gozman 2020-09-15 09:46:36 -07:00 committed by GitHub
parent 8bc09af458
commit 592bae1cea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 44 additions and 38 deletions

View File

@ -21,7 +21,7 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche
import { parseArgument, serializeResult } from './jsHandleDispatcher'; import { parseArgument, serializeResult } from './jsHandleDispatcher';
import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers'; import { ResponseDispatcher, RequestDispatcher } from './networkDispatchers';
import { ActionMetadata } from '../server/instrumentation'; import { ActionMetadata } from '../server/instrumentation';
import { runAbortableTask } from '../server/progress'; import { ProgressController, runAbortableTask } from '../server/progress';
export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer> implements channels.FrameChannel { export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer> implements channels.FrameChannel {
private _frame: Frame; private _frame: Frame;
@ -53,8 +53,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
}); });
} }
async goto(params: channels.FrameGotoParams): Promise<channels.FrameGotoResult> { async goto(params: channels.FrameGotoParams, metadata?: channels.Metadata): Promise<channels.FrameGotoResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._frame.goto(params.url, params)) }; const page = this._frame._page;
const actionMetadata: ActionMetadata = { ...metadata, type: 'goto', value: params.url, page };
const controller = new ProgressController(page._timeoutSettings.navigationTimeout(params), actionMetadata);
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._frame.goto(controller, params.url, params)) };
} }
async frameElement(): Promise<channels.FrameFrameElementResult> { async frameElement(): Promise<channels.FrameFrameElementResult> {
@ -98,8 +101,11 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
return { value: await this._frame.content() }; return { value: await this._frame.content() };
} }
async setContent(params: channels.FrameSetContentParams): Promise<void> { async setContent(params: channels.FrameSetContentParams, metadata?: channels.Metadata): Promise<void> {
await this._frame.setContent(params.html, params); const page = this._frame._page;
const actionMetadata: ActionMetadata = { ...metadata, type: 'setContent', value: params.html, page };
const controller = new ProgressController(page._timeoutSettings.navigationTimeout(params), actionMetadata);
return await this._frame.setContent(controller, params.html, params);
} }
async addScriptTag(params: channels.FrameAddScriptTagParams): Promise<channels.FrameAddScriptTagResult> { async addScriptTag(params: channels.FrameAddScriptTagParams): Promise<channels.FrameAddScriptTagResult> {

View File

@ -31,6 +31,8 @@ import { ElementHandleDispatcher, createHandle } from './elementHandlerDispatche
import { FileChooser } from '../server/fileChooser'; import { FileChooser } from '../server/fileChooser';
import { CRCoverage } from '../server/chromium/crCoverage'; import { CRCoverage } from '../server/chromium/crCoverage';
import { VideoDispatcher } from './videoDispatcher'; import { VideoDispatcher } from './videoDispatcher';
import { ActionMetadata } from '../server/instrumentation';
import { ProgressController } from '../server/progress';
export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel { export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> implements channels.PageChannel {
private _page: Page; private _page: Page;
@ -94,16 +96,22 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
await this._page.setExtraHTTPHeaders(params.headers); await this._page.setExtraHTTPHeaders(params.headers);
} }
async reload(params: channels.PageReloadParams): Promise<channels.PageReloadResult> { async reload(params: channels.PageReloadParams, metadata?: channels.Metadata): Promise<channels.PageReloadResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(params)) }; const actionMetadata: ActionMetadata = { ...metadata, type: 'reload', page: this._page };
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata);
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.reload(controller, params)) };
} }
async goBack(params: channels.PageGoBackParams): Promise<channels.PageGoBackResult> { async goBack(params: channels.PageGoBackParams, metadata?: channels.Metadata): Promise<channels.PageGoBackResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(params)) }; const actionMetadata: ActionMetadata = { ...metadata, type: 'goBack', page: this._page };
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata);
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goBack(controller, params)) };
} }
async goForward(params: channels.PageGoForwardParams): Promise<channels.PageGoForwardResult> { async goForward(params: channels.PageGoForwardParams, metadata?: channels.Metadata): Promise<channels.PageGoForwardResult> {
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(params)) }; const actionMetadata: ActionMetadata = { ...metadata, type: 'goForward', page: this._page };
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(params), actionMetadata);
return { response: lookupNullableDispatcher<ResponseDispatcher>(await this._page.goForward(controller, params)) };
} }
async emulateMedia(params: channels.PageEmulateMediaParams): Promise<void> { async emulateMedia(params: channels.PageEmulateMediaParams): Promise<void> {

View File

@ -430,8 +430,7 @@ export class Frame extends EventEmitter {
this._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!'))); this._detachedPromise.then(() => controller.abort(new Error('Navigating frame was detached!')));
} }
async goto(url: string, options: types.GotoOptions = {}): Promise<network.Response | null> { async goto(controller: ProgressController, url: string, options: types.GotoOptions = {}): Promise<network.Response | null> {
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(options));
this.setupNavigationProgressController(controller); this.setupNavigationProgressController(controller);
return controller.run(async progress => { return controller.run(async progress => {
const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil); const waitUntil = verifyLifecycle('waitUntil', options.waitUntil === undefined ? 'load' : options.waitUntil);
@ -607,8 +606,7 @@ export class Frame extends EventEmitter {
}); });
} }
async setContent(html: string, options: types.NavigateOptions = {}): Promise<void> { async setContent(controller: ProgressController, html: string, options: types.NavigateOptions = {}): Promise<void> {
const controller = new ProgressController(this._page._timeoutSettings.navigationTimeout(options));
this.setupNavigationProgressController(controller); this.setupNavigationProgressController(controller);
return controller.run(async progress => { return controller.run(async progress => {
const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil; const waitUntil = options.waitUntil === undefined ? 'load' : options.waitUntil;

View File

@ -19,9 +19,9 @@ import type { ElementHandle } from './dom';
import type { Page } from './page'; import type { Page } from './page';
export type ActionMetadata = { export type ActionMetadata = {
type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck', type: 'click' | 'fill' | 'dblclick' | 'hover' | 'selectOption' | 'setInputFiles' | 'type' | 'press' | 'check' | 'uncheck' | 'goto' | 'setContent' | 'goBack' | 'goForward' | 'reload',
page: Page, page: Page,
target: ElementHandle | string, target?: ElementHandle | string,
value?: string, value?: string,
stack?: string, stack?: string,
}; };

View File

@ -262,8 +262,7 @@ export class Page extends EventEmitter {
this.emit(Page.Events.Console, message); this.emit(Page.Events.Console, message);
} }
async reload(options: types.NavigateOptions = {}): Promise<network.Response | null> { async reload(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = new ProgressController(this._timeoutSettings.navigationTimeout(options));
this.mainFrame().setupNavigationProgressController(controller); this.mainFrame().setupNavigationProgressController(controller);
const response = await controller.run(async progress => { const response = await controller.run(async progress => {
const waitPromise = this.mainFrame()._waitForNavigation(progress, options); const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
@ -274,8 +273,7 @@ export class Page extends EventEmitter {
return response; return response;
} }
async goBack(options: types.NavigateOptions = {}): Promise<network.Response | null> { async goBack(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = new ProgressController(this._timeoutSettings.navigationTimeout(options));
this.mainFrame().setupNavigationProgressController(controller); this.mainFrame().setupNavigationProgressController(controller);
const response = await controller.run(async progress => { const response = await controller.run(async progress => {
const waitPromise = this.mainFrame()._waitForNavigation(progress, options); const waitPromise = this.mainFrame()._waitForNavigation(progress, options);
@ -290,8 +288,7 @@ export class Page extends EventEmitter {
return response; return response;
} }
async goForward(options: types.NavigateOptions = {}): Promise<network.Response | null> { async goForward(controller: ProgressController, options: types.NavigateOptions): Promise<network.Response | null> {
const controller = new ProgressController(this._timeoutSettings.navigationTimeout(options));
this.mainFrame().setupNavigationProgressController(controller); this.mainFrame().setupNavigationProgressController(controller);
const response = await controller.run(async progress => { const response = await controller.run(async progress => {
const waitPromise = this.mainFrame()._waitForNavigation(progress, options); const waitPromise = this.mainFrame()._waitForNavigation(progress, options);

View File

@ -25,7 +25,7 @@ import { ActionResult, InstrumentingAgent, instrumentingAgents, ActionMetadata }
import { Page } from '../server/page'; import { Page } from '../server/page';
import { Snapshotter } from './snapshotter'; import { Snapshotter } from './snapshotter';
import * as types from '../server/types'; import * as types from '../server/types';
import type { ElementHandle } from '../server/dom'; import { ElementHandle } from '../server/dom';
import { helper, RegisteredListener } from '../server/helper'; import { helper, RegisteredListener } from '../server/helper';
import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings'; import { DEFAULT_TIMEOUT } from '../utils/timeoutSettings';
@ -144,6 +144,7 @@ class ContextTracer implements SnapshotterDelegate {
type: 'action', type: 'action',
contextId: this._contextId, contextId: this._contextId,
action: 'snapshot', action: 'snapshot',
pageId: this._pageToId.get(page),
label: options.label || 'snapshot', label: options.label || 'snapshot',
snapshot, snapshot,
}; };
@ -157,7 +158,7 @@ class ContextTracer implements SnapshotterDelegate {
contextId: this._contextId, contextId: this._contextId,
pageId: this._pageToId.get(metadata.page), pageId: this._pageToId.get(metadata.page),
action: metadata.type, action: metadata.type,
target: await this._targetToString(metadata.target), target: metadata.target instanceof ElementHandle ? await metadata.target._previewPromise : metadata.target,
value: metadata.value, value: metadata.value,
snapshot, snapshot,
startTime: result.startTime, startTime: result.startTime,
@ -193,10 +194,6 @@ class ContextTracer implements SnapshotterDelegate {
}); });
} }
private async _targetToString(target: ElementHandle | string): Promise<string> {
return typeof target === 'string' ? target : await target._previewPromise;
}
private async _takeSnapshot(page: Page, timeout: number = 0): Promise<{ sha1: string, duration: number } | undefined> { private async _takeSnapshot(page: Page, timeout: number = 0): Promise<{ sha1: string, duration: number } | undefined> {
if (!timeout) { if (!timeout) {
// Never use zero timeout to avoid stalling because of snapshot. // Never use zero timeout to avoid stalling because of snapshot.

View File

@ -17,13 +17,13 @@
import { it, expect, describe, options } from './playwright.fixtures'; import { it, expect, describe, options } from './playwright.fixtures';
function crash(pageImpl, browserName) { function crash(page, toImpl, browserName) {
if (browserName === 'chromium') if (browserName === 'chromium')
pageImpl.mainFrame().goto('chrome://crash').catch(e => {}); page.goto('chrome://crash').catch(e => {});
else if (browserName === 'webkit') else if (browserName === 'webkit')
pageImpl._delegate._session.send('Page.crash', {}).catch(e => {}); toImpl(page)._delegate._session.send('Page.crash', {}).catch(e => {});
else if (browserName === 'firefox') else if (browserName === 'firefox')
pageImpl._delegate._session.send('Page.crash', {}).catch(e => {}); toImpl(page)._delegate._session.send('Page.crash', {}).catch(e => {});
} }
describe('', (suite, parameters) => { describe('', (suite, parameters) => {
@ -32,13 +32,13 @@ describe('', (suite, parameters) => {
}, () => { }, () => {
it('should emit crash event when page crashes', async ({page, browserName, toImpl}) => { it('should emit crash event when page crashes', async ({page, browserName, toImpl}) => {
await page.setContent(`<div>This page should crash</div>`); await page.setContent(`<div>This page should crash</div>`);
crash(toImpl(page), browserName); crash(page, toImpl, browserName);
await new Promise(f => page.on('crash', f)); await new Promise(f => page.on('crash', f));
}); });
it('should throw on any action after page crashes', async ({page, browserName, toImpl}) => { it('should throw on any action after page crashes', async ({page, browserName, toImpl}) => {
await page.setContent(`<div>This page should crash</div>`); await page.setContent(`<div>This page should crash</div>`);
crash(toImpl(page), browserName); crash(page, toImpl, browserName);
await page.waitForEvent('crash'); await page.waitForEvent('crash');
const err = await page.evaluate(() => {}).then(() => null, e => e); const err = await page.evaluate(() => {}).then(() => null, e => e);
expect(err).toBeTruthy(); expect(err).toBeTruthy();
@ -48,7 +48,7 @@ describe('', (suite, parameters) => {
it('should cancel waitForEvent when page crashes', async ({page, browserName, toImpl}) => { it('should cancel waitForEvent when page crashes', async ({page, browserName, toImpl}) => {
await page.setContent(`<div>This page should crash</div>`); await page.setContent(`<div>This page should crash</div>`);
const promise = page.waitForEvent('response').catch(e => e); const promise = page.waitForEvent('response').catch(e => e);
crash(toImpl(page), browserName); crash(page, toImpl, browserName);
const error = await promise; const error = await promise;
expect(error.message).toContain('Page crashed'); expect(error.message).toContain('Page crashed');
}); });
@ -58,7 +58,7 @@ describe('', (suite, parameters) => {
server.setRoute('/one-style.css', () => {}); server.setRoute('/one-style.css', () => {});
const promise = page.goto(server.PREFIX + '/one-style.html').catch(e => e); const promise = page.goto(server.PREFIX + '/one-style.html').catch(e => e);
await page.waitForNavigation({ waitUntil: 'domcontentloaded' }); await page.waitForNavigation({ waitUntil: 'domcontentloaded' });
crash(toImpl(page), browserName); crash(page, toImpl, browserName);
const error = await promise; const error = await promise;
expect(error.message).toContain('Navigation failed because page crashed'); expect(error.message).toContain('Navigation failed because page crashed');
}); });
@ -68,7 +68,7 @@ describe('', (suite, parameters) => {
test.flaky(options.FIREFOX(parameters) && WIN); test.flaky(options.FIREFOX(parameters) && WIN);
}, async ({page, browserName, toImpl}) => { }, async ({page, browserName, toImpl}) => {
await page.setContent(`<div>This page should crash</div>`); await page.setContent(`<div>This page should crash</div>`);
crash(toImpl(page), browserName); crash(page, toImpl, browserName);
await page.waitForEvent('crash'); await page.waitForEvent('crash');
await page.context().close(); await page.context().close();
}); });