Skip to content

Commit

Permalink
feat(inspector): pause on page/context close (#5319)
Browse files Browse the repository at this point in the history
  • Loading branch information
pavelfeldman committed Feb 19, 2021
1 parent 8a9048c commit bb2b296
Show file tree
Hide file tree
Showing 13 changed files with 85 additions and 40 deletions.
2 changes: 1 addition & 1 deletion src/browserServerImpl.ts
Expand Up @@ -171,7 +171,7 @@ class ConnectedBrowser extends BrowserDispatcher {

async close(): Promise<void> {
// 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();
}

Expand Down
8 changes: 4 additions & 4 deletions src/dispatchers/browserContextDispatcher.ts
Expand Up @@ -65,8 +65,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
});
}

async newPage(): Promise<channels.BrowserContextNewPageResult> {
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage()) };
async newPage(params: channels.BrowserContextNewPageParams, metadata: CallMetadata): Promise<channels.BrowserContextNewPageResult> {
return { page: lookupDispatcher<PageDispatcher>(await this._context.newPage(metadata)) };
}

async cookies(params: channels.BrowserContextCookiesParams): Promise<channels.BrowserContextCookiesResult> {
Expand Down Expand Up @@ -123,8 +123,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
return await this._context.storageState(metadata);
}

async close(): Promise<void> {
await this._context.close();
async close(params: channels.BrowserContextCloseParams, metadata: CallMetadata): Promise<void> {
await this._context.close(metadata);
}

async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
Expand Down
2 changes: 1 addition & 1 deletion src/dispatchers/pageDispatcher.ts
Expand Up @@ -146,7 +146,7 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
}

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> {
Expand Down
2 changes: 2 additions & 0 deletions src/protocol/channels.ts
Expand Up @@ -738,6 +738,7 @@ export type BrowserContextPauseResult = void;
export type BrowserContextRecorderSupplementEnableParams = {
language?: string,
startRecording?: boolean,
pauseOnNextStatement?: boolean,
launchOptions?: any,
contextOptions?: any,
device?: string,
Expand All @@ -747,6 +748,7 @@ export type BrowserContextRecorderSupplementEnableParams = {
export type BrowserContextRecorderSupplementEnableOptions = {
language?: string,
startRecording?: boolean,
pauseOnNextStatement?: boolean,
launchOptions?: any,
contextOptions?: any,
device?: string,
Expand Down
1 change: 1 addition & 0 deletions src/protocol/protocol.yml
Expand Up @@ -648,6 +648,7 @@ BrowserContext:
parameters:
language: string?
startRecording: boolean?
pauseOnNextStatement: boolean?
launchOptions: json?
contextOptions: json?
device: string?
Expand Down
1 change: 1 addition & 0 deletions src/protocol/validator.ts
Expand Up @@ -363,6 +363,7 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
language: tOptional(tString),
startRecording: tOptional(tBoolean),
pauseOnNextStatement: tOptional(tBoolean),
launchOptions: tOptional(tAny),
contextOptions: tOptional(tAny),
device: tOptional(tString),
Expand Down
7 changes: 0 additions & 7 deletions src/server/browser.ts
Expand Up @@ -72,13 +72,6 @@ export abstract class Browser extends SdkObject {
abstract isConnected(): boolean;
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) {
const download = new Download(page, this.options.downloadsPath || '', uuid, url, suggestedFilename);
this._downloads.set(uuid, download);
Expand Down
24 changes: 13 additions & 11 deletions src/server/browserContext.ts
Expand Up @@ -27,7 +27,7 @@ import { Progress } from './progress';
import { Selectors, serverSelectors } from './selectors';
import * as types from './types';
import path from 'path';
import { CallMetadata, SdkObject } from './instrumentation';
import { CallMetadata, internalCallMetadata, SdkObject } from './instrumentation';

export class Video {
readonly _videoId: string;
Expand Down Expand Up @@ -209,8 +209,8 @@ export abstract class BrowserContext extends SdkObject {
// - chromium fails to change isMobile for existing page;
// - webkit fails to change locale for existing page.
const oldPage = pages[0];
await this.newPage();
await oldPage.close();
await this.newPage(progress.metadata);
await oldPage.close(progress.metadata);
}
}

Expand Down Expand Up @@ -245,7 +245,7 @@ export abstract class BrowserContext extends SdkObject {
return this._closedStatus !== 'open';
}

async close() {
async close(metadata: CallMetadata) {
if (this._closedStatus === 'open') {
this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing';
Expand All @@ -255,7 +255,7 @@ export abstract class BrowserContext extends SdkObject {
if (this._isPersistentContext) {
// Close all the pages instead of the 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 {
// Close the context.
await this._doClose();
Expand Down Expand Up @@ -286,7 +286,7 @@ export abstract class BrowserContext extends SdkObject {
await this._closePromise;
}

async newPage(): Promise<Page> {
async newPage(metadata: CallMetadata): Promise<Page> {
const pageDelegate = await this.newPageDelegate();
const pageOrError = await pageDelegate.pageOrError();
if (pageOrError instanceof Page) {
Expand All @@ -307,21 +307,22 @@ export abstract class BrowserContext extends SdkObject {
origins: []
};
if (this._origins.size) {
const page = await this.newPage();
const internalMetadata = internalCallMetadata();
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>' }).catch(() => {});
});
for (const origin of this._origins) {
const originStorage: types.OriginStorage = { origin, localStorage: [] };
result.origins.push(originStorage);
const frame = page.mainFrame();
await frame.goto(metadata, origin);
await frame.goto(internalMetadata, origin);
const storage = await frame._evaluateExpression(`({
localStorage: Object.keys(localStorage).map(name => ({ name, value: localStorage.getItem(name) })),
})`, false, undefined, 'utility');
originStorage.localStorage = storage.localStorage;
}
await page.close();
await page.close(internalMetadata);
}
return result;
}
Expand All @@ -330,7 +331,8 @@ export abstract class BrowserContext extends SdkObject {
if (state.cookies)
await this.addCookies(state.cookies);
if (state.origins && state.origins.length) {
const page = await this.newPage();
const internalMetadata = internalCallMetadata();
const page = await this.newPage(internalMetadata);
await page._setServerRequestInterceptor(handler => {
handler.fulfill({ body: '<html></html>' }).catch(() => {});
});
Expand All @@ -343,7 +345,7 @@ export abstract class BrowserContext extends SdkObject {
localStorage.setItem(name, value);
}`, true, originState, 'utility');
}
await page.close();
await page.close(internalMetadata);
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/server/page.ts
Expand Up @@ -428,7 +428,7 @@ export class Page extends SdkObject {
this._timeoutSettings.timeout(options));
}

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

private _setIsError() {
Expand Down
16 changes: 9 additions & 7 deletions src/server/supplements/inspectorController.ts
Expand Up @@ -25,7 +25,7 @@ export class InspectorController implements InstrumentationListener {

async onContextCreated(context: BrowserContext): Promise<void> {
if (isDebugMode())
RecorderSupplement.getOrCreate(context);
RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });
}

async onBeforeCall(sdkObject: SdkObject, metadata: CallMetadata): Promise<void> {
Expand All @@ -52,12 +52,8 @@ export class InspectorController implements InstrumentationListener {
}
}

if (metadata.method === 'pause') {
// Force create recorder on pause.
if (!context._browser.options.headful && !isUnderTest())
return;
RecorderSupplement.getOrCreate(context);
}
if (shouldOpenInspector(sdkObject, metadata))
RecorderSupplement.getOrCreate(context, { pauseOnNextStatement: true });

const recorder = await RecorderSupplement.getNoCreate(context);
await recorder?.onBeforeCall(sdkObject, metadata);
Expand Down Expand Up @@ -104,3 +100,9 @@ export class InspectorController implements InstrumentationListener {
await recorder?.updateCallLog([metadata]);
}
}

function shouldOpenInspector(sdkObject: SdkObject, metadata: CallMetadata): boolean {
if (!sdkObject.attribution.browser?.options.headful && !isUnderTest())
return false;
return metadata.method === 'pause';
}
4 changes: 2 additions & 2 deletions src/server/supplements/recorder/recorderApp.ts
Expand Up @@ -53,7 +53,7 @@ export class RecorderApp extends EventEmitter {
}

async close() {
await this._page.context().close();
await this._page.context().close(internalCallMetadata());
}

private async _init() {
Expand Down Expand Up @@ -85,7 +85,7 @@ export class RecorderApp extends EventEmitter {

this._page.once('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();
Expand Down
17 changes: 14 additions & 3 deletions src/server/supplements/recorderSupplement.ts
Expand Up @@ -50,7 +50,7 @@ export class RecorderSupplement {
private _params: channels.BrowserContextRecorderSupplementEnableParams;
private _currentCallsMetadata = new Map<CallMetadata, SdkObject>();
private _pausedCallsMetadata = new Map<CallMetadata, () => void>();
private _pauseOnNextStatement = false;
private _pauseOnNextStatement: boolean;
private _recorderSources: Source[];
private _userSources = new Map<string, Source>();

Expand All @@ -72,6 +72,7 @@ export class RecorderSupplement {
this._context = context;
this._params = params;
this._mode = params.startRecording ? 'recording' : 'none';
this._pauseOnNextStatement = !!params.pauseOnNextStatement;
const language = params.language || context._options.sdkLanguage;

const languages = new Set([
Expand Down Expand Up @@ -367,7 +368,7 @@ export class RecorderSupplement {
this._currentCallsMetadata.set(metadata, sdkObject);
this._updateUserSources();
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);
if (metadata.params && metadata.params.selector) {
this._highlightedSelector = metadata.params.selector;
Expand Down Expand Up @@ -477,4 +478,14 @@ function languageForFile(file: string) {
if (file.endsWith('.cs'))
return 'csharp';
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';
}
37 changes: 35 additions & 2 deletions test/pause.spec.ts
Expand Up @@ -17,11 +17,20 @@
import { expect } from 'folio';
import { Page } from '..';
import { folio } from './recorder.fixtures';
const { it, describe} = folio;
const { afterEach, it, describe } = folio;

describe('pause', (suite, { mode }) => {
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 }) => {
const scriptPromise = (async () => {
await page.pause();
Expand Down Expand Up @@ -117,7 +126,7 @@ describe('pause', (suite, { mode }) => {
expect(Math.abs(x1 - x2) < 2).toBeTruthy();
expect(Math.abs(y1 - y2) < 2).toBeTruthy();

await recorderPage.click('[title="Step over"]');
await recorderPage.click('[title=Resume]');
await scriptPromise;
});

Expand Down Expand Up @@ -196,6 +205,30 @@ describe('pause', (suite, { mode }) => {
const error = await scriptPromise;
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[]> {
Expand Down

0 comments on commit bb2b296

Please sign in to comment.