diff --git a/src/cli/cli.ts b/src/cli/cli.ts index c8e71956ef370..ee5fd6f718607 100755 --- a/src/cli/cli.ts +++ b/src/cli/cli.ts @@ -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())); diff --git a/src/client/browserContext.ts b/src/client/browserContext.ts index 26084e21e5678..efd8175206a31 100644 --- a/src/client/browserContext.ts +++ b/src/client/browserContext.ts @@ -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; _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 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 { + 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 { diff --git a/src/client/page.ts b/src/client/page.ts index e841b9b4260f4..6530d9ec28620 100644 --- a/src/client/page.ts +++ b/src/client/page.ts @@ -640,9 +640,7 @@ export class Page extends ChannelOwner { - await this._channel.pause(); - }); + await this.context()._pause(); } async _pdf(options: PDFOptions = {}): Promise { diff --git a/src/client/supplements/consoleApiSupplement.ts b/src/client/supplements/consoleApiSupplement.ts deleted file mode 100644 index 33599a78d09fe..0000000000000 --- a/src/client/supplements/consoleApiSupplement.ts +++ /dev/null @@ -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 => {}); - } -} diff --git a/src/client/supplements/recorderSupplement.ts b/src/client/supplements/recorderSupplement.ts deleted file mode 100644 index b875c5d1b7d8f..0000000000000 --- a/src/client/supplements/recorderSupplement.ts +++ /dev/null @@ -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; -} diff --git a/src/dispatchers/browserContextDispatcher.ts b/src/dispatchers/browserContextDispatcher.ts index 10b7e00a72a6e..1c00c0b8c7a13 100644 --- a/src/dispatchers/browserContextDispatcher.ts +++ b/src/dispatchers/browserContextDispatcher.ts @@ -38,6 +38,8 @@ export class BrowserContextDispatcher extends Dispatcher 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 { - 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 { diff --git a/src/dispatchers/pageDispatcher.ts b/src/dispatchers/pageDispatcher.ts index 2f43389205674..0cfbc0851ef90 100644 --- a/src/dispatchers/pageDispatcher.ts +++ b/src/dispatchers/pageDispatcher.ts @@ -237,10 +237,6 @@ export class PageDispatcher extends Dispatcher i return { entries: await coverage.stopCSSCoverage() }; } - async pause() { - await this._page.pause(); - } - _onFrameAttached(frame: Frame) { this._dispatchEvent('frameAttached', { frame: FrameDispatcher.from(this._scope, frame) }); } diff --git a/src/protocol/channels.ts b/src/protocol/channels.ts index e0e20aea8e1b4..fb597d57eba55 100644 --- a/src/protocol/channels.ts +++ b/src/protocol/channels.ts @@ -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; addInitScript(params: BrowserContextAddInitScriptParams, metadata?: Metadata): Promise; clearCookies(params?: BrowserContextClearCookiesParams, metadata?: Metadata): Promise; @@ -555,6 +555,7 @@ export interface BrowserContextChannel extends Channel { setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise; storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise; consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise; + pause(params?: BrowserContextPauseParams, metadata?: Metadata): Promise; recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise; crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise; } @@ -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; touchscreenTap(params: PageTouchscreenTapParams, metadata?: Metadata): Promise; accessibilitySnapshot(params: PageAccessibilitySnapshotParams, metadata?: Metadata): Promise; - pause(params?: PagePauseParams, metadata?: Metadata): Promise; pdf(params: PagePdfParams, metadata?: Metadata): Promise; crStartJSCoverage(params: PageCrStartJSCoverageParams, metadata?: Metadata): Promise; crStopJSCoverage(params?: PageCrStopJSCoverageParams, metadata?: Metadata): Promise; @@ -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, diff --git a/src/protocol/protocol.yml b/src/protocol/protocol.yml index 8bdaf0900fa34..0ed6b0e1f17ce 100644 --- a/src/protocol/protocol.yml +++ b/src/protocol/protocol.yml @@ -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,21 +639,21 @@ BrowserContext: route: Route request: Request - crBackgroundPage: + stdout: parameters: - page: Page + data: binary - crServiceWorker: + stderr: parameters: - worker: Worker + data: binary - recorderSupplementPrintLn: + crBackgroundPage: parameters: - text: string + page: Page - recorderSupplementPopLn: + crServiceWorker: parameters: - text: string + worker: Worker Page: type: interface @@ -854,8 +859,6 @@ Page: returns: rootAXNode: AXNode? - pause: - pdf: parameters: scale: number? diff --git a/src/protocol/validator.ts b/src/protocol/validator.ts index 0548ef0ffc20e..3759bcb5226ca 100644 --- a/src/protocol/validator.ts +++ b/src/protocol/validator.ts @@ -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), diff --git a/src/server/browserContext.ts b/src/server/browserContext.ts index 7709118863dd3..6e12d2dfab3c6 100644 --- a/src/server/browserContext.ts +++ b/src/server/browserContext.ts @@ -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) diff --git a/src/server/frames.ts b/src/server/frames.ts index 94871b8814b49..b522ade64b45a 100644 --- a/src/server/frames.ts +++ b/src/server/frames.ts @@ -32,10 +32,7 @@ type ContextData = { contextPromise: Promise; contextResolveCallback: (c: dom.FrameExecutionContext) => void; context: dom.FrameExecutionContext | null; - rerunnableTasks: Set<{ - rerun(context: dom.FrameExecutionContext): Promise; - terminate(error: Error): void; - }>; + rerunnableTasks: Set; }; type DocumentInfo = { @@ -1049,24 +1046,6 @@ export class Frame extends EventEmitter { this._parentFrame = null; } - async evaluateSurvivingNavigations(callback: (context: dom.FrameExecutionContext) => Promise, world: types.World) { - return new Promise((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(progress: Progress, world: types.World, task: dom.SchedulableTask): Promise { const data = this._contextData.get(world)!; const rerunnableTask = new RerunnableTask(data, progress, task, true /* returnByValue */); diff --git a/src/server/page.ts b/src/server/page.ts index 0a7c190bbfbe2..5296e35d62360 100644 --- a/src/server/page.ts +++ b/src/server/page.ts @@ -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 { diff --git a/src/server/supplements/injected/recorder.ts b/src/server/supplements/injected/recorder.ts index 99715811a9e33..769340cb4eb6a 100644 --- a/src/server/supplements/injected/recorder.ts +++ b/src/server/supplements/injected/recorder.ts @@ -24,6 +24,9 @@ declare global { playwrightRecorderPerformAction: (action: actions.Action) => Promise; playwrightRecorderRecordAction: (action: actions.Action) => Promise; playwrightRecorderCommitAction: () => Promise; + playwrightRecorderState: () => Promise<{ state: any, paused: boolean, tool: 'codegen' | 'pause' }>; + playwrightRecorderSetState: (state: any) => Promise; + playwrightRecorderResume: () => Promise; } } @@ -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; } - + `); + + this._toolbarElement = html` + `; + + this._inspectElement = html` + + + `; + this._recordElement = html` + + + `; + this._resumeElement = html` + + + `; + + this._populateToolbar(); + this._pollRecorderMode(); + setInterval(() => { this._refreshListenersIfNeeded(); - }, 100); + }, 500); + } + + private _populateToolbar() { + const toolbarShadow = this._toolbarElement.attachShadow({ mode: 'open' }); + toolbarShadow.appendChild(html` + `); + + const iconElement = html``; + 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;"> `; 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); } diff --git a/src/server/supplements/recorder/codeGenerator.ts b/src/server/supplements/recorder/codeGenerator.ts index 63632d8db177f..42a416b3b2430 100644 --- a/src/server/supplements/recorder/codeGenerator.ts +++ b/src/server/supplements/recorder/codeGenerator.ts @@ -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) { diff --git a/src/client/supplements/recorderOutputs.ts b/src/server/supplements/recorder/outputs.ts similarity index 89% rename from src/client/supplements/recorderOutputs.ts rename to src/server/supplements/recorder/outputs.ts index fc1d6279866b8..4052a5d6ebc37 100644 --- a/src/client/supplements/recorderOutputs.ts +++ b/src/server/supplements/recorder/outputs.ts @@ -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) { diff --git a/src/server/supplements/recorderSupplement.ts b/src/server/supplements/recorderSupplement.ts index 2a56ae335e866..e2deaad6e8789 100644 --- a/src/server/supplements/recorderSupplement.ts +++ b/src/server/supplements/recorderSupplement.ts @@ -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(); private _context: BrowserContext; + private _resumeCallback: (() => void) | null = null; + private _recorderState: { mode: Mode }; + private _paused = false; + private _tool: Tool; + private _output: OutputMultiplexer; + + static getOrCreate(context: BrowserContext, tool: Tool, params: channels.BrowserContextRecorderSupplementEnableParams): Promise { + let recorderPromise = (context as any)[symbol] as Promise; + if (!recorderPromise) { + const recorder = new RecorderSupplement(context, tool, params); + recorderPromise = recorder.install().then(() => recorder); + (context as any)[symbol] = recorderPromise; + } + return recorderPromise; + } - constructor(context: BrowserContext, params: { language: string, launchOptions: any, contextOptions: any, device?: string, saveStorage?: string}, output: CodeGeneratorOutput) { + 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) { diff --git a/test/cli/cli.fixtures.ts b/test/cli/cli.fixtures.ts index 5a0d441c13f7f..206fd193e8136 100644 --- a/test/cli/cli.fixtures.ts +++ b/test/cli/cli.fixtures.ts @@ -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; @@ -41,8 +38,8 @@ export const fixtures = baseFolio.extend(); 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(); diff --git a/test/pause.spec.ts b/test/pause.spec.ts index 2b041e5f816f3..7d4da352fabf0 100644 --- a/test/pause.spec.ts +++ b/test/pause.spec.ts @@ -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); }); diff --git a/test/selector-generator.spec.ts b/test/selector-generator.spec.ts index b7aa06b638916..40ab8d5be85d3 100644 --- a/test/selector-generator.spec.ts +++ b/test/selector-generator.spec.ts @@ -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(); diff --git a/utils/check_deps.js b/utils/check_deps.js index 34847dbf0ff7b..52836d051aaea 100644 --- a/utils/check_deps.js +++ b/utils/check_deps.js @@ -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/'];