mirror of
https://github.com/microsoft/playwright.git
synced 2024-10-27 21:58:52 +03:00
chore: make recorder a supplement (#5131)
This commit is contained in:
parent
be9bef513e
commit
464fdc1800
@ -22,16 +22,17 @@ import * as path from 'path';
|
||||
import * as program from 'commander';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import { OutputMultiplexer, TerminalOutput, FileOutput } from './codegen/outputs';
|
||||
import { CodeGenerator, CodeGeneratorOutput } from './codegen/codeGenerator';
|
||||
import { JavaScriptLanguageGenerator, LanguageGenerator } from './codegen/languages';
|
||||
import { PythonLanguageGenerator } from './codegen/languages/python';
|
||||
import { CSharpLanguageGenerator } from './codegen/languages/csharp';
|
||||
import { RecorderController } from './codegen/recorderController';
|
||||
import { runServer, printApiJson, installBrowsers } from './driver';
|
||||
import { showTraceViewer } from './traceViewer/traceViewer';
|
||||
import type { Browser, BrowserContext, Page, BrowserType, BrowserContextOptions, LaunchOptions } from '../..';
|
||||
import * as playwright from '../..';
|
||||
import { BrowserContext } from '../client/browserContext';
|
||||
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)
|
||||
@ -317,36 +318,31 @@ async function openPage(context: BrowserContext, url: string | undefined): Promi
|
||||
|
||||
async function open(options: Options, url: string | undefined) {
|
||||
const { context } = await launchContext(options, false);
|
||||
(context as any)._exposeConsoleApi();
|
||||
new ConsoleApiSupplement(context);
|
||||
await openPage(context, url);
|
||||
if (process.env.PWCLI_EXIT_FOR_TEST)
|
||||
await Promise.all(context.pages().map(p => p.close()));
|
||||
}
|
||||
|
||||
async function codegen(options: Options, url: string | undefined, target: string, outputFile?: string) {
|
||||
let languageGenerator: LanguageGenerator;
|
||||
|
||||
switch (target) {
|
||||
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
|
||||
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
|
||||
case 'python':
|
||||
case 'python-async': languageGenerator = new PythonLanguageGenerator(target === 'python-async'); break;
|
||||
default: throw new Error(`Invalid target: '${target}'`);
|
||||
}
|
||||
|
||||
const { context, browserName, launchOptions, contextOptions } = await launchContext(options, false);
|
||||
|
||||
if (process.env.PWTRACE)
|
||||
(contextOptions as any)._traceDir = path.join(process.cwd(), '.trace');
|
||||
|
||||
const outputs: CodeGeneratorOutput[] = [TerminalOutput.create(process.stdout, languageGenerator.highlighterType())];
|
||||
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);
|
||||
|
||||
const generator = new CodeGenerator(browserName, launchOptions, contextOptions, output, languageGenerator, options.device, options.saveStorage);
|
||||
new RecorderController(context, generator);
|
||||
(context as any)._exposeConsoleApi();
|
||||
new ConsoleApiSupplement(context);
|
||||
new RecorderSupplement(context,
|
||||
language,
|
||||
launchOptions,
|
||||
contextOptions,
|
||||
options.device,
|
||||
options.saveStorage,
|
||||
output);
|
||||
|
||||
await openPage(context, url);
|
||||
if (process.env.PWCLI_EXIT_FOR_TEST)
|
||||
await Promise.all(context.pages().map(p => p.close()));
|
||||
@ -387,20 +383,23 @@ async function pdf(options: Options, captureOptions: CaptureOptions, url: string
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
function lookupBrowserType(options: Options): BrowserType<Browser> {
|
||||
function lookupBrowserType(options: Options): BrowserType {
|
||||
let name = options.browser;
|
||||
if (options.device) {
|
||||
const device = playwright.devices[options.device];
|
||||
name = device.defaultBrowserType;
|
||||
}
|
||||
let browserType: any;
|
||||
switch (name) {
|
||||
case 'chromium': return playwright.chromium!;
|
||||
case 'webkit': return playwright.webkit!;
|
||||
case 'firefox': return playwright.firefox!;
|
||||
case 'cr': return playwright.chromium!;
|
||||
case 'wk': return playwright.webkit!;
|
||||
case 'ff': return playwright.firefox!;
|
||||
case 'chromium': browserType = playwright.chromium; break;
|
||||
case 'webkit': browserType = playwright.webkit; break;
|
||||
case 'firefox': browserType = playwright.firefox; break;
|
||||
case 'cr': browserType = playwright.chromium; break;
|
||||
case 'wk': browserType = playwright.webkit; break;
|
||||
case 'ff': browserType = playwright.firefox; break;
|
||||
}
|
||||
if (browserType)
|
||||
return browserType;
|
||||
program.help();
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,7 @@
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { installInspectorController } from '../server/inspector/inspectorController';
|
||||
import { installInspectorController } from '../server/supplements/inspectorController';
|
||||
import { DispatcherConnection } from '../dispatchers/dispatcher';
|
||||
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
|
||||
import { installBrowsersWithProgressBar } from '../install/installer';
|
||||
|
@ -253,14 +253,6 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel,
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async _exposeConsoleApi() {
|
||||
await this._channel.exposeConsoleApi();
|
||||
}
|
||||
|
||||
async _enableRecorder<Arg>() {
|
||||
await this._channel.enableRecorder();
|
||||
}
|
||||
}
|
||||
|
||||
export async function prepareBrowserContextOptions(options: BrowserContextOptions): Promise<channels.BrowserNewContextOptions> {
|
||||
|
23
src/client/supplements/consoleApiSupplement.ts
Normal file
23
src/client/supplements/consoleApiSupplement.ts
Normal file
@ -0,0 +1,23 @@
|
||||
/**
|
||||
* 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 => {});
|
||||
}
|
||||
}
|
@ -18,11 +18,11 @@ import * as fs from 'fs';
|
||||
import * as querystring from 'querystring';
|
||||
import { Writable } from 'stream';
|
||||
import * as hljs from '../../third_party/highlightjs/highlightjs';
|
||||
import { CodeGeneratorOutput } from './codeGenerator';
|
||||
import { RecorderOutput } from './recorderSupplement';
|
||||
|
||||
export class OutputMultiplexer implements CodeGeneratorOutput {
|
||||
private _outputs: CodeGeneratorOutput[]
|
||||
constructor(outputs: CodeGeneratorOutput[]) {
|
||||
export class OutputMultiplexer implements RecorderOutput {
|
||||
private _outputs: RecorderOutput[]
|
||||
constructor(outputs: RecorderOutput[]) {
|
||||
this._outputs = outputs;
|
||||
}
|
||||
|
||||
@ -58,7 +58,7 @@ export class BufferOutput {
|
||||
}
|
||||
}
|
||||
|
||||
export class FileOutput extends BufferOutput implements CodeGeneratorOutput {
|
||||
export class FileOutput extends BufferOutput implements RecorderOutput {
|
||||
private _fileName: string;
|
||||
|
||||
constructor(fileName: string) {
|
||||
@ -71,7 +71,7 @@ export class FileOutput extends BufferOutput implements CodeGeneratorOutput {
|
||||
}
|
||||
}
|
||||
|
||||
export class TerminalOutput implements CodeGeneratorOutput {
|
||||
export class TerminalOutput implements RecorderOutput {
|
||||
private _output: Writable
|
||||
private _language: string;
|
||||
|
||||
@ -127,7 +127,7 @@ export class TerminalOutput implements CodeGeneratorOutput {
|
||||
flush() {}
|
||||
}
|
||||
|
||||
export class FlushingTerminalOutput extends BufferOutput implements CodeGeneratorOutput {
|
||||
export class FlushingTerminalOutput extends BufferOutput implements RecorderOutput {
|
||||
private _output: Writable
|
||||
|
||||
constructor(output: Writable) {
|
51
src/client/supplements/recorderSupplement.ts
Normal file
51
src/client/supplements/recorderSupplement.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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;
|
||||
}
|
@ -21,6 +21,8 @@ import * as channels from '../protocol/channels';
|
||||
import { RouteDispatcher, RequestDispatcher } from './networkDispatchers';
|
||||
import { CRBrowserContext } from '../server/chromium/crBrowser';
|
||||
import { CDPSessionDispatcher } from './cdpSessionDispatcher';
|
||||
import { RecorderSupplement } from '../server/supplements/recorderSupplement';
|
||||
import { ConsoleApiSupplement } from '../server/supplements/consoleApiSupplement';
|
||||
|
||||
export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channels.BrowserContextInitializer> implements channels.BrowserContextChannel {
|
||||
private _context: BrowserContext;
|
||||
@ -125,12 +127,17 @@ export class BrowserContextDispatcher extends Dispatcher<BrowserContext, channel
|
||||
await this._context.close();
|
||||
}
|
||||
|
||||
async exposeConsoleApi(): Promise<void> {
|
||||
await this._context.exposeConsoleApi();
|
||||
async consoleSupplementExpose(): Promise<void> {
|
||||
const consoleApi = new ConsoleApiSupplement(this._context);
|
||||
await consoleApi.install();
|
||||
}
|
||||
|
||||
async enableRecorder(): Promise<void> {
|
||||
await this._context.enableRecorder();
|
||||
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 recorder.install();
|
||||
}
|
||||
|
||||
async crNewCDPSession(params: channels.BrowserContextCrNewCDPSessionParams): Promise<channels.BrowserContextCrNewCDPSessionResult> {
|
||||
|
@ -20,7 +20,7 @@ import type { Playwright as PlaywrightAPI } from './client/playwright';
|
||||
import { PlaywrightDispatcher } from './dispatchers/playwrightDispatcher';
|
||||
import { Connection } from './client/connection';
|
||||
import { BrowserServerLauncherImpl } from './browserServerImpl';
|
||||
import { installInspectorController } from './server/inspector/inspectorController';
|
||||
import { installInspectorController } from './server/supplements/inspectorController';
|
||||
import { installTracer } from './trace/tracer';
|
||||
import { installHarTracer } from './trace/harTracer';
|
||||
import * as path from 'path';
|
||||
|
@ -535,6 +535,8 @@ export interface BrowserContextChannel extends Channel {
|
||||
on(event: 'route', callback: (params: BrowserContextRouteEvent) => 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>;
|
||||
@ -552,8 +554,8 @@ export interface BrowserContextChannel extends Channel {
|
||||
setNetworkInterceptionEnabled(params: BrowserContextSetNetworkInterceptionEnabledParams, metadata?: Metadata): Promise<BrowserContextSetNetworkInterceptionEnabledResult>;
|
||||
setOffline(params: BrowserContextSetOfflineParams, metadata?: Metadata): Promise<BrowserContextSetOfflineResult>;
|
||||
storageState(params?: BrowserContextStorageStateParams, metadata?: Metadata): Promise<BrowserContextStorageStateResult>;
|
||||
exposeConsoleApi(params?: BrowserContextExposeConsoleApiParams, metadata?: Metadata): Promise<BrowserContextExposeConsoleApiResult>;
|
||||
enableRecorder(params?: BrowserContextEnableRecorderParams, metadata?: Metadata): Promise<BrowserContextEnableRecorderResult>;
|
||||
consoleSupplementExpose(params?: BrowserContextConsoleSupplementExposeParams, metadata?: Metadata): Promise<BrowserContextConsoleSupplementExposeResult>;
|
||||
recorderSupplementEnable(params: BrowserContextRecorderSupplementEnableParams, metadata?: Metadata): Promise<BrowserContextRecorderSupplementEnableResult>;
|
||||
crNewCDPSession(params: BrowserContextCrNewCDPSessionParams, metadata?: Metadata): Promise<BrowserContextCrNewCDPSessionResult>;
|
||||
}
|
||||
export type BrowserContextBindingCallEvent = {
|
||||
@ -573,6 +575,12 @@ export type BrowserContextCrBackgroundPageEvent = {
|
||||
export type BrowserContextCrServiceWorkerEvent = {
|
||||
worker: WorkerChannel,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementPrintLnEvent = {
|
||||
text: string,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementPopLnEvent = {
|
||||
text: string,
|
||||
};
|
||||
export type BrowserContextAddCookiesParams = {
|
||||
cookies: SetNetworkCookie[],
|
||||
};
|
||||
@ -695,12 +703,21 @@ export type BrowserContextStorageStateResult = {
|
||||
cookies: NetworkCookie[],
|
||||
origins: OriginStorage[],
|
||||
};
|
||||
export type BrowserContextExposeConsoleApiParams = {};
|
||||
export type BrowserContextExposeConsoleApiOptions = {};
|
||||
export type BrowserContextExposeConsoleApiResult = void;
|
||||
export type BrowserContextEnableRecorderParams = {};
|
||||
export type BrowserContextEnableRecorderOptions = {};
|
||||
export type BrowserContextEnableRecorderResult = void;
|
||||
export type BrowserContextConsoleSupplementExposeParams = {};
|
||||
export type BrowserContextConsoleSupplementExposeOptions = {};
|
||||
export type BrowserContextConsoleSupplementExposeResult = void;
|
||||
export type BrowserContextRecorderSupplementEnableParams = {
|
||||
language: string,
|
||||
launchOptions: any,
|
||||
contextOptions: any,
|
||||
device?: string,
|
||||
saveStorage?: string,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementEnableOptions = {
|
||||
device?: string,
|
||||
saveStorage?: string,
|
||||
};
|
||||
export type BrowserContextRecorderSupplementEnableResult = void;
|
||||
export type BrowserContextCrNewCDPSessionParams = {
|
||||
page: PageChannel,
|
||||
};
|
||||
|
@ -599,11 +599,17 @@ BrowserContext:
|
||||
type: array
|
||||
items: OriginStorage
|
||||
|
||||
exposeConsoleApi:
|
||||
consoleSupplementExpose:
|
||||
experimental: True
|
||||
|
||||
enableRecorder:
|
||||
recorderSupplementEnable:
|
||||
experimental: True
|
||||
parameters:
|
||||
language: string
|
||||
launchOptions: json
|
||||
contextOptions: json
|
||||
device: string?
|
||||
saveStorage: string?
|
||||
|
||||
crNewCDPSession:
|
||||
parameters:
|
||||
@ -636,7 +642,13 @@ BrowserContext:
|
||||
parameters:
|
||||
worker: Worker
|
||||
|
||||
recorderSupplementPrintLn:
|
||||
parameters:
|
||||
text: string
|
||||
|
||||
recorderSupplementPopLn:
|
||||
parameters:
|
||||
text: string
|
||||
|
||||
Page:
|
||||
type: interface
|
||||
|
@ -335,8 +335,14 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||
offline: tBoolean,
|
||||
});
|
||||
scheme.BrowserContextStorageStateParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextExposeConsoleApiParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextEnableRecorderParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextConsoleSupplementExposeParams = tOptional(tObject({}));
|
||||
scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
||||
language: tString,
|
||||
launchOptions: tAny,
|
||||
contextOptions: tAny,
|
||||
device: tOptional(tString),
|
||||
saveStorage: tOptional(tString),
|
||||
});
|
||||
scheme.BrowserContextCrNewCDPSessionParams = tObject({
|
||||
page: tChannel('Page'),
|
||||
});
|
||||
|
@ -17,7 +17,7 @@
|
||||
import * as debug from 'debug';
|
||||
import * as http from 'http';
|
||||
import * as WebSocket from 'ws';
|
||||
import { installInspectorController } from '../server/inspector/inspectorController';
|
||||
import { installInspectorController } from '../server/supplements/inspectorController';
|
||||
import { DispatcherConnection } from '../dispatchers/dispatcher';
|
||||
import { PlaywrightDispatcher } from '../dispatchers/playwrightDispatcher';
|
||||
import { Playwright } from '../server/playwright';
|
||||
|
@ -19,8 +19,6 @@ import { EventEmitter } from 'events';
|
||||
import { TimeoutSettings } from '../utils/timeoutSettings';
|
||||
import { mkdirIfNeeded } from '../utils/utils';
|
||||
import { Browser, BrowserOptions } from './browser';
|
||||
import * as consoleApiSource from '../generated/consoleApiSource';
|
||||
import * as recorderSource from '../generated/recorderSource';
|
||||
import * as dom from './dom';
|
||||
import { Download } from './download';
|
||||
import * as frames from './frames';
|
||||
@ -381,15 +379,7 @@ export abstract class BrowserContext extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
async exposeConsoleApi() {
|
||||
await this._extendInjectedScript(consoleApiSource.source);
|
||||
}
|
||||
|
||||
async enableRecorder() {
|
||||
await this._extendInjectedScript(recorderSource.source);
|
||||
}
|
||||
|
||||
private async _extendInjectedScript(source: string) {
|
||||
async extendInjectedScript(source: string) {
|
||||
const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source).catch(e => {});
|
||||
const installInPage = (page: Page) => {
|
||||
page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame);
|
||||
|
30
src/server/supplements/consoleApiSupplement.ts
Normal file
30
src/server/supplements/consoleApiSupplement.ts
Normal file
@ -0,0 +1,30 @@
|
||||
/**
|
||||
* 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 consoleApiSource from '../../generated/consoleApiSource';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
|
||||
export class ConsoleApiSupplement {
|
||||
private _context: BrowserContext;
|
||||
|
||||
constructor(context: BrowserContext) {
|
||||
this._context = context;
|
||||
}
|
||||
|
||||
async install() {
|
||||
await this._context.extendInjectedScript(consoleApiSource.source);
|
||||
}
|
||||
}
|
@ -14,16 +14,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type * as actions from '../../../cli/codegen/recorderActions';
|
||||
import type * as actions from '../recorder/recorderActions';
|
||||
import type InjectedScript from '../../injected/injectedScript';
|
||||
import { generateSelector } from './selectorGenerator';
|
||||
import { html } from './html';
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
performPlaywrightAction: (action: actions.Action) => Promise<void>;
|
||||
recordPlaywrightAction: (action: actions.Action) => Promise<void>;
|
||||
commitLastAction: () => Promise<void>;
|
||||
playwrightRecorderPerformAction: (action: actions.Action) => Promise<void>;
|
||||
playwrightRecorderRecordAction: (action: actions.Action) => Promise<void>;
|
||||
playwrightRecorderCommitAction: () => Promise<void>;
|
||||
}
|
||||
}
|
||||
|
||||
@ -238,7 +238,7 @@ export class Recorder {
|
||||
const { selector, elements } = generateSelector(this._injectedScript, hoveredElement);
|
||||
if ((this._hoveredModel && this._hoveredModel.selector === selector) || this._hoveredElement !== hoveredElement)
|
||||
return;
|
||||
window.commitLastAction();
|
||||
window.playwrightRecorderCommitAction();
|
||||
this._hoveredModel = selector ? { selector, elements } : null;
|
||||
this._updateHighlight();
|
||||
if ((window as any)._highlightUpdatedForTest)
|
||||
@ -331,7 +331,7 @@ export class Recorder {
|
||||
}
|
||||
|
||||
if (elementType === 'file') {
|
||||
window.recordPlaywrightAction({
|
||||
window.playwrightRecorderRecordAction({
|
||||
name: 'setInputFiles',
|
||||
selector: this._activeModel!.selector,
|
||||
signals: [],
|
||||
@ -343,7 +343,7 @@ export class Recorder {
|
||||
// Non-navigating actions are simply recorded by Playwright.
|
||||
if (this._consumedDueWrongTarget(event))
|
||||
return;
|
||||
window.recordPlaywrightAction({
|
||||
window.playwrightRecorderRecordAction({
|
||||
name: 'fill',
|
||||
selector: this._activeModel!.selector,
|
||||
signals: [],
|
||||
@ -434,7 +434,7 @@ export class Recorder {
|
||||
|
||||
private async _performAction(action: actions.Action) {
|
||||
this._performingAction = true;
|
||||
await window.performPlaywrightAction(action);
|
||||
await window.playwrightRecorderPerformAction(action);
|
||||
this._performingAction = false;
|
||||
|
||||
// Action could have changed DOM, update hovered model selectors.
|
@ -16,6 +16,7 @@
|
||||
|
||||
import { BrowserContext, ContextListener, contextListeners } from '../browserContext';
|
||||
import { isDebugMode } from '../../utils/utils';
|
||||
import { ConsoleApiSupplement } from './consoleApiSupplement';
|
||||
|
||||
export function installInspectorController() {
|
||||
contextListeners.add(new InspectorController());
|
||||
@ -23,8 +24,10 @@ export function installInspectorController() {
|
||||
|
||||
class InspectorController implements ContextListener {
|
||||
async onContextCreated(context: BrowserContext): Promise<void> {
|
||||
if (isDebugMode())
|
||||
context.exposeConsoleApi();
|
||||
if (isDebugMode()) {
|
||||
const consoleApi = new ConsoleApiSupplement(context);
|
||||
await consoleApi.install();
|
||||
}
|
||||
}
|
||||
async onContextWillDestroy(context: BrowserContext): Promise<void> {}
|
||||
async onContextDidDestroy(context: BrowserContext): Promise<void> {}
|
@ -14,8 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { LaunchOptions, Frame, BrowserContextOptions } from '../../..';
|
||||
import { LanguageGenerator } from './languages';
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import { Frame } from '../../frames';
|
||||
import { LanguageGenerator } from './language';
|
||||
import { Action, Signal } from './recorderActions';
|
||||
|
||||
export type ActionInContext = {
|
||||
@ -28,7 +29,6 @@ export type ActionInContext = {
|
||||
export interface CodeGeneratorOutput {
|
||||
printLn(text: string): void;
|
||||
popLn(text: string): void;
|
||||
flush(): void;
|
||||
}
|
||||
|
||||
export class CodeGenerator {
|
||||
@ -50,10 +50,6 @@ export class CodeGenerator {
|
||||
this._output.printLn(this._footerText);
|
||||
}
|
||||
|
||||
exit() {
|
||||
this._output.flush();
|
||||
}
|
||||
|
||||
addAction(action: ActionInContext) {
|
||||
this.willPerformAction(action);
|
||||
this.didPerformAction(action);
|
@ -15,18 +15,14 @@
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import * as playwright from '../../../..';
|
||||
import { HighlighterType, LanguageGenerator } from '.';
|
||||
import { ActionInContext } from '../codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from '../recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from '../utils';
|
||||
import { LanguageGenerator, sanitizeDeviceOptions } from './language';
|
||||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
|
||||
export class CSharpLanguageGenerator implements LanguageGenerator {
|
||||
|
||||
highlighterType(): HighlighterType {
|
||||
return 'csharp';
|
||||
}
|
||||
|
||||
generateAction(actionInContext: ActionInContext, performingAction: boolean): string {
|
||||
const { action, pageAlias, frame } = actionInContext;
|
||||
const formatter = new CSharpFormatter(0);
|
||||
@ -240,16 +236,10 @@ function toPascal(value: string): string {
|
||||
}
|
||||
|
||||
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string {
|
||||
const device = deviceName && playwright.devices[deviceName];
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (!device)
|
||||
return formatArgs(options);
|
||||
// Filter out all the properties from the device descriptor.
|
||||
const cleanedOptions: Record<string, any> = {};
|
||||
for (const property in options) {
|
||||
if ((device as any)[property] !== (options as any)[property])
|
||||
cleanedOptions[property] = (options as any)[property];
|
||||
}
|
||||
const serializedObject = formatObject(cleanedOptions, ' ');
|
||||
const serializedObject = formatObject(sanitizeDeviceOptions(device, options), ' ');
|
||||
// When there are no additional context options, we still want to spread the device inside.
|
||||
|
||||
if (!serializedObject)
|
@ -15,18 +15,14 @@
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import * as playwright from '../../../..';
|
||||
import { HighlighterType, LanguageGenerator } from '.';
|
||||
import { ActionInContext } from '../codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from '../recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from '../utils';
|
||||
import { LanguageGenerator, sanitizeDeviceOptions } from './language';
|
||||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
|
||||
export class JavaScriptLanguageGenerator implements LanguageGenerator {
|
||||
|
||||
highlighterType(): HighlighterType {
|
||||
return 'javascript';
|
||||
}
|
||||
|
||||
generateAction(actionInContext: ActionInContext, performingAction: boolean): string {
|
||||
const { action, pageAlias, frame } = actionInContext;
|
||||
const formatter = new JavaScriptFormatter(2);
|
||||
@ -195,16 +191,11 @@ function formatObjectOrVoid(value: any, indent = ' '): string {
|
||||
}
|
||||
|
||||
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string {
|
||||
const device = deviceName && playwright.devices[deviceName];
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (!device)
|
||||
return formatObjectOrVoid(options);
|
||||
// Filter out all the properties from the device descriptor.
|
||||
const cleanedOptions: Record<string, any> = {};
|
||||
for (const property in options) {
|
||||
if ((device as any)[property] !== (options as any)[property])
|
||||
cleanedOptions[property] = (options as any)[property];
|
||||
}
|
||||
let serializedObject = formatObjectOrVoid(cleanedOptions);
|
||||
let serializedObject = formatObjectOrVoid(sanitizeDeviceOptions(device, options));
|
||||
// When there are no additional context options, we still want to spread the device inside.
|
||||
if (!serializedObject)
|
||||
serializedObject = '{\n}';
|
@ -15,15 +15,20 @@
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import { ActionInContext } from '../codeGenerator';
|
||||
|
||||
export type HighlighterType = 'javascript' | 'csharp' | 'python';
|
||||
import { ActionInContext } from './codeGenerator';
|
||||
|
||||
export interface LanguageGenerator {
|
||||
generateHeader(browserName: string, launchOptions: LaunchOptions, contextOptions: BrowserContextOptions, deviceName?: string): string;
|
||||
generateAction(actionInContext: ActionInContext, performingAction: boolean): string;
|
||||
generateFooter(saveStorage: string | undefined): string;
|
||||
highlighterType(): HighlighterType;
|
||||
}
|
||||
|
||||
export { JavaScriptLanguageGenerator } from './javascript';
|
||||
export function sanitizeDeviceOptions(device: any, options: BrowserContextOptions): BrowserContextOptions {
|
||||
// Filter out all the properties from the device descriptor.
|
||||
const cleanedOptions: Record<string, any> = {};
|
||||
for (const property in options) {
|
||||
if (JSON.stringify(device[property]) !== JSON.stringify((options as any)[property]))
|
||||
cleanedOptions[property] = (options as any)[property];
|
||||
}
|
||||
return cleanedOptions;
|
||||
}
|
@ -15,11 +15,11 @@
|
||||
*/
|
||||
|
||||
import type { BrowserContextOptions, LaunchOptions } from '../../../..';
|
||||
import * as playwright from '../../../..';
|
||||
import { HighlighterType, LanguageGenerator } from '.';
|
||||
import { ActionInContext } from '../codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from '../recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from '../utils';
|
||||
import { LanguageGenerator, sanitizeDeviceOptions } from './language';
|
||||
import { ActionInContext } from './codeGenerator';
|
||||
import { actionTitle, NavigationSignal, PopupSignal, DownloadSignal, DialogSignal, Action } from './recorderActions';
|
||||
import { MouseClickOptions, toModifiers } from './utils';
|
||||
import deviceDescriptors = require('../../deviceDescriptors');
|
||||
|
||||
export class PythonLanguageGenerator implements LanguageGenerator {
|
||||
private _awaitPrefix: '' | 'await ';
|
||||
@ -32,10 +32,6 @@ export class PythonLanguageGenerator implements LanguageGenerator {
|
||||
this._asyncPrefix = isAsync ? 'async ' : '';
|
||||
}
|
||||
|
||||
highlighterType(): HighlighterType {
|
||||
return 'python';
|
||||
}
|
||||
|
||||
generateAction(actionInContext: ActionInContext, performingAction: boolean): string {
|
||||
const { action, pageAlias, frame } = actionInContext;
|
||||
const formatter = new PythonFormatter(4);
|
||||
@ -217,16 +213,10 @@ function formatOptions(value: any, hasArguments: boolean): string {
|
||||
}
|
||||
|
||||
function formatContextOptions(options: BrowserContextOptions, deviceName: string | undefined): string {
|
||||
const device = deviceName && playwright.devices[deviceName];
|
||||
const device = deviceName && deviceDescriptors[deviceName];
|
||||
if (!device)
|
||||
return formatOptions(options, false);
|
||||
// Filter out all the properties from the device descriptor.
|
||||
const cleanedOptions: Record<string, any> = {};
|
||||
for (const property in options) {
|
||||
if ((device as any)[property] !== (options as any)[property])
|
||||
cleanedOptions[property] = (options as any)[property];
|
||||
}
|
||||
return `**playwright.devices["${deviceName}"]` + formatOptions(cleanedOptions, true);
|
||||
return `**playwright.devices["${deviceName}"]` + formatOptions(sanitizeDeviceOptions(device, options), true);
|
||||
}
|
||||
|
||||
class PythonFormatter {
|
@ -14,10 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Page } from '../../..';
|
||||
import { Frame } from '../../frames';
|
||||
import * as actions from './recorderActions';
|
||||
|
||||
export type MouseClickOptions = Parameters<Page['click']>[1];
|
||||
export type MouseClickOptions = Parameters<Frame['click']>[2];
|
||||
|
||||
export function toClickOptions(action: actions.ClickAction): { method: 'click' | 'dblclick', options: MouseClickOptions } {
|
||||
let method: 'click' | 'dblclick' = 'click';
|
@ -14,52 +14,74 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import type { Page, BrowserContext, Frame, Download, Dialog } from '../../..';
|
||||
import * as actions from './recorderActions';
|
||||
import { CodeGenerator, ActionInContext } from './codeGenerator';
|
||||
import { toClickOptions, toModifiers } from './utils';
|
||||
import * as actions from './recorder/recorderActions';
|
||||
import { CodeGenerator, ActionInContext, CodeGeneratorOutput } from './recorder/codeGenerator';
|
||||
import { toClickOptions, toModifiers } from './recorder/utils';
|
||||
import { Page } from '../page';
|
||||
import { Frame } from '../frames';
|
||||
import { BrowserContext } from '../browserContext';
|
||||
import { LanguageGenerator } from './recorder/language';
|
||||
import { JavaScriptLanguageGenerator } from './recorder/javascript';
|
||||
import { CSharpLanguageGenerator } from './recorder/csharp';
|
||||
import { PythonLanguageGenerator } from './recorder/python';
|
||||
import { ProgressController } from '../progress';
|
||||
import * as recorderSource from '../../generated/recorderSource';
|
||||
|
||||
type BindingSource = { frame: Frame, page: Page };
|
||||
|
||||
export class RecorderController {
|
||||
export class RecorderSupplement {
|
||||
private _generator: CodeGenerator;
|
||||
private _pageAliases = new Map<Page, string>();
|
||||
private _lastPopupOrdinal = 0;
|
||||
private _lastDialogOrdinal = 0;
|
||||
private _timers = new Set<NodeJS.Timeout>();
|
||||
private _context: BrowserContext;
|
||||
|
||||
constructor(context: BrowserContext, generator: CodeGenerator) {
|
||||
(context as any)._enableRecorder();
|
||||
constructor(context: BrowserContext, params: { language: string, launchOptions: any, contextOptions: any, device?: string, saveStorage?: string}, output: CodeGeneratorOutput) {
|
||||
this._context = context;
|
||||
let languageGenerator: LanguageGenerator;
|
||||
|
||||
switch (params.language) {
|
||||
case 'javascript': languageGenerator = new JavaScriptLanguageGenerator(); break;
|
||||
case 'csharp': languageGenerator = new CSharpLanguageGenerator(); break;
|
||||
case 'python':
|
||||
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);
|
||||
this._generator = generator;
|
||||
}
|
||||
|
||||
// Input actions that potentially lead to navigation are intercepted on the page and are
|
||||
// performed by the Playwright.
|
||||
context.exposeBinding('performPlaywrightAction',
|
||||
(source: BindingSource, action: actions.Action) => this._performAction(source.frame, action)).catch(e => {});
|
||||
|
||||
// Other non-essential actions are simply being recorded.
|
||||
context.exposeBinding('recordPlaywrightAction',
|
||||
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action)).catch(e => {});
|
||||
|
||||
// Commits last action so that no further signals are added to it.
|
||||
context.exposeBinding('commitLastAction',
|
||||
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction()).catch(e => {});
|
||||
|
||||
context.on('page', page => this._onPage(page));
|
||||
for (const page of context.pages())
|
||||
async install() {
|
||||
this._context.on('page', page => this._onPage(page));
|
||||
for (const page of this._context.pages())
|
||||
this._onPage(page);
|
||||
|
||||
context.once('close', () => {
|
||||
this._context.once('close', () => {
|
||||
for (const timer of this._timers)
|
||||
clearTimeout(timer);
|
||||
this._timers.clear();
|
||||
this._generator.exit();
|
||||
});
|
||||
|
||||
// Input actions that potentially lead to navigation are intercepted on the page and are
|
||||
// performed by the Playwright.
|
||||
await this._context.exposeBinding('playwrightRecorderPerformAction', false,
|
||||
(source: BindingSource, action: actions.Action) => this._performAction(source.frame, action));
|
||||
|
||||
// Other non-essential actions are simply being recorded.
|
||||
await this._context.exposeBinding('playwrightRecorderRecordAction', false,
|
||||
(source: BindingSource, action: actions.Action) => this._recordAction(source.frame, action));
|
||||
|
||||
// Commits last action so that no further signals are added to it.
|
||||
await this._context.exposeBinding('playwrightRecorderCommitAction', false,
|
||||
(source: BindingSource, action: actions.Action) => this._generator.commitLastAction());
|
||||
|
||||
await this._context.extendInjectedScript(recorderSource.source);
|
||||
}
|
||||
|
||||
private async _onPage(page: Page) {
|
||||
// First page is called page, others are called popup1, popup2, etc.
|
||||
const frame = page.mainFrame();
|
||||
page.on('close', () => {
|
||||
this._pageAliases.delete(page);
|
||||
this._generator.addAction({
|
||||
@ -72,10 +94,10 @@ export class RecorderController {
|
||||
}
|
||||
});
|
||||
});
|
||||
page.on('framenavigated', frame => this._onFrameNavigated(frame, page));
|
||||
page.on('download', download => this._onDownload(page, download));
|
||||
page.on('popup', popup => this._onPopup(page, popup));
|
||||
page.on('dialog', dialog => this._onDialog(page, dialog));
|
||||
frame.on(Frame.Events.Navigation, () => this._onFrameNavigated(frame, page));
|
||||
page.on(Page.Events.Download, () => this._onDownload(page));
|
||||
page.on(Page.Events.Popup, popup => this._onPopup(page, popup));
|
||||
page.on(Page.Events.Dialog, () => this._onDialog(page));
|
||||
const suffix = this._pageAliases.size ? String(++this._lastPopupOrdinal) : '';
|
||||
const pageAlias = 'page' + suffix;
|
||||
this._pageAliases.set(page, pageAlias);
|
||||
@ -91,7 +113,7 @@ export class RecorderController {
|
||||
committed: true,
|
||||
action: {
|
||||
name: 'openPage',
|
||||
url: page.url(),
|
||||
url: page.mainFrame().url(),
|
||||
signals: [],
|
||||
}
|
||||
});
|
||||
@ -99,7 +121,8 @@ export class RecorderController {
|
||||
}
|
||||
|
||||
private async _performAction(frame: Frame, action: actions.Action) {
|
||||
const page = frame.page();
|
||||
const page = frame._page;
|
||||
const controller = new ProgressController();
|
||||
const actionInContext: ActionInContext = {
|
||||
pageAlias: this._pageAliases.get(page)!,
|
||||
frame,
|
||||
@ -108,19 +131,19 @@ export class RecorderController {
|
||||
this._generator.willPerformAction(actionInContext);
|
||||
if (action.name === 'click') {
|
||||
const { options } = toClickOptions(action);
|
||||
await frame.click(action.selector, options);
|
||||
await frame.click(controller, action.selector, options);
|
||||
}
|
||||
if (action.name === 'press') {
|
||||
const modifiers = toModifiers(action.modifiers);
|
||||
const shortcut = [...modifiers, action.key].join('+');
|
||||
await frame.press(action.selector, shortcut);
|
||||
await frame.press(controller, action.selector, shortcut);
|
||||
}
|
||||
if (action.name === 'check')
|
||||
await frame.check(action.selector);
|
||||
await frame.check(controller, action.selector);
|
||||
if (action.name === 'uncheck')
|
||||
await frame.uncheck(action.selector);
|
||||
await frame.uncheck(controller, action.selector);
|
||||
if (action.name === 'select')
|
||||
await frame.selectOption(action.selector, action.options);
|
||||
await frame.selectOption(controller, action.selector, [], action.options.map(value => ({ value })));
|
||||
const timer = setTimeout(() => {
|
||||
actionInContext.committed = true;
|
||||
this._timers.delete(timer);
|
||||
@ -132,15 +155,13 @@ export class RecorderController {
|
||||
private async _recordAction(frame: Frame, action: actions.Action) {
|
||||
// We are lacking frame.page() in
|
||||
this._generator.addAction({
|
||||
pageAlias: this._pageAliases.get(frame.page())!,
|
||||
pageAlias: this._pageAliases.get(frame._page)!,
|
||||
frame,
|
||||
action
|
||||
});
|
||||
}
|
||||
|
||||
private _onFrameNavigated(frame: Frame, page: Page) {
|
||||
if (frame.parentFrame())
|
||||
return;
|
||||
const pageAlias = this._pageAliases.get(page);
|
||||
this._generator.signal(pageAlias!, frame, { name: 'navigation', url: frame.url() });
|
||||
}
|
||||
@ -150,12 +171,12 @@ export class RecorderController {
|
||||
const popupAlias = this._pageAliases.get(popup)!;
|
||||
this._generator.signal(pageAlias, page.mainFrame(), { name: 'popup', popupAlias });
|
||||
}
|
||||
private _onDownload(page: Page, download: Download) {
|
||||
private _onDownload(page: Page) {
|
||||
const pageAlias = this._pageAliases.get(page)!;
|
||||
this._generator.signal(pageAlias, page.mainFrame(), { name: 'download' });
|
||||
}
|
||||
|
||||
private _onDialog(page: Page, dialog: Dialog) {
|
||||
private _onDialog(page: Page) {
|
||||
const pageAlias = this._pageAliases.get(page)!;
|
||||
this._generator.signal(pageAlias, page.mainFrame(), { name: 'dialog', dialogAlias: String(++this._lastDialogOrdinal) });
|
||||
}
|
@ -235,8 +235,7 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
recorder.waitForOutput('check'),
|
||||
page.click('input')
|
||||
]);
|
||||
await recorder.waitForOutput('check');
|
||||
expect(recorder.output()).toContain(`
|
||||
await recorder.waitForOutput(`
|
||||
// Check input[name="accept"]
|
||||
await page.check('input[name="accept"]');`);
|
||||
expect(message.text()).toBe('true');
|
||||
@ -253,8 +252,7 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
recorder.waitForOutput('check'),
|
||||
page.keyboard.press('Space')
|
||||
]);
|
||||
await recorder.waitForOutput('check');
|
||||
expect(recorder.output()).toContain(`
|
||||
await recorder.waitForOutput(`
|
||||
// Check input[name="accept"]
|
||||
await page.check('input[name="accept"]');`);
|
||||
expect(message.text()).toBe('true');
|
||||
@ -321,7 +319,6 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
|
||||
const selector = await recorder.hoverOverElement('a');
|
||||
expect(selector).toBe('text="link"');
|
||||
|
||||
await Promise.all([
|
||||
page.waitForNavigation(),
|
||||
recorder.waitForOutput('assert'),
|
||||
@ -398,8 +395,7 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
await page.setInputFiles('input[type=file]', 'test/assets/file-to-upload.txt');
|
||||
await page.click('input[type=file]');
|
||||
|
||||
await recorder.waitForOutput('setInputFiles');
|
||||
expect(recorder.output()).toContain(`
|
||||
await recorder.waitForOutput(`
|
||||
// Upload file-to-upload.txt
|
||||
await page.setInputFiles('input[type="file"]', 'file-to-upload.txt');`);
|
||||
});
|
||||
@ -415,8 +411,7 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
await page.setInputFiles('input[type=file]', ['test/assets/file-to-upload.txt', 'test/assets/file-to-upload-2.txt']);
|
||||
await page.click('input[type=file]');
|
||||
|
||||
await recorder.waitForOutput('setInputFiles');
|
||||
expect(recorder.output()).toContain(`
|
||||
await recorder.waitForOutput(`
|
||||
// Upload file-to-upload.txt, file-to-upload-2.txt
|
||||
await page.setInputFiles('input[type="file"]', ['file-to-upload.txt', 'file-to-upload-2.txt']);`);
|
||||
});
|
||||
@ -432,13 +427,14 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
await page.setInputFiles('input[type=file]', []);
|
||||
await page.click('input[type=file]');
|
||||
|
||||
await recorder.waitForOutput('setInputFiles');
|
||||
expect(recorder.output()).toContain(`
|
||||
await recorder.waitForOutput(`
|
||||
// Clear selected files
|
||||
await page.setInputFiles('input[type="file"]', []);`);
|
||||
});
|
||||
|
||||
it('should download files', async ({ page, recorder, httpServer }) => {
|
||||
it('should download files', (test, {browserName}) => {
|
||||
test.fixme(browserName === 'webkit', 'Generated page.waitForNavigation next to page.waitForEvent(download)');
|
||||
}, async ({ page, recorder, httpServer }) => {
|
||||
httpServer.setHandler((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||
const pathName = url.parse(req.url!).path;
|
||||
if (pathName === '/download') {
|
||||
@ -458,8 +454,7 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
page.waitForEvent('download'),
|
||||
page.click('text=Download')
|
||||
]);
|
||||
await recorder.waitForOutput('page.click');
|
||||
expect(recorder.output()).toContain(`
|
||||
await recorder.waitForOutput(`
|
||||
// Click text="Download"
|
||||
const [download] = await Promise.all([
|
||||
page.waitForEvent('download'),
|
||||
@ -476,8 +471,7 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
await dialog.dismiss();
|
||||
});
|
||||
await page.click('text="click me"');
|
||||
await recorder.waitForOutput('page.once');
|
||||
expect(recorder.output()).toContain(`
|
||||
await recorder.waitForOutput(`
|
||||
// Click text="click me"
|
||||
page.once('dialog', dialog => {
|
||||
console.log(\`Dialog message: $\{dialog.message()}\`);
|
||||
@ -500,8 +494,7 @@ describe('cli codegen', (test, { browserName, headful }) => {
|
||||
</script>`, httpServer.PREFIX);
|
||||
for (let i = 1; i < 3; ++i) {
|
||||
await page.evaluate('pushState()');
|
||||
await recorder.waitForOutput(`seqNum=${i}`);
|
||||
expect(recorder.output()).toContain(`await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`);
|
||||
await recorder.waitForOutput(`await page.goto('${httpServer.PREFIX}/#seqNum=${i}');`);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -21,10 +21,8 @@ import { ChildProcess, spawn } from 'child_process';
|
||||
import { folio as baseFolio } from '../fixtures';
|
||||
import type { Page, BrowserType, Browser, BrowserContext } from '../..';
|
||||
export { config } from 'folio';
|
||||
import { RecorderController } from '../../src/cli/codegen/recorderController';
|
||||
import { TerminalOutput } from '../../src/cli/codegen/outputs';
|
||||
import { JavaScriptLanguageGenerator } from '../../src/cli/codegen/languages';
|
||||
import { CodeGenerator } from '../../src/cli/codegen/codeGenerator';
|
||||
import { FlushingTerminalOutput } from '../../lib/client/supplements/recorderOutputs';
|
||||
import { RecorderSupplement } from '../../lib/client/supplements/recorderSupplement';
|
||||
|
||||
type WorkerFixtures = {
|
||||
browserType: BrowserType<Browser>;
|
||||
@ -41,12 +39,10 @@ type TestFixtures = {
|
||||
export const fixtures = baseFolio.extend<TestFixtures, WorkerFixtures>();
|
||||
|
||||
fixtures.contextWrapper.init(async ({ browser }, runTest) => {
|
||||
const context = await browser.newContext();
|
||||
const context = await browser.newContext() as BrowserContext;
|
||||
const outputBuffer = new WritableBuffer();
|
||||
const output = new TerminalOutput(outputBuffer as any as Writable, 'javascript');
|
||||
const languageGenerator = new JavaScriptLanguageGenerator();
|
||||
const generator = new CodeGenerator('chromium', {}, {}, output, languageGenerator, undefined, undefined);
|
||||
new RecorderController(context, generator);
|
||||
const output = new FlushingTerminalOutput(outputBuffer as any as Writable);
|
||||
new RecorderSupplement(context, 'javascript', {}, {}, undefined, undefined, output);
|
||||
await runTest({ context, output: outputBuffer });
|
||||
await context.close();
|
||||
});
|
||||
|
@ -16,10 +16,11 @@
|
||||
|
||||
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) => {
|
||||
await (context as any)._exposeConsoleApi();
|
||||
new ConsoleApiSupplement(context);
|
||||
await run(context);
|
||||
});
|
||||
const { describe, it, expect } = fixtures.build();
|
||||
|
@ -68,8 +68,8 @@ function runBuild() {
|
||||
const webPackFiles = [
|
||||
'src/server/injected/injectedScript.webpack.config.js',
|
||||
'src/server/injected/utilityScript.webpack.config.js',
|
||||
'src/server/inspector/injected/consoleApi.webpack.config.js',
|
||||
'src/server/inspector/injected/recorder.webpack.config.js',
|
||||
'src/server/supplements/injected/consoleApi.webpack.config.js',
|
||||
'src/server/supplements/injected/recorder.webpack.config.js',
|
||||
'src/cli/traceViewer/web/web.webpack.config.js',
|
||||
];
|
||||
for (const file of webPackFiles) {
|
||||
|
@ -125,13 +125,15 @@ DEPS['src/server/'] = [
|
||||
// Can depend on any files in these subdirectories.
|
||||
'src/server/common/**',
|
||||
'src/server/injected/**',
|
||||
'src/server/supplements/**',
|
||||
];
|
||||
|
||||
// No dependencies for code shared between node and page.
|
||||
DEPS['src/server/common/'] = [];
|
||||
// Strict dependencies for injected code.
|
||||
DEPS['src/server/injected/'] = ['src/server/common/'];
|
||||
DEPS['src/server/inspector/injected/'] = ['src/server/common/', 'src/cli/codegen/', 'src/server/injected/'];
|
||||
|
||||
DEPS['src/client/supplements/'] = ['src/client/'];
|
||||
|
||||
// Electron and Clank use chromium internally.
|
||||
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
|
||||
@ -144,7 +146,7 @@ DEPS['src/cli/driver.ts'] = DEPS['src/inprocess.ts'] = DEPS['src/browserServerIm
|
||||
DEPS['src/trace/'] = ['src/utils/', 'src/client/**', 'src/server/**'];
|
||||
|
||||
// The service is a cross-cutting feature, and so it depends on a bunch of things.
|
||||
DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/inspector/', 'src/server/electron/', 'src/trace/'];
|
||||
DEPS['src/remote/'] = ['src/client/', 'src/debug/', 'src/dispatchers/', 'src/server/', 'src/server/supplements/', 'src/server/electron/', 'src/trace/'];
|
||||
DEPS['src/service.ts'] = ['src/remote/'];
|
||||
|
||||
// CLI should only use client-side features.
|
||||
|
Loading…
Reference in New Issue
Block a user