chore: switch to the new debug controller harness (#18308)

This commit is contained in:
Pavel Feldman 2022-10-25 12:55:20 -04:00 committed by GitHub
parent d819f97f40
commit 37250cde17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 124 additions and 154 deletions

View File

@ -57,7 +57,7 @@ program
commandWithOpenOptions('open [url]', 'open page in browser specified via -b, --browser', []) commandWithOpenOptions('open [url]', 'open page in browser specified via -b, --browser', [])
.action(function(url, options) { .action(function(url, options) {
open(options, url, language()).catch(logErrorAndExit); open(options, url, codegenId()).catch(logErrorAndExit);
}) })
.addHelpText('afterAll', ` .addHelpText('afterAll', `
Examples: Examples:
@ -68,7 +68,7 @@ Examples:
commandWithOpenOptions('codegen [url]', 'open page and generate code for user actions', commandWithOpenOptions('codegen [url]', 'open page and generate code for user actions',
[ [
['-o, --output <file name>', 'saves the generated script to a file'], ['-o, --output <file name>', 'saves the generated script to a file'],
['--target <language>', `language to generate, one of javascript, test, python, python-async, pytest, csharp, csharp-mstest, csharp-nunit, java`, language()], ['--target <language>', `language to generate, one of javascript, playwright-test, python, python-async, python-pytest, csharp, csharp-mstest, csharp-nunit, java`, codegenId()],
['--save-trace <filename>', 'record a trace for the session and save it to a file'], ['--save-trace <filename>', 'record a trace for the session and save it to a file'],
]).action(function(url, options) { ]).action(function(url, options) {
codegen(options, url, options.target, options.output).catch(logErrorAndExit); codegen(options, url, options.target, options.output).catch(logErrorAndExit);
@ -267,13 +267,12 @@ program
program program
.command('run-server', { hidden: true }) .command('run-server', { hidden: true })
.option('--reuse-browser', 'Whether to reuse the browser instance')
.option('--port <port>', 'Server port') .option('--port <port>', 'Server port')
.option('--path <path>', 'Endpoint Path', '/') .option('--path <path>', 'Endpoint Path', '/')
.option('--max-clients <maxClients>', 'Maximum clients') .option('--max-clients <maxClients>', 'Maximum clients')
.option('--no-socks-proxy', 'Disable Socks Proxy') .option('--no-socks-proxy', 'Disable Socks Proxy')
.action(function(options) { .action(function(options) {
runServer(options.port ? +options.port : undefined, options.path, options.maxClients ? +options.maxClients : Infinity, options.socksProxy, options.reuseBrowser).catch(logErrorAndExit); runServer(options.port ? +options.port : undefined, options.path, options.maxClients ? +options.maxClients : Infinity, options.socksProxy).catch(logErrorAndExit);
}); });
program program
@ -682,8 +681,8 @@ function logErrorAndExit(e: Error) {
process.exit(1); process.exit(1);
} }
function language(): string { function codegenId(): string {
return process.env.PW_LANG_NAME || 'test'; return process.env.PW_LANG_NAME || 'playwright-test';
} }
function commandWithOpenOptions(command: string, description: string, options: any[][]): Command { function commandWithOpenOptions(command: string, description: string, options: any[][]): Command {

View File

@ -21,12 +21,9 @@ import * as playwright from '../..';
import type { BrowserType } from '../client/browserType'; import type { BrowserType } from '../client/browserType';
import type { LaunchServerOptions } from '../client/types'; import type { LaunchServerOptions } from '../client/types';
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server'; import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
import type { Playwright } from '../server/playwright';
import { IpcTransport, PipeTransport } from '../protocol/transport'; import { IpcTransport, PipeTransport } from '../protocol/transport';
import { PlaywrightServer } from '../remote/playwrightServer'; import { PlaywrightServer } from '../remote/playwrightServer';
import { gracefullyCloseAll } from '../utils/processLauncher'; import { gracefullyCloseAll } from '../utils/processLauncher';
import type { Mode } from '@recorder/recorderTypes';
import { DebugController } from '../server/debugController';
export function printApiJson() { export function printApiJson() {
// Note: this file is generated by build-playwright-driver.sh // Note: this file is generated by build-playwright-driver.sh
@ -49,14 +46,12 @@ export function runDriver() {
}; };
} }
export async function runServer(port: number | undefined, path = '/', maxConnections = Infinity, enableSocksProxy = true, reuseBrowser = false) { export async function runServer(port: number | undefined, path = '/', maxConnections = Infinity, enableSocksProxy = true) {
const server = new PlaywrightServer({ path, maxConnections, enableSocksProxy }); const server = new PlaywrightServer({ path, maxConnections, enableSocksProxy });
const wsEndpoint = await server.listen(port); const wsEndpoint = await server.listen(port);
process.on('exit', () => server.close().catch(console.error)); process.on('exit', () => server.close().catch(console.error));
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
process.stdin.on('close', () => selfDestruct()); process.stdin.on('close', () => selfDestruct());
if (reuseBrowser && process.send)
wireController(server.preLaunchedPlaywright(), wsEndpoint);
} }
export async function launchBrowserServer(browserName: string, configFile?: string) { export async function launchBrowserServer(browserName: string, configFile?: string) {
@ -76,67 +71,3 @@ function selfDestruct() {
process.exit(0); process.exit(0);
}); });
} }
class ProtocolHandler {
private _controller: DebugController;
constructor(playwright: Playwright) {
this._controller = playwright.debugController;
this._controller.setAutoCloseAllowed(true);
this._controller.setReportStateChanged(true);
this._controller.on(DebugController.Events.BrowsersChanged, browsers => {
process.send!({ method: 'browsersChanged', params: { browsers } });
});
this._controller.on(DebugController.Events.InspectRequested, ({ selector, locators }) => {
process.send!({ method: 'inspectRequested', params: { selector, locators } });
});
this._controller.on(DebugController.Events.SourcesChanged, sources => {
process.send!({ method: 'sourcesChanged', params: { sources } });
});
}
async resetForReuse() {
await this._controller.resetForReuse();
}
async navigate(params: { url: string }) {
await this._controller.navigate(params.url);
}
async setMode(params: { mode: Mode, language?: string, file?: string }) {
await this._controller.setRecorderMode(params);
}
async setAutoClose(params: { enabled: boolean }) {
await this._controller.setAutoCloseEnabled(params.enabled);
}
async highlight(params: { selector: string }) {
await this._controller.highlight(params.selector);
}
async hideHighlight() {
await this._controller.hideHighlight();
}
async closeAllBrowsers() {
await this._controller.closeAllBrowsers();
}
async kill() {
await this._controller.kill();
}
}
function wireController(playwright: Playwright, wsEndpoint: string) {
process.send!({ method: 'ready', params: { wsEndpoint } });
const handler = new ProtocolHandler(playwright);
process.on('message', async message => {
try {
const result = await (handler as any)[message.method](message.params);
process.send!({ id: message.id, result });
} catch (e) {
process.send!({ id: message.id, error: e.toString() });
}
});
}

View File

@ -58,21 +58,21 @@ function determineUserAgent(): string {
additionalTokens.push('CI/1'); additionalTokens.push('CI/1');
const serializedTokens = additionalTokens.length ? ' ' + additionalTokens.join(' ') : ''; const serializedTokens = additionalTokens.length ? ' ' + additionalTokens.join(' ') : '';
const { langName, langVersion } = getClientLanguage(); const { embedderName, embedderVersion } = getEmbedderName();
return `Playwright/${getPlaywrightVersion()} (${os.arch()}; ${osIdentifier} ${osVersion}) ${langName}/${langVersion}${serializedTokens}`; return `Playwright/${getPlaywrightVersion()} (${os.arch()}; ${osIdentifier} ${osVersion}) ${embedderName}/${embedderVersion}${serializedTokens}`;
} }
export function getClientLanguage(): { langName: string, langVersion: string } { export function getEmbedderName(): { embedderName: string, embedderVersion: string } {
let langName = 'unknown'; let embedderName = 'unknown';
let langVersion = 'unknown'; let embedderVersion = 'unknown';
if (!process.env.PW_LANG_NAME) { if (!process.env.PW_LANG_NAME) {
langName = 'node'; embedderName = 'node';
langVersion = process.version.substring(1).split('.').slice(0, 2).join('.'); embedderVersion = process.version.substring(1).split('.').slice(0, 2).join('.');
} else if (['node', 'python', 'java', 'csharp'].includes(process.env.PW_LANG_NAME)) { } else if (['node', 'python', 'java', 'csharp'].includes(process.env.PW_LANG_NAME)) {
langName = process.env.PW_LANG_NAME; embedderName = process.env.PW_LANG_NAME;
langVersion = process.env.PW_LANG_NAME_VERSION ?? 'unknown'; embedderVersion = process.env.PW_LANG_NAME_VERSION ?? 'unknown';
} }
return { langName, langVersion }; return { embedderName, embedderVersion };
} }
export function getPlaywrightVersion(majorMinorOnly = false): string { export function getPlaywrightVersion(majorMinorOnly = false): string {

View File

@ -333,11 +333,14 @@ scheme.RecorderSource = tObject({
scheme.DebugControllerInitializer = tOptional(tObject({})); scheme.DebugControllerInitializer = tOptional(tObject({}));
scheme.DebugControllerInspectRequestedEvent = tObject({ scheme.DebugControllerInspectRequestedEvent = tObject({
selector: tString, selector: tString,
locators: tArray(tType('NameValue')), locator: tString,
}); });
scheme.DebugControllerStateChangedEvent = tObject({ scheme.DebugControllerStateChangedEvent = tObject({
pageCount: tNumber, pageCount: tNumber,
}); });
scheme.DebugControllerSourceChangedEvent = tObject({
text: tString,
});
scheme.DebugControllerBrowsersChangedEvent = tObject({ scheme.DebugControllerBrowsersChangedEvent = tObject({
browsers: tArray(tObject({ browsers: tArray(tObject({
contexts: tArray(tObject({ contexts: tArray(tObject({
@ -345,9 +348,11 @@ scheme.DebugControllerBrowsersChangedEvent = tObject({
})), })),
})), })),
}); });
scheme.DebugControllerSourcesChangedEvent = tObject({ scheme.DebugControllerInitializeParams = tObject({
sources: tArray(tType('RecorderSource')), codegenId: tString,
sdkLanguage: tEnum(['javascript', 'python', 'java', 'csharp']),
}); });
scheme.DebugControllerInitializeResult = tOptional(tObject({}));
scheme.DebugControllerSetReportStateChangedParams = tObject({ scheme.DebugControllerSetReportStateChangedParams = tObject({
enabled: tBoolean, enabled: tBoolean,
}); });
@ -360,8 +365,6 @@ scheme.DebugControllerNavigateParams = tObject({
scheme.DebugControllerNavigateResult = tOptional(tObject({})); scheme.DebugControllerNavigateResult = tOptional(tObject({}));
scheme.DebugControllerSetRecorderModeParams = tObject({ scheme.DebugControllerSetRecorderModeParams = tObject({
mode: tEnum(['inspecting', 'recording', 'none']), mode: tEnum(['inspecting', 'recording', 'none']),
language: tOptional(tString),
file: tOptional(tString),
}); });
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({})); scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
scheme.DebugControllerHighlightParams = tObject({ scheme.DebugControllerHighlightParams = tObject({

View File

@ -25,7 +25,6 @@ import { Recorder } from './recorder';
import { EmptyRecorderApp } from './recorder/recorderApp'; import { EmptyRecorderApp } from './recorder/recorderApp';
import { asLocator } from './isomorphic/locatorGenerators'; import { asLocator } from './isomorphic/locatorGenerators';
import type { Language } from './isomorphic/locatorGenerators'; import type { Language } from './isomorphic/locatorGenerators';
import type { NameValue } from '../common/types';
const internalMetadata = serverSideCallMetadata(); const internalMetadata = serverSideCallMetadata();
@ -34,7 +33,7 @@ export class DebugController extends SdkObject {
BrowsersChanged: 'browsersChanged', BrowsersChanged: 'browsersChanged',
StateChanged: 'stateChanged', StateChanged: 'stateChanged',
InspectRequested: 'inspectRequested', InspectRequested: 'inspectRequested',
SourcesChanged: 'sourcesChanged', SourceChanged: 'sourceChanged',
}; };
private _autoCloseTimer: NodeJS.Timeout | undefined; private _autoCloseTimer: NodeJS.Timeout | undefined;
@ -42,12 +41,19 @@ export class DebugController extends SdkObject {
private _autoCloseAllowed = false; private _autoCloseAllowed = false;
private _trackHierarchyListener: InstrumentationListener | undefined; private _trackHierarchyListener: InstrumentationListener | undefined;
private _playwright: Playwright; private _playwright: Playwright;
_sdkLanguage: Language = 'javascript';
_codegenId: string = 'playwright-test';
constructor(playwright: Playwright) { constructor(playwright: Playwright) {
super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'DebugController'); super({ attribution: { isInternalPlaywright: true }, instrumentation: createInstrumentation() } as any, undefined, 'DebugController');
this._playwright = playwright; this._playwright = playwright;
} }
initialize(codegenId: string, sdkLanguage: Language) {
this._codegenId = codegenId;
this._sdkLanguage = sdkLanguage;
}
setAutoCloseAllowed(allowed: boolean) { setAutoCloseAllowed(allowed: boolean) {
this._autoCloseAllowed = allowed; this._autoCloseAllowed = allowed;
} }
@ -83,7 +89,8 @@ export class DebugController extends SdkObject {
await p.mainFrame().goto(internalMetadata, url); await p.mainFrame().goto(internalMetadata, url);
} }
async setRecorderMode(params: { mode: Mode, language?: string, file?: string }) { async setRecorderMode(params: { mode: Mode, file?: string }) {
// TODO: |file| is only used in the legacy mode.
await this._closeBrowsersWithoutPages(); await this._closeBrowsersWithoutPages();
if (params.mode === 'none') { if (params.mode === 'none') {
@ -108,7 +115,7 @@ export class DebugController extends SdkObject {
for (const recorder of await this._allRecorders()) { for (const recorder of await this._allRecorders()) {
recorder.setHighlightedSelector(''); recorder.setHighlightedSelector('');
if (params.mode === 'recording') if (params.mode === 'recording')
recorder.setOutput(params.language!, params.file); recorder.setOutput(this._codegenId, params.file);
recorder.setMode(params.mode); recorder.setMode(params.mode);
} }
this.setAutoCloseEnabled(true); this.setAutoCloseEnabled(true);
@ -216,11 +223,12 @@ class InspectingRecorderApp extends EmptyRecorderApp {
} }
override async setSelector(selector: string): Promise<void> { override async setSelector(selector: string): Promise<void> {
const locators: NameValue[] = ['javascript', 'python', 'java', 'csharp'].map(l => ({ name: l, value: asLocator(l as Language, selector) })); const locator: string = asLocator(this._debugController._sdkLanguage, selector);
this._debugController.emit(DebugController.Events.InspectRequested, { selector, locators }); this._debugController.emit(DebugController.Events.InspectRequested, { selector, locator });
} }
override async setSources(sources: Source[]): Promise<void> { override async setSources(sources: Source[]): Promise<void> {
this._debugController.emit(DebugController.Events.SourcesChanged, sources); const source = sources.find(s => s.id === this._debugController._codegenId);
this._debugController.emit(DebugController.Events.SourceChanged, source?.text || '');
} }
} }

View File

@ -28,14 +28,18 @@ export class DebugControllerDispatcher extends Dispatcher<DebugController, chann
this._object.on(DebugController.Events.StateChanged, params => { this._object.on(DebugController.Events.StateChanged, params => {
this._dispatchEvent('stateChanged', params); this._dispatchEvent('stateChanged', params);
}); });
this._object.on(DebugController.Events.InspectRequested, ({ selector, locators }) => { this._object.on(DebugController.Events.InspectRequested, ({ selector, locator }) => {
this._dispatchEvent('inspectRequested', { selector, locators }); this._dispatchEvent('inspectRequested', { selector, locator });
}); });
this._object.on(DebugController.Events.SourcesChanged, sources => { this._object.on(DebugController.Events.SourceChanged, text => {
this._dispatchEvent('sourcesChanged', { sources }); this._dispatchEvent('sourceChanged', { text });
}); });
} }
async initialize(params: channels.DebugControllerInitializeParams) {
this._object.initialize(params.codegenId, params.sdkLanguage);
}
async setReportStateChanged(params: channels.DebugControllerSetReportStateChangedParams) { async setReportStateChanged(params: channels.DebugControllerSetReportStateChangedParams) {
this._object.setReportStateChanged(params.enabled); this._object.setReportStateChanged(params.enabled);
} }

View File

@ -214,8 +214,8 @@ export class Recorder implements InstrumentationListener {
this._refreshOverlay(); this._refreshOverlay();
} }
setOutput(language: string, outputFile: string | undefined) { setOutput(codegenId: string, outputFile: string | undefined) {
this._contextRecorder.setOutput(language, outputFile); this._contextRecorder.setOutput(codegenId, outputFile);
} }
private _refreshOverlay() { private _refreshOverlay() {
@ -367,7 +367,7 @@ class ContextRecorder extends EventEmitter {
this._generator = generator; this._generator = generator;
} }
setOutput(language: string, outputFile: string | undefined) { setOutput(codegenId: string, outputFile?: string) {
const languages = new Set([ const languages = new Set([
new JavaLanguageGenerator(), new JavaLanguageGenerator(),
new JavaScriptLanguageGenerator(/* isPlaywrightTest */false), new JavaScriptLanguageGenerator(/* isPlaywrightTest */false),
@ -379,10 +379,9 @@ class ContextRecorder extends EventEmitter {
new CSharpLanguageGenerator('nunit'), new CSharpLanguageGenerator('nunit'),
new CSharpLanguageGenerator('library'), new CSharpLanguageGenerator('library'),
]); ]);
const primaryLanguage = [...languages].find(l => l.id === language); const primaryLanguage = [...languages].find(l => l.id === codegenId);
if (!primaryLanguage) if (!primaryLanguage)
throw new Error(`\n===============================\nUnsupported language: '${language}'\n===============================\n`); throw new Error(`\n===============================\nUnsupported language: '${codegenId}'\n===============================\n`);
languages.delete(primaryLanguage); languages.delete(primaryLanguage);
this._orderedLanguages = [primaryLanguage, ...languages]; this._orderedLanguages = [primaryLanguage, ...languages];
this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null; this._throttledOutputFile = outputFile ? new ThrottledFile(outputFile) : null;

View File

@ -33,7 +33,7 @@ export class JavaScriptLanguageGenerator implements LanguageGenerator {
private _isTest: boolean; private _isTest: boolean;
constructor(isTest: boolean) { constructor(isTest: boolean) {
this.id = isTest ? 'test' : 'javascript'; this.id = isTest ? 'playwright-test' : 'javascript';
this.name = isTest ? 'Test Runner' : 'Library'; this.name = isTest ? 'Test Runner' : 'Library';
this._isTest = isTest; this._isTest = isTest;
} }
@ -175,7 +175,7 @@ ${useText ? '\ntest.use(' + useText + ');\n' : ''}
} }
generateTestFooter(saveStorage: string | undefined): string { generateTestFooter(saveStorage: string | undefined): string {
return `\n});`; return `});`;
} }
generateStandaloneHeader(options: LanguageGeneratorOptions): string { generateStandaloneHeader(options: LanguageGeneratorOptions): string {

View File

@ -37,7 +37,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
private _isPyTest: boolean; private _isPyTest: boolean;
constructor(isAsync: boolean, isPyTest: boolean) { constructor(isAsync: boolean, isPyTest: boolean) {
this.id = isPyTest ? 'pytest' : (isAsync ? 'python-async' : 'python'); this.id = isPyTest ? 'python-pytest' : (isAsync ? 'python-async' : 'python');
this.name = isPyTest ? 'Pytest' : (isAsync ? 'Library Async' : 'Library'); this.name = isPyTest ? 'Pytest' : (isAsync ? 'Library Async' : 'Library');
this._isAsync = isAsync; this._isAsync = isAsync;
this._isPyTest = isPyTest; this._isPyTest = isPyTest;

View File

@ -22,7 +22,7 @@ import * as fs from 'fs';
import { lockfile } from '../../utilsBundle'; import { lockfile } from '../../utilsBundle';
import { getLinuxDistributionInfo } from '../../utils/linuxUtils'; import { getLinuxDistributionInfo } from '../../utils/linuxUtils';
import { fetchData } from '../../common/netUtils'; import { fetchData } from '../../common/netUtils';
import { getClientLanguage } from '../../common/userAgent'; import { getEmbedderName } from '../../common/userAgent';
import { getFromENV, getAsBooleanFromENV, calculateSha1, wrapInASCIIBox } from '../../utils'; import { getFromENV, getAsBooleanFromENV, calculateSha1, wrapInASCIIBox } from '../../utils';
import { removeFolders, existsAsync, canAccessFile } from '../../utils/fileUtils'; import { removeFolders, existsAsync, canAccessFile } from '../../utils/fileUtils';
import { hostPlatform } from '../../utils/hostPlatform'; import { hostPlatform } from '../../utils/hostPlatform';
@ -696,9 +696,9 @@ export class Registry {
if (!executable._install) if (!executable._install)
throw new Error(`ERROR: Playwright does not support installing ${executable.name}`); throw new Error(`ERROR: Playwright does not support installing ${executable.name}`);
const { langName } = getClientLanguage(); const { embedderName } = getEmbedderName();
if (!getAsBooleanFromENV('CI') && !executable._isHermeticInstallation && !forceReinstall && executable.executablePath(langName)) { if (!getAsBooleanFromENV('CI') && !executable._isHermeticInstallation && !forceReinstall && executable.executablePath(embedderName)) {
const command = buildPlaywrightCLICommand(langName, 'install --force ' + executable.name); const command = buildPlaywrightCLICommand(embedderName, 'install --force ' + executable.name);
throw new Error('\n' + wrapInASCIIBox([ throw new Error('\n' + wrapInASCIIBox([
`ATTENTION: "${executable.name}" is already installed on the system!`, `ATTENTION: "${executable.name}" is already installed on the system!`,
``, ``,

View File

@ -591,11 +591,12 @@ export type DebugControllerInitializer = {};
export interface DebugControllerEventTarget { export interface DebugControllerEventTarget {
on(event: 'inspectRequested', callback: (params: DebugControllerInspectRequestedEvent) => void): this; on(event: 'inspectRequested', callback: (params: DebugControllerInspectRequestedEvent) => void): this;
on(event: 'stateChanged', callback: (params: DebugControllerStateChangedEvent) => void): this; on(event: 'stateChanged', callback: (params: DebugControllerStateChangedEvent) => void): this;
on(event: 'sourceChanged', callback: (params: DebugControllerSourceChangedEvent) => void): this;
on(event: 'browsersChanged', callback: (params: DebugControllerBrowsersChangedEvent) => void): this; on(event: 'browsersChanged', callback: (params: DebugControllerBrowsersChangedEvent) => void): this;
on(event: 'sourcesChanged', callback: (params: DebugControllerSourcesChangedEvent) => void): this;
} }
export interface DebugControllerChannel extends DebugControllerEventTarget, Channel { export interface DebugControllerChannel extends DebugControllerEventTarget, Channel {
_type_DebugController: boolean; _type_DebugController: boolean;
initialize(params: DebugControllerInitializeParams, metadata?: Metadata): Promise<DebugControllerInitializeResult>;
setReportStateChanged(params: DebugControllerSetReportStateChangedParams, metadata?: Metadata): Promise<DebugControllerSetReportStateChangedResult>; setReportStateChanged(params: DebugControllerSetReportStateChangedParams, metadata?: Metadata): Promise<DebugControllerSetReportStateChangedResult>;
resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise<DebugControllerResetForReuseResult>; resetForReuse(params?: DebugControllerResetForReuseParams, metadata?: Metadata): Promise<DebugControllerResetForReuseResult>;
navigate(params: DebugControllerNavigateParams, metadata?: Metadata): Promise<DebugControllerNavigateResult>; navigate(params: DebugControllerNavigateParams, metadata?: Metadata): Promise<DebugControllerNavigateResult>;
@ -607,11 +608,14 @@ export interface DebugControllerChannel extends DebugControllerEventTarget, Chan
} }
export type DebugControllerInspectRequestedEvent = { export type DebugControllerInspectRequestedEvent = {
selector: string, selector: string,
locators: NameValue[], locator: string,
}; };
export type DebugControllerStateChangedEvent = { export type DebugControllerStateChangedEvent = {
pageCount: number, pageCount: number,
}; };
export type DebugControllerSourceChangedEvent = {
text: string,
};
export type DebugControllerBrowsersChangedEvent = { export type DebugControllerBrowsersChangedEvent = {
browsers: { browsers: {
contexts: { contexts: {
@ -619,9 +623,14 @@ export type DebugControllerBrowsersChangedEvent = {
}[], }[],
}[], }[],
}; };
export type DebugControllerSourcesChangedEvent = { export type DebugControllerInitializeParams = {
sources: RecorderSource[], codegenId: string,
sdkLanguage: 'javascript' | 'python' | 'java' | 'csharp',
}; };
export type DebugControllerInitializeOptions = {
};
export type DebugControllerInitializeResult = void;
export type DebugControllerSetReportStateChangedParams = { export type DebugControllerSetReportStateChangedParams = {
enabled: boolean, enabled: boolean,
}; };
@ -641,12 +650,9 @@ export type DebugControllerNavigateOptions = {
export type DebugControllerNavigateResult = void; export type DebugControllerNavigateResult = void;
export type DebugControllerSetRecorderModeParams = { export type DebugControllerSetRecorderModeParams = {
mode: 'inspecting' | 'recording' | 'none', mode: 'inspecting' | 'recording' | 'none',
language?: string,
file?: string,
}; };
export type DebugControllerSetRecorderModeOptions = { export type DebugControllerSetRecorderModeOptions = {
language?: string,
file?: string,
}; };
export type DebugControllerSetRecorderModeResult = void; export type DebugControllerSetRecorderModeResult = void;
export type DebugControllerHighlightParams = { export type DebugControllerHighlightParams = {
@ -669,8 +675,8 @@ export type DebugControllerCloseAllBrowsersResult = void;
export interface DebugControllerEvents { export interface DebugControllerEvents {
'inspectRequested': DebugControllerInspectRequestedEvent; 'inspectRequested': DebugControllerInspectRequestedEvent;
'stateChanged': DebugControllerStateChangedEvent; 'stateChanged': DebugControllerStateChangedEvent;
'sourceChanged': DebugControllerSourceChangedEvent;
'browsersChanged': DebugControllerBrowsersChangedEvent; 'browsersChanged': DebugControllerBrowsersChangedEvent;
'sourcesChanged': DebugControllerSourcesChangedEvent;
} }
// ----------- SocksSupport ----------- // ----------- SocksSupport -----------

View File

@ -660,6 +660,17 @@ DebugController:
type: interface type: interface
commands: commands:
initialize:
parameters:
codegenId: string
sdkLanguage:
type: enum
literals:
- javascript
- python
- java
- csharp
setReportStateChanged: setReportStateChanged:
parameters: parameters:
enabled: boolean enabled: boolean
@ -678,8 +689,6 @@ DebugController:
- inspecting - inspecting
- recording - recording
- none - none
language: string?
file: string?
highlight: highlight:
parameters: parameters:
@ -695,14 +704,16 @@ DebugController:
inspectRequested: inspectRequested:
parameters: parameters:
selector: string selector: string
locators: locator: string
type: array
items: NameValue
stateChanged: stateChanged:
parameters: parameters:
pageCount: number pageCount: number
sourceChanged:
parameters:
text: string
# Deprecated # Deprecated
browsersChanged: browsersChanged:
parameters: parameters:
@ -720,12 +731,6 @@ DebugController:
type: array type: array
items: string items: string
sourcesChanged:
parameters:
sources:
type: array
items: RecorderSource
SocksSupport: SocksSupport:
type: interface type: interface

View File

@ -70,20 +70,10 @@ test('should pick element', async ({ backend, connectedBrowser }) => {
expect(events).toEqual([ expect(events).toEqual([
{ {
selector: 'body', selector: 'body',
locators: [ locator: 'locator(\'body\')',
{ name: 'javascript', value: 'locator(\'body\')' },
{ name: 'python', value: 'locator("body")' },
{ name: 'java', value: 'locator("body")' },
{ name: 'csharp', value: 'Locator("body")' }
]
}, { }, {
selector: 'body', selector: 'body',
locators: [ locator: 'locator(\'body\')',
{ name: 'javascript', value: 'locator(\'body\')' },
{ name: 'python', value: 'locator("body")' },
{ name: 'java', value: 'locator("body")' },
{ name: 'csharp', value: 'Locator("body")' }
]
}, },
]); ]);
@ -156,3 +146,29 @@ test('should highlight all', async ({ backend, connectedBrowser }) => {
await expect(page1.getByText('locator(\'button\')')).toBeHidden({ timeout: 1000000 }); await expect(page1.getByText('locator(\'button\')')).toBeHidden({ timeout: 1000000 });
await expect(page2.getByText('locator(\'button\')')).toBeHidden(); await expect(page2.getByText('locator(\'button\')')).toBeHidden();
}); });
test('should record', async ({ backend, connectedBrowser }) => {
const events = [];
backend.on('sourceChanged', event => events.push(event));
await backend.setMode({ mode: 'recording' });
const context = await connectedBrowser._newContextForReuse();
const [page] = context.pages();
await page.locator('body').click();
await expect.poll(() => events[events.length - 1]).toEqual({
text: `import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
await page.goto('about:blank');
await page.locator('body').click();
});`
});
const length = events.length;
// No events after mode disabled
await backend.setMode({ mode: 'none' });
await page.locator('body').click();
expect(events).toHaveLength(length);
});

View File

@ -21,7 +21,7 @@ import { test, expect } from './inspectorTest';
const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString(); const emptyHTML = new URL('file://' + path.join(__dirname, '..', '..', 'assets', 'empty.html')).toString();
test('should print the correct imports and context options', async ({ runCLI }) => { test('should print the correct imports and context options', async ({ runCLI }) => {
const cli = runCLI(['--target=pytest', emptyHTML]); const cli = runCLI(['--target=python-pytest', emptyHTML]);
const expectedResult = `from playwright.sync_api import Page, expect const expectedResult = `from playwright.sync_api import Page, expect
@ -34,7 +34,7 @@ test('should print the correct context options when using a device and lang', as
test.skip(browserName !== 'webkit'); test.skip(browserName !== 'webkit');
const tmpFile = testInfo.outputPath('script.js'); const tmpFile = testInfo.outputPath('script.js');
const cli = runCLI(['--target=pytest', '--device=iPhone 11', '--lang=en-US', '--output', tmpFile, emptyHTML]); const cli = runCLI(['--target=python-pytest', '--device=iPhone 11', '--lang=en-US', '--output', tmpFile, emptyHTML]);
await cli.exited; await cli.exited;
const content = fs.readFileSync(tmpFile); const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`import pytest expect(content.toString()).toBe(`import pytest
@ -54,7 +54,7 @@ def test_example(page: Page) -> None:
test('should save the codegen output to a file if specified', async ({ runCLI }, testInfo) => { test('should save the codegen output to a file if specified', async ({ runCLI }, testInfo) => {
const tmpFile = testInfo.outputPath('test_example.py'); const tmpFile = testInfo.outputPath('test_example.py');
const cli = runCLI(['--target=pytest', '--output', tmpFile, emptyHTML]); const cli = runCLI(['--target=python-pytest', '--output', tmpFile, emptyHTML]);
await cli.exited; await cli.exited;
const content = fs.readFileSync(tmpFile); const content = fs.readFileSync(tmpFile);
expect(content.toString()).toBe(`from playwright.sync_api import Page, expect expect(content.toString()).toBe(`from playwright.sync_api import Page, expect

View File

@ -25,7 +25,6 @@ test('should print the correct imports and context options', async ({ runCLI })
const expectedResult = `import { test, expect } from '@playwright/test'; const expectedResult = `import { test, expect } from '@playwright/test';
test('test', async ({ page }) => { test('test', async ({ page }) => {
});`; });`;
await cli.waitFor(expectedResult); await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult); expect(cli.text()).toContain(expectedResult);
@ -93,7 +92,7 @@ test('test', async ({ page }) => {`;
test('should work with --save-har', async ({ runCLI }, testInfo) => { test('should work with --save-har', async ({ runCLI }, testInfo) => {
const harFileName = testInfo.outputPath('har.har'); const harFileName = testInfo.outputPath('har.har');
const cli = runCLI(['--target=test', `--save-har=${harFileName}`]); const cli = runCLI(['--target=playwright-test', `--save-har=${harFileName}`]);
const expectedResult = ` const expectedResult = `
recordHar: { recordHar: {
mode: 'minimal', mode: 'minimal',

View File

@ -33,11 +33,11 @@ const codegenLang2Id: Map<string, string> = new Map([
['Java', 'java'], ['Java', 'java'],
['Python', 'python'], ['Python', 'python'],
['Python Async', 'python-async'], ['Python Async', 'python-async'],
['Pytest', 'pytest'], ['Pytest', 'python-pytest'],
['C#', 'csharp'], ['C#', 'csharp'],
['C# NUnit', 'csharp-nunit'], ['C# NUnit', 'csharp-nunit'],
['C# MSTest', 'csharp-mstest'], ['C# MSTest', 'csharp-mstest'],
['Playwright Test', 'test'], ['Playwright Test', 'playwright-test'],
]); ]);
const codegenLangId2lang = new Map([...codegenLang2Id.entries()].map(([lang, langId]) => [langId, lang])); const codegenLangId2lang = new Map([...codegenLang2Id.entries()].map(([lang, langId]) => [langId, lang]));