feat(inspector): pause on page/context close (#5319)

This commit is contained in:
Pavel Feldman 2021-02-19 09:33:24 -08:00 committed by GitHub
parent 8a9048c2b5
commit bb2b29631a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 85 additions and 40 deletions

View file

@ -171,7 +171,7 @@ class ConnectedBrowser extends BrowserDispatcher {
async close(): Promise<void> { async close(): Promise<void> {
// Only close our own contexts. // Only close our own contexts.
await Promise.all(this._contexts.map(context => context.close())); await Promise.all(this._contexts.map(context => context.close({}, internalCallMetadata())));
this._didClose(); this._didClose();
} }

View file

@ -65,8 +65,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}); });
} }
async newPage(): Promise<channels.BrowserContextNewPageResult> { async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage()) }; return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) };
} }
async cookies(params: channels.BrowserContextCookiesParams): Promise<channels.BrowserContextCookiesResult> { async cookies(params: channels.BrowserContextCookiesParams): Promise<channels.BrowserContextCookiesResult> {
@ -123,8 +123,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return await this._context.storageState(metadata); return await this._context.storageState(metadata);
} }
async close(): Promise<void> { async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise<void> {
await this._context.close(); await this._context.close(metadata);
} }
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> { async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {

View file

@ -146,7 +146,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
} }
async close(params: channels.PageCloseParams, metadata: CallMetadata): Promise<void> { async close(params: channels.PageCloseParams, metadata: CallMetadata): Promise<void> {
await this._page.close(params); await this._page.close(metadata, params);
} }
async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams, metadata: CallMetadata): Promise<void> { async setFileChooserInterceptedNoReply(params: channels.PageSetFileChooserInterceptedNoReplyParams, metadata: CallMetadata): Promise<void> {

View file

@ -738,6 +738,7 @@ export type BrowserContextPauseResult = void;
export type BrowserContextRecorderSupplementEnableParams = { export type BrowserContextRecorderSupplementEnableParams = {
language?: string, language?: string,
startRecording?: boolean, startRecording?: boolean,
pauseOnNextStatement?: boolean,
launchOptions?: any, launchOptions?: any,
contextOptions?: any, contextOptions?: any,
device?: string, device?: string,
@ -747,6 +748,7 @@ export type BrowserContextRecorderSupplementEnableParams = {
export type BrowserContextRecorderSupplementEnableOptions = { export type BrowserContextRecorderSupplementEnableOptions = {
language?: string, language?: string,
startRecording?: boolean, startRecording?: boolean,
pauseOnNextStatement?: boolean,
launchOptions?: any, launchOptions?: any,
contextOptions?: any, contextOptions?: any,
device?: string, device?: string,

View file

@ -648,6 +648,7 @@ BrowserContext:
parameters: parameters:
language: string? language: string?
startRecording: boolean? startRecording: boolean?
pauseOnNextStatement: boolean?
launchOptions: json? launchOptions: json?
contextOptions: json? contextOptions: json?
device: string? device: string?

View file

@ -363,6 +363,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.BrowserContextRecorderSupplementEnableParams = tObject({ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
language: tOptional(tString), language: tOptional(tString),
startRecording: tOptional(tBoolean), startRecording: tOptional(tBoolean),
pauseOnNextStatement: tOptional(tBoolean),
launchOptions: tOptional(tAny), launchOptions: tOptional(tAny),
contextOptions: tOptional(tAny), contextOptions: tOptional(tAny),
device: tOptional(tString), device: tOptional(tString),

View file

@ -72,13 +72,6 @@ export abstract class Browser extends SdkObject {
abstract isConnected(): boolean; abstract isConnected(): boolean;
abstract version(): string; abstract version(): string;
async newPage(options: types.BrowserContextOptions): Promise<Page> {
const context = await this.newContext(options);
const page = await context.newPage();
page._ownedContext = context;
return page;
}
_downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) { _downloadCreated(page: Page, uuid: string, url: string, suggestedFilename?: string) {
const download = new Download(page, this.options.downloadsPath || '', uuid, url, suggestedFilename); const download = new Download(page, this.options.downloadsPath || '', uuid, url, suggestedFilename);
this._downloads.set(uuid, download); this._downloads.set(uuid, download);

View file

@ -27,7 +27,7 @@ import { Progress } from './progress';
import { Selectors, serverSelectors } from './selectors'; import { Selectors, serverSelectors } from './selectors';
import * as types from './types'; import * as types from './types';
import path from 'path'; import path from 'path';
import { CallMetadata, SdkObject } from './instrumentation'; import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';
export class Video { export class Video {
readonly _videoId: string; readonly _videoId: string;
@ -209,8 +209,8 @@ export abstract class BrowserContext extends SdkObject {
// - chromium fails to change isMobile for existing page; // - chromium fails to change isMobile for existing page;
// - webkit fails to change locale for existing page. // - webkit fails to change locale for existing page.
const oldPage = pages[0]; const oldPage = pages[0];
await this.newPage(); await this.newPage(progress.metadata);
await oldPage.close(); await oldPage.close(progress.metadata);
} }
} }
@ -245,7 +245,7 @@ export abstract class BrowserContext extends SdkObject {
return this._closedStatus !== 'open'; return this._closedStatus !== 'open';
} }
async close() { async close(metadata: CallMetadata) {
if (this._closedStatus === 'open') { if (this._closedStatus === 'open') {
this.emit(BrowserContext.Events.BeforeClose); this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing'; this._closedStatus = 'closing';
@ -255,7 +255,7 @@ export abstract class BrowserContext extends SdkObject {
if (this._isPersistentContext) { if (this._isPersistentContext) {
// Close all the pages instead of the context, // Close all the pages instead of the context,
// because we cannot close the default context. // because we cannot close the default context.
await Promise.all(this.pages().map(page => page.close())); await Promise.all(this.pages().map(page => page.close(metadata)));
} else { } else {
// Close the context. // Close the context.
await this._doClose(); await this._doClose();
@ -286,7 +286,7 @@ export abstract class BrowserContext extends SdkObject {
await this._closePromise; await this._closePromise;
} }
async newPage(): Promise<Page> { async newPage(metadata: CallMetadata): Promise<Page> {
const pageDelegate = await this.newPageDelegate(); const pageDelegate = await this.newPageDelegate();
const pageOrError = await pageDelegate.pageOrError(); const pageOrError = await pageDelegate.pageOrError();
if (pageOrError instanceof Page) { if (pageOrError instanceof Page) {
@ -307,7 +307,8 @@ export abstract class BrowserContext extends SdkObject {
origins: [] origins: []
}; };
if (this._origins.size) { if (this._origins.size) {
const page = await this.newPage(); const internalMetadata = internalCallMetadata();
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => { await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>' }).catch(() => {}); handler.fulfill({ body: '<html></html>' }).catch(() => {});
}); });
@ -315,13 +316,13 @@ export abstract class BrowserContext extends SdkObject {
const originStorage: types.OriginStorage = { origin, localStorage: [] }; const originStorage: types.OriginStorage = { origin, localStorage: [] };
result.origins.push(originStorage); result.origins.push(originStorage);
const frame = page.mainFrame(); const frame = page.mainFrame();
await frame.goto(metadata, origin); await frame.goto(internalMetadata, origin);
const storage = await frame._evaluateExpression(`({ const storage = await frame._evaluateExpression(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })), localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, false, undefined, 'utility'); })`, false, undefined, 'utility');
originStorage.localStorage = storage.localStorage; originStorage.localStorage = storage.localStorage;
} }
await page.close(); await page.close(internalMetadata);
} }
return result; return result;
} }
@ -330,7 +331,8 @@ export abstract class BrowserContext extends SdkObject {
if (state.cookies) if (state.cookies)
await this.addCookies(state.cookies); await this.addCookies(state.cookies);
if (state.origins && state.origins.length) { if (state.origins && state.origins.length) {
const page = await this.newPage(); const internalMetadata = internalCallMetadata();
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => { await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>' }).catch(() => {}); handler.fulfill({ body: '<html></html>' }).catch(() => {});
}); });
@ -343,7 +345,7 @@ export abstract class BrowserContext extends SdkObject {
localStorage.setItem(name, value); localStorage.setItem(name, value);
}`, true, originState, 'utility'); }`, true, originState, 'utility');
} }
await page.close(); await page.close(internalMetadata);
} }
} }

View file

@ -428,7 +428,7 @@ export class Page extends SdkObject {
this._timeoutSettings.timeout(options)); this._timeoutSettings.timeout(options));
} }
async close(options?: { runBeforeUnload?: boolean }) { async close(metadata: CallMetadata, options?: { runBeforeUnload?: boolean }) {
if (this._closedState === 'closed') if (this._closedState === 'closed')
return; return;
const runBeforeUnload = !!options && !!options.runBeforeUnload; const runBeforeUnload = !!options && !!options.runBeforeUnload;
@ -442,7 +442,7 @@ export class Page extends SdkObject {
if (!runBeforeUnload) if (!runBeforeUnload)
await this._closedPromise; await this._closedPromise;
if (this._ownedContext) if (this._ownedContext)
await this._ownedContext.close(); await this._ownedContext.close(metadata);
} }
private _setIsError() { private _setIsError() {

View file

@ -25,7 +25,7 @@ export class InspectorController implements InstrumentationListener {
async onContextCreated(context: BrowserContext): Promise<void> { async onContextCreated(context: BrowserContext): Promise<void> {
if (isDebugMode()) if (isDebugMode())
RecorderSupplement.getOrCreate(context); RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });
} }
async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> { async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
@ -52,12 +52,8 @@ export class InspectorController implements InstrumentationListener {
} }
} }
if (metadata.method === 'pause') { if (shouldOpenInspector(sdkObject, metadata))
// Force create recorder on pause. RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });
if (!context._browser.options.headful && !isUnderTest())
return;
RecorderSupplement.getOrCreate(context);
}
const recorder = await RecorderSupplement.getNoCreate(context); const recorder = await RecorderSupplement.getNoCreate(context);
await recorder?.onBeforeCall(sdkObject, metadata); await recorder?.onBeforeCall(sdkObject, metadata);
@ -104,3 +100,9 @@ export class InspectorController implements InstrumentationListener {
await recorder?.updateCallLog([metadata]); await recorder?.updateCallLog([metadata]);
} }
} }
function shouldOpenInspector(sdkObject: SdkObject, metadata: CallMetadata): boolean {
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
return false;
return metadata.method === 'pause';
}

View file

@ -53,7 +53,7 @@ export class RecorderApp extends EventEmitter {
} }
async close() { async close() {
await this._page.context().close(); await this._page.context().close(internalCallMetadata());
} }
private async _init() { private async _init() {
@ -85,7 +85,7 @@ export class RecorderApp extends EventEmitter {
this._page.once('close', () => { this._page.once('close', () => {
this.emit('close'); this.emit('close');
this._page.context().close().catch(e => console.error(e)); this._page.context().close(internalCallMetadata()).catch(e => console.error(e));
}); });
const mainFrame = this._page.mainFrame(); const mainFrame = this._page.mainFrame();

View file

@ -50,7 +50,7 @@ export class RecorderSupplement {
private _params: channels.BrowserContextRecorderSupplementEnableParams; private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>(); private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
private _pausedCallsMetadata = new Map<CallMetadata, () => void>(); private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
private _pauseOnNextStatement = false; private _pauseOnNextStatement: boolean;
private _recorderSources: Source[]; private _recorderSources: Source[];
private _userSources = new Map<string, Source>(); private _userSources = new Map<string, Source>();
@ -72,6 +72,7 @@ export class RecorderSupplement {
this._context = context; this._context = context;
this._params = params; this._params = params;
this._mode = params.startRecording ? 'recording' : 'none'; this._mode = params.startRecording ? 'recording' : 'none';
this._pauseOnNextStatement = !!params.pauseOnNextStatement;
const language = params.language || context._options.sdkLanguage; const language = params.language || context._options.sdkLanguage;
const languages = new Set([ const languages = new Set([
@ -367,7 +368,7 @@ export class RecorderSupplement {
this._currentCallsMetadata.set(metadata, sdkObject); this._currentCallsMetadata.set(metadata, sdkObject);
this._updateUserSources(); this._updateUserSources();
this.updateCallLog([metadata]); this.updateCallLog([metadata]);
if (metadata.method === 'pause' || (this._pauseOnNextStatement && metadata.method === 'goto')) if (shouldPauseOnCall(sdkObject, metadata) || (this._pauseOnNextStatement && shouldPauseOnStep(sdkObject, metadata)))
await this.pause(metadata); await this.pause(metadata);
if (metadata.params && metadata.params.selector) { if (metadata.params && metadata.params.selector) {
this._highlightedSelector = metadata.params.selector; this._highlightedSelector = metadata.params.selector;
@ -477,4 +478,14 @@ function languageForFile(file: string) {
if (file.endsWith('.cs')) if (file.endsWith('.cs'))
return 'csharp'; return 'csharp';
return 'javascript'; return 'javascript';
} }
function shouldPauseOnCall(sdkObject: SdkObject, metadata: CallMetadata): boolean {
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
return false;
return metadata.method === 'pause';
}
function shouldPauseOnStep(sdkObject: SdkObject, metadata: CallMetadata): boolean {
return metadata.method === 'goto' || metadata.method === 'close';
}

View file

@ -17,11 +17,20 @@
import { expect } from 'folio'; import { expect } from 'folio';
import { Page } from '..'; import { Page } from '..';
import { folio } from './recorder.fixtures'; import { folio } from './recorder.fixtures';
const { it, describe} = folio; const { afterEach, it, describe } = folio;
describe('pause', (suite, { mode }) => { describe('pause', (suite, { mode }) => {
suite.skip(mode !== 'default'); suite.skip(mode !== 'default');
}, () => { }, () => {
afterEach(async ({ recorderPageGetter }) => {
try {
const recorderPage = await recorderPageGetter();
recorderPage.click('[title=Resume]').catch(() => {});
} catch (e) {
// Some tests close context.
}
});
it('should pause and resume the script', async ({ page, recorderPageGetter }) => { it('should pause and resume the script', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => { const scriptPromise = (async () => {
await page.pause(); await page.pause();
@ -117,7 +126,7 @@ describe('pause', (suite, { mode }) => {
expect(Math.abs(x1 - x2) < 2).toBeTruthy(); expect(Math.abs(x1 - x2) < 2).toBeTruthy();
expect(Math.abs(y1 - y2) < 2).toBeTruthy(); expect(Math.abs(y1 - y2) < 2).toBeTruthy();
await recorderPage.click('[title="Step over"]'); await recorderPage.click('[title=Resume]');
await scriptPromise; await scriptPromise;
}); });
@ -196,6 +205,30 @@ describe('pause', (suite, { mode }) => {
const error = await scriptPromise; const error = await scriptPromise;
expect(error.message).toContain('Not a checkbox or radio button'); expect(error.message).toContain('Not a checkbox or radio button');
}); });
it('should pause on page close', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
await page.pause();
await page.close();
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.close();")');
await recorderPage.click('[title=Resume]');
await scriptPromise;
});
it('should pause on context close', async ({ page, recorderPageGetter }) => {
const scriptPromise = (async () => {
await page.pause();
await page.context().close();
})();
const recorderPage = await recorderPageGetter();
await recorderPage.click('[title="Step over"]');
await recorderPage.waitForSelector('.source-line-paused:has-text("page.context().close();")');
await recorderPage.click('[title=Resume]');
await scriptPromise;
});
}); });
async function sanitizeLog(recorderPage: Page): Promise<string[]> { async function sanitizeLog(recorderPage: Page): Promise<string[]> {