chore: introduce debug toolbar (#5145)

This commit is contained in:
Pavel Feldman 2021-01-25 14:49:26 -08:00 committed by GitHub
parent 894abbfe28
commit 01d6f83597
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 412 additions and 246 deletions

View File

@ -30,9 +30,6 @@ import { Browser } from '../client/browser';
import { Page } from '../client/page';
import { BrowserType } from '../client/browserType';
import { BrowserContextOptions, LaunchOptions } from '../client/types';
import { RecorderOutput, RecorderSupplement } from '../client/supplements/recorderSupplement';
import { ConsoleApiSupplement } from '../client/supplements/consoleApiSupplement';
import { FileOutput, OutputMultiplexer, TerminalOutput } from '../client/supplements/recorderOutputs';
program
.version('Version ' + require('../../package.json').version)
@ -318,7 +315,7 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
async function open(options: Options, url: string | undefined) {
const { context } = await launchContext(options, false);
new ConsoleApiSupplement(context);
await context._enableConsoleApi();
await openPage(context, url);
if (process.env.PWCLI_EXIT_FOR_TEST)
await Promise.all(context.pages().map(p => p.close()));
@ -326,23 +323,9 @@ async function open(options: Options, url: string | undefined) {
async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) {
const { context, launchOptions, contextOptions } = await launchContext(options, false);
let highlighterType = language;
if (highlighterType === 'python-async')
highlighterType = 'python';
const outputs: RecorderOutput[] = [TerminalOutput.create(process.stdout, highlighterType)];
if (outputFile)
outputs.push(new FileOutput(outputFile));
const output = new OutputMultiplexer(outputs);
new ConsoleApiSupplement(context);
new RecorderSupplement(context,
language,
launchOptions,
contextOptions,
options.device,
options.saveStorage,
output);
if (process.env.PWTRACE)
contextOptions._traceDir = path.join(process.cwd(), '.trace');
await context._enableRecorder(language, launchOptions, contextOptions, options.device, options.saveStorage, !!process.stdout.columns, outputFile ? path.resolve(outputFile) : undefined);
await openPage(context, url);
if (process.env.PWCLI_EXIT_FOR_TEST)
await Promise.all(context.pages().map(p => p.close()));

View File

@ -26,7 +26,7 @@ import { Browser } from './browser';
import { Events } from './events';
import { TimeoutSettings } from '../utils/timeoutSettings';
import { Waiter } from './waiter';
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState } from './types';
import { URLMatch, Headers, WaitForEventOptions, BrowserContextOptions, StorageState, LaunchOptions } from './types';
import { isUnderTest, headersObjectToArray, mkdirIfNeeded } from '../utils/utils';
import { isSafeCloseError } from '../utils/errors';
import * as api from '../../types/types';
@ -44,6 +44,8 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
_ownerPage: Page | undefined;
private _closedPromise: Promise<void>;
_options: channels.BrowserNewContextParams = {};
private _stdout: NodeJS.WriteStream;
private _stderr: NodeJS.WriteStream;
static from(context: channels.BrowserContextChannel): BrowserContext {
return (context as any)._object;
@ -62,6 +64,10 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
this._channel.on('close', () => this._onClose());
this._channel.on('page', ({page}) => this._onPage(Page.from(page)));
this._channel.on('route', ({ route, request }) => this._onRoute(network.Route.from(route), network.Request.from(request)));
this._stdout = process.stdout;
this._stderr = process.stderr;
this._channel.on('stdout', ({ data }) => this._stdout.write(Buffer.from(data, 'base64')));
this._channel.on('stderr', ({ data }) => this._stderr.write(Buffer.from(data, 'base64')));
this._closedPromise = new Promise(f => this.once(Events.BrowserContext.Close, f));
}
@ -253,6 +259,35 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
throw e;
}
}
async _pause() {
return this._wrapApiCall('browserContext.pause', async () => {
await this._channel.pause();
});
}
async _enableConsoleApi() {
await this._channel.consoleSupplementExpose();
}
async _enableRecorder(
language: string,
launchOptions?: LaunchOptions,
contextOptions?: BrowserContextOptions,
device?: string,
saveStorage?: string,
terminal?: boolean,
outputFile?: string) {
await this._channel.recorderSupplementEnable({
language,
launchOptions,
contextOptions,
device,
saveStorage,
terminal,
outputFile,
});
}
}
export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise<channels.BrowserNewContextOptions> {

View File

@ -640,9 +640,7 @@ export class Page extends ChannelOwner<channels.PageChannel, channels.PageInitia
}
async _pause() {
return this._wrapApiCall('page.pause', async () => {
await this._channel.pause();
});
await this.context()._pause();
}
async _pdf(options: PDFOptions = {}): Promise<Buffer> {

View File

@ -1,23 +0,0 @@
/**
* 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.
*/
import { BrowserContext } from '../browserContext';
export class ConsoleApiSupplement {
constructor(context: BrowserContext) {
context._channel.consoleSupplementExpose().catch(e => {});
}
}

View File

@ -1,51 +0,0 @@
/**
* 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.
*/
import * as path from 'path';
import { BrowserContext } from '../browserContext';
import { BrowserContextOptions, LaunchOptions } from '../types';
export class RecorderSupplement {
constructor(context: BrowserContext,
language: string,
launchOptions: LaunchOptions,
contextOptions: BrowserContextOptions,
device: string | undefined,
saveStorage: string | undefined,
output: RecorderOutput) {
if (process.env.PWTRACE)
contextOptions._traceDir = path.join(process.cwd(), '.trace');
context._channel.on('recorderSupplementPrintLn', event => output.printLn(event.text));
context._channel.on('recorderSupplementPopLn', event => output.popLn(event.text));
context.on('close', () => output.flush());
context._channel.recorderSupplementEnable({
language,
launchOptions,
contextOptions,
device,
saveStorage,
}).catch(e => {});
}
}
export interface RecorderOutput {
printLn(text: string): void;
popLn(text: string): void;
flush(): void;
}

View File

@ -38,6 +38,8 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
this._dispatchEvent('close');
this._dispose();
});
context.on(BrowserContext.Events.StdOut, data => this._dispatchEvent('stdout', { data: Buffer.from(data, 'utf8').toString('base64') }));
context.on(BrowserContext.Events.StdErr, data => this._dispatchEvent('stderr', { data: Buffer.from(data, 'utf8').toString('base64') }));
if (context._browser._options.name === 'chromium') {
for (const page of (context as CRBrowserContext).backgroundPages())
@ -133,11 +135,17 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
}
async recorderSupplementEnable(params: channels.BrowserContextRecorderSupplementEnableParams): Promise<void> {
const recorder = new RecorderSupplement(this._context, params, {
printLn: text => this._dispatchEvent('recorderSupplementPrintLn', { text }),
popLn: text => this._dispatchEvent('recorderSupplementPopLn', { text }),
await RecorderSupplement.getOrCreate(this._context, 'codegen', params);
}
async pause() {
if (!this._context._browser._options.headful)
return;
const recorder = await RecorderSupplement.getOrCreate(this._context, 'pause', {
language: 'javascript',
terminal: true
});
await recorder.install();
await recorder.pause();
}
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {

View File

@ -237,10 +237,6 @@ export class PageDispatcher extends Dispatcher<Page, channels.PageInitializer> i
return { entries: await coverage.stopCSSCoverage() };
}
async pause() {
await this._page.pause();
}
_onFrameAttached(frame: Frame) {
this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) });
}

View File

@ -533,10 +533,10 @@ export interface BrowserContextChannel extends Channel {
on(event: 'close', callback: (params: BrowserContextCloseEvent) => void): this;
on(event: 'page', callback: (params: BrowserContextPageEvent) => void): this;
on(event: 'route', callback: (params: BrowserContextRouteEvent) => void): this;
on(event: 'stdout', callback: (params: BrowserContextStdoutEvent) => void): this;
on(event: 'stderr', callback: (params: BrowserContextStderrEvent) => void): this;
on(event: 'crBackgroundPage', callback: (params: BrowserContextCrBackgroundPageEvent) => void): this;
on(event: 'crServiceWorker', callback: (params: BrowserContextCrServiceWorkerEvent) => void): this;
on(event: 'recorderSupplementPrintLn', callback: (params: BrowserContextRecorderSupplementPrintLnEvent) => void): this;
on(event: 'recorderSupplementPopLn', callback: (params: BrowserContextRecorderSupplementPopLnEvent) => void): this;
addCookies(params: BrowserContextAddCookiesParams, metadata?: Metadata): Promise<BrowserContextAddCookiesResult>;
addInitScript(params: BrowserContextAddInitScriptParams, metadata?: Metadata): Promise<BrowserContextAddInitScriptResult>;
clearCookies(params?: BrowserContextClearCookiesParams, metadata?: Metadata): Promise<BrowserContextClearCookiesResult>;
@ -555,6 +555,7 @@ export interface BrowserContextChannel extends Channel {
setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise<BrowserContextSetOfflineResult>;
storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise<BrowserContextStorageStateResult>;
consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise<BrowserContextConsoleSupplementExposeResult>;
pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise<BrowserContextPauseResult>;
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextCrNewCDPSessionResult>;
}
@ -569,18 +570,18 @@ export type BrowserContextRouteEvent = {
route: RouteChannel,
request: RequestChannel,
};
export type BrowserContextStdoutEvent = {
data: Binary,
};
export type BrowserContextStderrEvent = {
data: Binary,
};
export type BrowserContextCrBackgroundPageEvent = {
page: PageChannel,
};
export type BrowserContextCrServiceWorkerEvent = {
worker: WorkerChannel,
};
export type BrowserContextRecorderSupplementPrintLnEvent = {
text: string,
};
export type BrowserContextRecorderSupplementPopLnEvent = {
text: string,
};
export type BrowserContextAddCookiesParams = {
cookies: SetNetworkCookie[],
};
@ -706,16 +707,25 @@ export type BrowserContextStorageStateResult = {
export type BrowserContextConsoleSupplementExposeParams = {};
export type BrowserContextConsoleSupplementExposeOptions = {};
export type BrowserContextConsoleSupplementExposeResult = void;
export type BrowserContextPauseParams = {};
export type BrowserContextPauseOptions = {};
export type BrowserContextPauseResult = void;
export type BrowserContextRecorderSupplementEnableParams = {
language: string,
launchOptions: any,
contextOptions: any,
launchOptions?: any,
contextOptions?: any,
device?: string,
saveStorage?: string,
terminal?: boolean,
outputFile?: string,
};
export type BrowserContextRecorderSupplementEnableOptions = {
launchOptions?: any,
contextOptions?: any,
device?: string,
saveStorage?: string,
terminal?: boolean,
outputFile?: string,
};
export type BrowserContextRecorderSupplementEnableResult = void;
export type BrowserContextCrNewCDPSessionParams = {
@ -786,7 +796,6 @@ export interface PageChannel extends Channel {
mouseClick(params: PageMouseClickParams, metadata?: Metadata): Promise<PageMouseClickResult>;
touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise<PageTouchscreenTapResult>;
accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise<PageAccessibilitySnapshotResult>;
pause(params?: PagePauseParams, metadata?: Metadata): Promise<PagePauseResult>;
pdf(params: PagePdfParams, metadata?: Metadata): Promise<PagePdfResult>;
crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise<PageCrStartJSCoverageResult>;
crStopJSCoverage(params?: PageCrStopJSCoverageParams, metadata?: Metadata): Promise<PageCrStopJSCoverageResult>;
@ -1083,9 +1092,6 @@ export type PageAccessibilitySnapshotOptions = {
export type PageAccessibilitySnapshotResult = {
rootAXNode?: AXNode,
};
export type PagePauseParams = {};
export type PagePauseOptions = {};
export type PagePauseResult = void;
export type PagePdfParams = {
scale?: number,
displayHeaderFooter?: boolean,

View File

@ -602,14 +602,19 @@ BrowserContext:
consoleSupplementExpose:
experimental: True
pause:
experimental: True
recorderSupplementEnable:
experimental: True
parameters:
language: string
launchOptions: json
contextOptions: json
launchOptions: json?
contextOptions: json?
device: string?
saveStorage: string?
terminal: boolean?
outputFile: string?
crNewCDPSession:
parameters:
@ -634,6 +639,14 @@ BrowserContext:
route: Route
request: Request
stdout:
parameters:
data: binary
stderr:
parameters:
data: binary
crBackgroundPage:
parameters:
page: Page
@ -642,14 +655,6 @@ BrowserContext:
parameters:
worker: Worker
recorderSupplementPrintLn:
parameters:
text: string
recorderSupplementPopLn:
parameters:
text: string
Page:
type: interface
@ -854,8 +859,6 @@ Page:
returns:
rootAXNode: AXNode?
pause:
pdf:
parameters:
scale: number?

View File

@ -336,12 +336,15 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
});
scheme.BrowserContextStorageStateParams = tOptional(tObject({}));
scheme.BrowserContextConsoleSupplementExposeParams = tOptional(tObject({}));
scheme.BrowserContextPauseParams = tOptional(tObject({}));
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
language: tString,
launchOptions: tAny,
contextOptions: tAny,
launchOptions: tOptional(tAny),
contextOptions: tOptional(tAny),
device: tOptional(tString),
saveStorage: tOptional(tString),
terminal: tOptional(tBoolean),
outputFile: tOptional(tString),
});
scheme.BrowserContextCrNewCDPSessionParams = tObject({
page: tChannel('Page'),
@ -447,7 +450,6 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
interestingOnly: tOptional(tBoolean),
root: tOptional(tChannel('ElementHandle')),
});
scheme.PagePauseParams = tOptional(tObject({}));
scheme.PagePdfParams = tObject({
scale: tOptional(tNumber),
displayHeaderFooter: tOptional(tBoolean),

View File

@ -93,6 +93,9 @@ export abstract class BrowserContext extends EventEmitter {
Close: 'close',
Page: 'page',
VideoStarted: 'videostarted',
BeforeClose: 'beforeclose',
StdOut: 'stdout',
StdErr: 'stderr',
};
readonly _timeoutSettings = new TimeoutSettings();
@ -280,6 +283,7 @@ export abstract class BrowserContext extends EventEmitter {
async close() {
if (this._closedStatus === 'open') {
this.emit(BrowserContext.Events.BeforeClose);
this._closedStatus = 'closing';
for (const listener of contextListeners)

View File

@ -32,10 +32,7 @@ type ContextData = {
contextPromise: Promise<dom.FrameExecutionContext>;
contextResolveCallback: (c: dom.FrameExecutionContext) => void;
context: dom.FrameExecutionContext | null;
rerunnableTasks: Set<{
rerun(context: dom.FrameExecutionContext): Promise<void>;
terminate(error: Error): void;
}>;
rerunnableTasks: Set<RerunnableTask>;
};
type DocumentInfo = {
@ -1049,24 +1046,6 @@ export class Frame extends EventEmitter {
this._parentFrame = null;
}
async evaluateSurvivingNavigations<T>(callback: (context: dom.FrameExecutionContext) => Promise<T>, world: types.World) {
return new Promise<T>((resolve, terminate) => {
const data = this._contextData.get(world)!;
const task = {
terminate,
async rerun(context: dom.FrameExecutionContext) {
try {
resolve(await callback(context));
data.rerunnableTasks.delete(task);
} catch (e) {}
}
};
data.rerunnableTasks.add(task);
if (data.context)
task.rerun(data.context);
});
}
private _scheduleRerunnableTask<T>(progress: Progress, world: types.World, task: dom.SchedulableTask<T>): Promise<T> {
const data = this._contextData.get(world)!;
const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */);

View File

@ -492,31 +492,6 @@ export class Page extends EventEmitter {
const identifier = PageBinding.identifier(name, world);
return this._pageBindings.get(identifier) || this._browserContext._pageBindings.get(identifier);
}
async pause() {
if (!this._browserContext._browser._options.headful)
throw new Error('Cannot pause in headless mode.');
await this.mainFrame().evaluateSurvivingNavigations(async context => {
await context.evaluateInternal(async () => {
const element = document.createElement('playwright-resume');
element.style.position = 'absolute';
element.style.top = '10px';
element.style.left = '10px';
element.style.zIndex = '2147483646';
element.style.opacity = '0.9';
element.setAttribute('role', 'button');
element.tabIndex = 0;
element.style.fontSize = '50px';
element.textContent = '▶️';
element.title = 'Resume script';
document.body.appendChild(element);
await new Promise(x => {
element.onclick = x;
});
element.remove();
});
}, 'utility');
}
}
export class Worker extends EventEmitter {

View File

@ -24,6 +24,9 @@ declare global {
playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
playwrightRecorderCommitAction: () => Promise<void>;
playwrightRecorderState: () => Promise<{ state: any, paused: boolean, tool: 'codegen' | 'pause' }>;
playwrightRecorderSetState: (state: any) => Promise<void>;
playwrightRecorderResume: () => Promise<boolean>;
}
}
@ -37,11 +40,19 @@ export class Recorder {
private _innerGlassPaneElement: HTMLElement;
private _highlightElements: HTMLElement[] = [];
private _tooltipElement: HTMLElement;
private _listeners: RegisteredListener[] = [];
private _listeners: (() => void)[] = [];
private _hoveredModel: HighlightModel | null = null;
private _hoveredElement: HTMLElement | null = null;
private _activeModel: HighlightModel | null = null;
private _expectProgrammaticKeyUp = false;
private _pollRecorderModeTimer: NodeJS.Timeout | undefined;
private _toolbarElement: HTMLElement;
private _inspectElement: HTMLElement;
private _recordElement: HTMLElement;
private _resumeElement: HTMLElement;
private _mode: 'inspecting' | 'recording' | 'none' = 'none';
private _tool: 'codegen' | 'pause' = 'pause';
private _paused = false;
constructor(injectedScript: InjectedScript) {
this._injectedScript = injectedScript;
@ -53,7 +64,7 @@ export class Recorder {
right: 0;
bottom: 0;
left: 0;
z-index: 2147483647;
z-index: 2147483646;
pointer-events: none;
display: flex;
">
@ -94,11 +105,108 @@ export class Recorder {
position: absolute;
top: 0;
}
</style>
</style>
`);
this._toolbarElement = html`
<x-pw-toolbar style="
position: fixed;
top: 100px;
left: 10px;
z-index: 2147483647;
background-color: #ffffffe6;
padding: 4px;
border-radius: 22px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 0.25em 0.5em;
flex-direction: column;"
></x-pw-toolbar>`;
this._inspectElement = html`
<x-pw-button tabIndex=0>
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 8c-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4-1.79-4-4-4zm8.94 3c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"/></svg>
</x-pw-button>`;
this._recordElement = html`
<x-pw-button class="record" tabIndex=0>
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M24 24H0V0h24v24z" fill="none"/><circle cx="12" cy="12" r="8"/></svg>
</x-pw-button>`;
this._resumeElement = html`
<x-pw-button tabIndex=0 class="playwright-resume">
<svg xmlns="http://www.w3.org/2000/svg" height="24" width="24"><path d="M0 0h24v24H0z" fill="none"/><path d="M8 5v14l11-7z"/></svg>
</x-pw-button>`;
this._populateToolbar();
this._pollRecorderMode();
setInterval(() => {
this._refreshListenersIfNeeded();
}, 100);
}, 500);
}
private _populateToolbar() {
const toolbarShadow = this._toolbarElement.attachShadow({ mode: 'open' });
toolbarShadow.appendChild(html`
<style>
x-pw-button {
width: 36px;
height: 36px;
background-position: center;
background-repeat: no-repeat;
border-radius: 16px;
cursor: pointer;
outline: none;
display: flex;
align-items: center;
justify-content: center;
margin-left: 2px;
fill: #333;
}
x-pw-button.logo {
cursor: inherit;
margin: 0;
}
x-pw-button.toggled {
fill: #468fd2;
}
x-pw-button:hover:not(.logo):not(.disabled) {
background-color: #f2f2f2;
}
x-pw-button.record.toggled {
fill: red;
}
x-pw-button.disabled {
fill: #777777 !important;
cursor: inherit;
}
x-pw-button.hidden {
display: none;
}
</style>`);
const iconElement = html`<x-pw-button class="logo" tabIndex=0 style="background-size: 32px 32px;"></x-pw-button>`;
iconElement.style.backgroundImage = `url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAwIiBoZWlnaHQ9IjQwMCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cGF0aCBkPSJNMTM2IDIyMmMtMTIgMy0yMSAxMC0yNiAxNiA1LTUgMTItOSAyMi0xMiAxMC0yIDE4LTIgMjUtMXYtNmMtNiAwLTEzIDAtMjEgM3ptLTI3LTQ2bC00OCAxMiAzIDMgNDAtMTBzMCA3LTUgMTRjOS03IDEwLTE5IDEwLTE5em00MCAxMTJDODIgMzA2IDQ2IDIyOCAzNSAxODhhMjI3IDIyNyAwIDAxLTctNDVjLTQgMS02IDItNSA4IDAgOSAyIDIzIDcgNDIgMTEgNDAgNDcgMTE4IDExNCAxMDAgMTUtNCAyNi0xMSAzNC0yMC03IDctMTcgMTItMjkgMTV6bTEzLTE2MHY1aDI2bC0yLTVoLTI0eiIgZmlsbD0iIzJENDU1MiIvPjxwYXRoIGQ9Ik0xOTQgMTY4YzEyIDMgMTggMTEgMjEgMTlsMTQgM3MtMi0yNS0yNS0zMmMtMjItNi0zNiAxMi0zNyAxNCA2LTQgMTUtOCAyNy00em0xMDUgMTljLTIxLTYtMzUgMTItMzYgMTQgNi00IDE1LTggMjctNSAxMiA0IDE4IDEyIDIxIDE5bDE0IDRzLTItMjYtMjYtMzJ6bS0xMyA2OGwtMTEwLTMxczEgNiA2IDE0bDkzIDI2IDExLTl6bS03NiA2NmMtODctMjMtNzctMTM0LTYzLTE4NyA2LTIyIDEyLTM4IDE3LTQ5LTMgMC01IDEtOCA2LTUgMTEtMTIgMjgtMTggNTItMTQgNTMtMjUgMTY0IDYyIDE4OCA0MSAxMSA3My02IDk3LTMyYTkwIDkwIDAgMDEtODcgMjJ6IiBmaWxsPSIjMkQ0NTUyIi8+PHBhdGggZD0iTTE2MiAyNjJ2LTIybC02MyAxOHM1LTI3IDM3LTM2YzEwLTMgMTktMyAyNi0ydi05MmgzMWwtMTAtMjRjLTQtOS05LTMtMTkgNi04IDYtMjcgMTktNTUgMjctMjkgOC01MiA2LTYxIDQtMTQtMi0yMS01LTIwIDUgMCA5IDIgMjMgNyA0MiAxMSA0MCA0NyAxMTggMTE0IDEwMCAxOC00IDMwLTE0IDM5LTI2aC0yNnpNNjEgMTg4bDQ4LTEycy0xIDE4LTE5IDIzLTI5LTExLTI5LTExeiIgZmlsbD0iI0UyNTc0QyIvPjxwYXRoIGQ9Ik0zNDIgMTI5Yy0xMyAyLTQzIDUtNzktNS0zNy0xMC02Mi0yNy03MS0zNS0xNC0xMi0yMC0yMC0yNi04LTUgMTEtMTIgMjktMTkgNTMtMTQgNTMtMjQgMTY0IDYzIDE4N3MxMzQtNzggMTQ4LTEzMWM2LTI0IDktNDIgMTAtNTQgMS0xNC05LTEwLTI2LTd6bS0xNzYgNDRzMTQtMjIgMzgtMTVjMjMgNyAyNSAzMiAyNSAzMmwtNjMtMTd6bTU3IDk2Yy00MS0xMi00Ny00NS00Ny00NWwxMTAgMzFzLTIyIDI2LTYzIDE0em0zOS02OHMxNC0yMSAzNy0xNGMyNCA2IDI2IDMyIDI2IDMybC02My0xOHoiIGZpbGw9IiMyRUFEMzMiLz48cGF0aCBkPSJNMTQwIDI0NmwtNDEgMTJzNS0yNiAzNS0zNmwtMjMtODYtMiAxYy0yOSA4LTUyIDYtNjEgNC0xNC0yLTIxLTUtMjAgNSAwIDkgMiAyMyA3IDQyIDExIDQwIDQ3IDExOCAxMTQgMTAwaDJsLTExLTQyem0tNzktNThsNDgtMTJzLTEgMTgtMTkgMjMtMjktMTEtMjktMTF6IiBmaWxsPSIjRDY1MzQ4Ii8+PHBhdGggZD0iTTIyNSAyNjloLTJjLTQxLTEyLTQ3LTQ1LTQ3LTQ1bDU3IDE2IDMwLTExNmMtMzctMTAtNjItMjctNzEtMzUtMTQtMTItMjAtMjAtMjYtOC01IDExLTEyIDI5LTE5IDUzLTE0IDUzLTI0IDE2NCA2MyAxODdsMiAxIDEzLTUzem0tNTktOTZzMTQtMjIgMzgtMTVjMjMgNyAyNSAzMiAyNSAzMmwtNjMtMTd6IiBmaWxsPSIjMUQ4RDIyIi8+PHBhdGggZD0iTTE0MiAyNDVsLTExIDRjMyAxNCA3IDI4IDE0IDQwbDQtMSA5LTNjLTgtMTItMTMtMjUtMTYtNDB6bS00LTEwMmMtNiAyMS0xMSA1MS0xMCA4MWw4LTIgMi0xYTI3MyAyNzMgMCAwMTE0LTEwM2wtOCA1LTYgMjB6IiBmaWxsPSIjQzA0QjQxIi8+PC9zdmc+')`;
toolbarShadow.appendChild(iconElement);
toolbarShadow.appendChild(this._inspectElement);
toolbarShadow.appendChild(this._recordElement);
toolbarShadow.appendChild(this._resumeElement);
this._inspectElement.addEventListener('click', () => {
if (this._inspectElement.classList.contains('disabled'))
return;
this._inspectElement.classList.toggle('toggled');
this._setMode(this._inspectElement.classList.contains('toggled') ? 'inspecting' : 'none');
});
this._recordElement.addEventListener('click', () => {
if (this._recordElement.classList.contains('disabled'))
return;
this._recordElement.classList.toggle('toggled');
this._setMode(this._recordElement.classList.contains('toggled') ? 'recording' : 'none');
});
this._resumeElement.addEventListener('click', () => {
if (!this._resumeElement.classList.contains('disabled')) {
this._setMode('none');
window.playwrightRecorderResume().catch(e => {});
}
});
}
private _refreshListenersIfNeeded() {
@ -122,10 +230,47 @@ export class Recorder {
}, true),
];
document.documentElement.appendChild(this._outerGlassPaneElement);
document.documentElement.appendChild(this._toolbarElement);
if ((window as any)._recorderScriptReadyForTest)
(window as any)._recorderScriptReadyForTest();
}
private async _setMode(mode: 'inspecting' | 'recording' | 'paused' | 'none') {
window.playwrightRecorderSetState({ mode }).then(() => this._pollRecorderMode());
}
private async _pollRecorderMode() {
if (this._pollRecorderModeTimer)
clearTimeout(this._pollRecorderModeTimer);
const result = await window.playwrightRecorderState().catch(e => null);
if (result) {
const { state, paused, tool } = result;
if (state && state.mode !== this._mode) {
this._mode = state.mode as any;
this._inspectElement.classList.toggle('toggled', this._mode === 'inspecting');
this._recordElement.classList.toggle('toggled', this._mode === 'recording');
this._inspectElement.classList.toggle('disabled', this._mode === 'recording');
this._resumeElement.classList.toggle('disabled', this._mode === 'recording');
this._clearHighlight();
}
if (paused !== this._paused) {
this._paused = paused;
this._resumeElement.classList.toggle('disabled', !this._paused);
}
if (tool !== this._tool) {
this._tool = tool;
this._resumeElement.classList.toggle('hidden', this._tool !== 'pause');
}
}
this._pollRecorderModeTimer = setTimeout(() => this._pollRecorderMode(), 250);
}
private _clearHighlight() {
this._hoveredModel = null;
this._activeModel = null;
this._updateHighlight();
}
private _actionInProgress(event: Event): boolean {
// If Playwright is performing action for us, bail.
if (this._performingAction)
@ -143,7 +288,7 @@ export class Recorder {
}
private _consumedDueWrongTarget(event: Event): boolean {
if (this._activeModel && this._activeModel.elements[0] === deepEventTarget(event))
if (this._activeModel && this._activeModel.elements[0] === this._deepEventTarget(event))
return false;
consumeEvent(event);
return true;
@ -157,7 +302,7 @@ export class Recorder {
if (this._consumedDueToNoModel(event, this._hoveredModel))
return;
const checkbox = asCheckbox(deepEventTarget(event));
const checkbox = asCheckbox(this._deepEventTarget(event));
if (checkbox) {
// Interestingly, inputElement.checked is reversed inside this event handler.
this._performAction({
@ -178,8 +323,20 @@ export class Recorder {
});
}
private _isInToolbar(element: Element | undefined | null): boolean {
return !!element && element.nodeName.toLowerCase().startsWith('x-pw-');
}
private _shouldIgnoreMouseEvent(event: MouseEvent): boolean {
const target = deepEventTarget(event);
const target = this._deepEventTarget(event);
if (this._isInToolbar(target))
return true;
if (this._mode === 'none')
return true;
if (this._mode === 'inspecting') {
consumeEvent(event);
return true;
}
const nodeName = target.nodeName;
if (nodeName === 'SELECT')
return true;
@ -204,7 +361,11 @@ export class Recorder {
}
private _onMouseMove(event: MouseEvent) {
const target = deepEventTarget(event);
if (this._mode === 'none')
return;
const target = this._deepEventTarget(event);
if (this._isInToolbar(target))
return;
if (this._hoveredElement === target)
return;
this._hoveredElement = target;
@ -214,14 +375,14 @@ export class Recorder {
private _onMouseLeave(event: MouseEvent) {
// Leaving iframe.
if (deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
if (this._deepEventTarget(event).nodeType === Node.DOCUMENT_NODE) {
this._hoveredElement = null;
this._commitActionAndUpdateModelForHoveredElement();
}
}
private _onFocus() {
const activeElement = deepActiveElement(document);
const activeElement = this._deepActiveElement(document);
const result = activeElement ? generateSelector(this._injectedScript, activeElement) : null;
this._activeModel = result && result.selector ? result : null;
if ((window as any)._highlightUpdatedForTest)
@ -290,7 +451,7 @@ export class Recorder {
this._highlightElements = [];
for (const box of boxes) {
const highlightElement = pool.length ? pool.shift()! : this._createHighlightElement();
highlightElement.style.borderColor = this._highlightElements.length ? 'hotpink' : '#8929ff';
highlightElement.style.backgroundColor = this._highlightElements.length ? '#f6b26b7f' : '#6fa8dc7f';
highlightElement.style.left = box.x + 'px';
highlightElement.style.top = box.y + 'px';
highlightElement.style.width = box.width + 'px';
@ -313,7 +474,6 @@ export class Recorder {
left: 0;
width: 0;
height: 0;
border: 1px solid;
box-sizing: border-box;">
</x-pw-highlight>`;
this._glassPaneShadow.appendChild(highlightElement);
@ -321,7 +481,9 @@ export class Recorder {
}
private _onInput(event: Event) {
const target = deepEventTarget(event);
if (this._mode !== 'recording')
return true;
const target = this._deepEventTarget(event);
if (['INPUT', 'TEXTAREA'].includes(target.nodeName)) {
const inputElement = target as HTMLInputElement;
const elementType = (inputElement.type || '').toLowerCase();
@ -385,11 +547,17 @@ export class Recorder {
return false;
const hasModifier = event.ctrlKey || event.altKey || event.metaKey;
if (event.key.length === 1 && !hasModifier)
return !!asCheckbox(deepEventTarget(event));
return !!asCheckbox(this._deepEventTarget(event));
return true;
}
private _onKeyDown(event: KeyboardEvent) {
if (this._mode === 'inspecting') {
consumeEvent(event);
return;
}
if (this._mode !== 'recording')
return true;
if (!this._shouldGenerateKeyPressFor(event))
return;
if (this._actionInProgress(event)) {
@ -400,7 +568,7 @@ export class Recorder {
return;
// Similarly to click, trigger checkbox on key event, not input.
if (event.key === ' ') {
const checkbox = asCheckbox(deepEventTarget(event));
const checkbox = asCheckbox(this._deepEventTarget(event));
if (checkbox) {
this._performAction({
name: checkbox.checked ? 'uncheck' : 'check',
@ -449,17 +617,17 @@ export class Recorder {
});
}
}
}
function deepEventTarget(event: Event): HTMLElement {
return event.composedPath()[0] as HTMLElement;
}
private _deepEventTarget(event: Event): HTMLElement {
return event.composedPath()[0] as HTMLElement;
}
function deepActiveElement(document: Document): Element | null {
let activeElement = document.activeElement;
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
activeElement = activeElement.shadowRoot.activeElement;
return activeElement;
private _deepActiveElement(document: Document): Element | null {
let activeElement = document.activeElement;
while (activeElement && activeElement.shadowRoot && activeElement.shadowRoot.activeElement)
activeElement = activeElement.shadowRoot.activeElement;
return activeElement;
}
}
function modifiersForEvent(event: MouseEvent | KeyboardEvent): number {
@ -500,14 +668,17 @@ type RegisteredListener = {
useCapture?: boolean;
};
function addEventListener(target: EventTarget, eventName: string, listener: EventListener, useCapture?: boolean): RegisteredListener {
function addEventListener(target: EventTarget, eventName: string, listener: EventListener, useCapture?: boolean): () => void {
target.addEventListener(eventName, listener, useCapture);
return { target, eventName, listener, useCapture };
const remove = () => {
target.removeEventListener(eventName, listener, useCapture);
};
return remove;
}
function removeEventListeners(listeners: RegisteredListener[]) {
function removeEventListeners(listeners: (() => void)[]) {
for (const listener of listeners)
listener.target.removeEventListener(listener.eventName, listener.listener, listener.useCapture);
listener();
listeners.splice(0, listeners.length);
}

View File

@ -37,17 +37,19 @@ export class CodeGenerator {
private _lastActionText: string | undefined;
private _languageGenerator: LanguageGenerator;
private _output: CodeGeneratorOutput;
private _footerText: string;
private _footerText = '';
constructor(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) {
constructor(browserName: string, generateHeaders: boolean, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, output: CodeGeneratorOutput, languageGenerator: LanguageGenerator, deviceName: string | undefined, saveStorage: string | undefined) {
this._output = output;
this._languageGenerator = languageGenerator;
launchOptions = { headless: false, ...launchOptions };
const header = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
this._output.printLn(header);
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
this._output.printLn(this._footerText);
if (generateHeaders) {
const header = this._languageGenerator.generateHeader(browserName, launchOptions, contextOptions, deviceName);
this._output.printLn(header);
this._footerText = '\n' + this._languageGenerator.generateFooter(saveStorage);
this._output.printLn(this._footerText);
}
}
addAction(action: ActionInContext) {
@ -94,7 +96,8 @@ export class CodeGenerator {
}
_printAction(actionInContext: ActionInContext, eraseLastAction: boolean) {
this._output.popLn(this._footerText);
if (this._footerText)
this._output.popLn(this._footerText);
if (eraseLastAction && this._lastActionText)
this._output.popLn(this._lastActionText);
const performingAction = !!this._currentAction;
@ -102,7 +105,8 @@ export class CodeGenerator {
this._lastAction = actionInContext;
this._lastActionText = this._languageGenerator.generateAction(actionInContext, performingAction);
this._output.printLn(this._lastActionText);
this._output.printLn(this._footerText);
if (this._footerText)
this._output.printLn(this._footerText);
}
signal(pageAlias: string, frame: Frame, signal: Signal) {

View File

@ -16,27 +16,46 @@
import * as fs from 'fs';
import * as querystring from 'querystring';
import { Writable } from 'stream';
import * as hljs from '../../third_party/highlightjs/highlightjs';
import { RecorderOutput } from './recorderSupplement';
import * as hljs from '../../../third_party/highlightjs/highlightjs';
export interface RecorderOutput {
printLn(text: string): void;
popLn(text: string): void;
flush(): void;
}
export interface Writable {
write(data: string): void;
}
export class OutputMultiplexer implements RecorderOutput {
private _outputs: RecorderOutput[]
private _enabled = true;
constructor(outputs: RecorderOutput[]) {
this._outputs = outputs;
}
setEnabled(enabled: boolean) {
this._enabled = enabled;
}
printLn(text: string) {
if (!this._enabled)
return;
for (const output of this._outputs)
output.printLn(text);
}
popLn(text: string) {
if (!this._enabled)
return;
for (const output of this._outputs)
output.popLn(text);
}
flush() {
if (!this._enabled)
return;
for (const output of this._outputs)
output.flush();
}
@ -64,6 +83,7 @@ export class FileOutput extends BufferOutput implements RecorderOutput {
constructor(fileName: string) {
super();
this._fileName = fileName;
process.on('exit', () => this.flush());
}
flush() {
@ -72,7 +92,7 @@ export class FileOutput extends BufferOutput implements RecorderOutput {
}
export class TerminalOutput implements RecorderOutput {
private _output: Writable
private _output: Writable;
private _language: string;
static create(output: Writable, language: string) {

View File

@ -15,7 +15,8 @@
*/
import * as actions from './recorder/recorderActions';
import { CodeGenerator, ActionInContext, CodeGeneratorOutput } from './recorder/codeGenerator';
import type * as channels from '../../protocol/channels';
import { CodeGenerator, ActionInContext } from './recorder/codeGenerator';
import { toClickOptions, toModifiers } from './recorder/utils';
import { Page } from '../page';
import { Frame } from '../frames';
@ -26,8 +27,15 @@ import { CSharpLanguageGenerator } from './recorder/csharp';
import { PythonLanguageGenerator } from './recorder/python';
import { ProgressController } from '../progress';
import * as recorderSource from '../../generated/recorderSource';
import * as consoleApiSource from '../../generated/consoleApiSource';
import { FileOutput, FlushingTerminalOutput, OutputMultiplexer, RecorderOutput, TerminalOutput, Writable } from './recorder/outputs';
type BindingSource = { frame: Frame, page: Page };
type Tool = 'codegen' | 'pause';
type Mode = 'inspecting' | 'recording' | 'none';
const symbol = Symbol('RecorderSupplement');
export class RecorderSupplement {
private _generator: CodeGenerator;
@ -36,11 +44,27 @@ export class RecorderSupplement {
private _lastDialogOrdinal = 0;
private _timers = new Set<NodeJS.Timeout>();
private _context: BrowserContext;
private _resumeCallback: (() => void) | null = null;
private _recorderState: { mode: Mode };
private _paused = false;
private _tool: Tool;
private _output: OutputMultiplexer;
constructor(context: BrowserContext, params: { language: string, launchOptions: any, contextOptions: any, device?: string, saveStorage?: string}, output: CodeGeneratorOutput) {
static getOrCreate(context: BrowserContext, tool: Tool, params: channels.BrowserContextRecorderSupplementEnableParams): Promise<RecorderSupplement> {
let recorderPromise = (context as any)[symbol] as Promise<RecorderSupplement>;
if (!recorderPromise) {
const recorder = new RecorderSupplement(context, tool, params);
recorderPromise = recorder.install().then(() => recorder);
(context as any)[symbol] = recorderPromise;
}
return recorderPromise;
}
constructor(context: BrowserContext, tool: Tool, params: channels.BrowserContextRecorderSupplementEnableParams) {
this._context = context;
this._tool = tool;
this._recorderState = { mode: tool === 'codegen' ? 'recording' : 'none' };
let languageGenerator: LanguageGenerator;
switch (params.language) {
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
@ -48,7 +72,21 @@ export class RecorderSupplement {
case 'python-async': languageGenerator = new PythonLanguageGenerator(params.language === 'python-async'); break;
default: throw new Error(`Invalid target: '${params.language}'`);
}
const generator = new CodeGenerator(context._browser._options.name, params.launchOptions, params.contextOptions, output, languageGenerator, params.device, params.saveStorage);
let highlighterType = params.language;
if (highlighterType === 'python-async')
highlighterType = 'python';
const writable: Writable = {
write: (text: string) => context.emit(BrowserContext.Events.StdOut, text)
};
const outputs: RecorderOutput[] = [params.terminal ? new TerminalOutput(writable, highlighterType) : new FlushingTerminalOutput(writable)];
if (params.outputFile)
outputs.push(new FileOutput(params.outputFile));
this._output = new OutputMultiplexer(outputs);
this._output.setEnabled(tool === 'codegen');
context.on(BrowserContext.Events.BeforeClose, () => this._output.flush());
const generator = new CodeGenerator(context._browser._options.name, tool === 'codegen', params.launchOptions || {}, params.contextOptions || {}, this._output, languageGenerator, params.device, params.saveStorage);
this._generator = generator;
}
@ -76,7 +114,34 @@ export class RecorderSupplement {
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
await this._context.exposeBinding('playwrightRecorderState', false, () => {
return {
state: this._recorderState,
tool: this._tool,
paused: this._paused
};
});
await this._context.exposeBinding('playwrightRecorderSetState', false, (source, state) => {
this._recorderState = state;
this._output.setEnabled(state.mode === 'recording');
});
await this._context.exposeBinding('playwrightRecorderResume', false, () => {
if (this._resumeCallback) {
this._resumeCallback();
this._resumeCallback = null;
}
this._paused = false;
});
await this._context.extendInjectedScript(recorderSource.source);
await this._context.extendInjectedScript(consoleApiSource.source);
}
async pause() {
this._paused = true;
return new Promise(f => this._resumeCallback = f);
}
private async _onPage(page: Page) {

View File

@ -15,14 +15,11 @@
*/
import * as http from 'http';
import { Writable } from 'stream';
import * as path from 'path';
import { ChildProcess, spawn } from 'child_process';
import { folio as baseFolio } from '../fixtures';
import type { Page, BrowserType, Browser, BrowserContext } from '../..';
export { config } from 'folio';
import { FlushingTerminalOutput } from '../../lib/client/supplements/recorderOutputs';
import { RecorderSupplement } from '../../lib/client/supplements/recorderSupplement';
type WorkerFixtures = {
browserType: BrowserType<Browser>;
@ -41,8 +38,8 @@ export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
fixtures.contextWrapper.init(async ({ browser }, runTest) => {
const context = await browser.newContext() as BrowserContext;
const outputBuffer = new WritableBuffer();
const output = new FlushingTerminalOutput(outputBuffer as any as Writable);
new RecorderSupplement(context, 'javascript', {}, {}, undefined, undefined, output);
(context as any)._stdout = outputBuffer;
await (context as any)._enableRecorder('javascript');
await runTest({ context, output: outputBuffer });
await context.close();
});
@ -88,14 +85,10 @@ class WritableBuffer {
this._data = '';
}
write(chunk: string) {
if (!chunk)
write(data: Buffer) {
if (!data)
return;
if (chunk === '\u001B[F\u001B[2K') {
const index = this._data.lastIndexOf('\n');
this._data = this._data.substring(0, index);
return;
}
const chunk = data.toString('utf8');
this._data += chunk;
if (this._callback && chunk.includes(this._text))
this._callback();

View File

@ -27,7 +27,7 @@ it('should pause and resume the script', async ({page}) => {
const resumePromise = (page as any)._pause().then(() => resolved = true);
await new Promise(x => setTimeout(x, 0));
expect(resolved).toBe(false);
await page.click('playwright-resume');
await page.click('.playwright-resume');
await resumePromise;
expect(resolved).toBe(true);
});
@ -38,7 +38,7 @@ it('should pause through a navigation', async ({page, server}) => {
await new Promise(x => setTimeout(x, 0));
expect(resolved).toBe(false);
await page.goto(server.EMPTY_PAGE);
await page.click('playwright-resume');
await page.click('.playwright-resume');
await resumePromise;
expect(resolved).toBe(true);
});
@ -50,7 +50,7 @@ it('should pause after a navigation', async ({page, server}) => {
const resumePromise = (page as any)._pause().then(() => resolved = true);
await new Promise(x => setTimeout(x, 0));
expect(resolved).toBe(false);
await page.click('playwright-resume');
await page.click('.playwright-resume');
await resumePromise;
expect(resolved).toBe(true);
});

View File

@ -16,11 +16,10 @@
import { folio } from './fixtures';
import type { Page, Frame } from '..';
import { ConsoleApiSupplement } from '../lib/client/supplements/consoleApiSupplement';
const fixtures = folio.extend();
fixtures.context.override(async ({ context }, run) => {
new ConsoleApiSupplement(context);
await (context as any)._enableConsoleApi();
await run(context);
});
const { describe, it, expect } = fixtures.build();

View File

@ -126,6 +126,7 @@ DEPS['src/server/'] = [
'src/server/common/**',
'src/server/injected/**',
'src/server/supplements/**',
'src/protocol/**',
];
// No dependencies for code shared between node and page.
@ -133,8 +134,6 @@ DEPS['src/server/common/'] = [];
// Strict dependencies for injected code.
DEPS['src/server/injected/'] = ['src/server/common/'];
DEPS['src/client/supplements/'] = ['src/client/'];
// Electron and Clank use chromium internally.
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
DEPS['src/server/electron/'] = [...DEPS['src/server/'], 'src/server/chromium/'];