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', [])
.action(function(url, options) {
open(options, url, language()).catch(logErrorAndExit);
open(options, url, codegenId()).catch(logErrorAndExit);
})
.addHelpText('afterAll', `
Examples:
@ -68,7 +68,7 @@ Examples:
commandWithOpenOptions('codegen [url]', 'open page and generate code for user actions',
[
['-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'],
]).action(function(url, options) {
codegen(options, url, options.target, options.output).catch(logErrorAndExit);
@ -267,13 +267,12 @@ program
program
.command('run-server', { hidden: true })
.option('--reuse-browser', 'Whether to reuse the browser instance')
.option('--port <port>', 'Server port')
.option('--path <path>', 'Endpoint Path', '/')
.option('--max-clients <maxClients>', 'Maximum clients')
.option('--no-socks-proxy', 'Disable Socks Proxy')
.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
@ -682,8 +681,8 @@ function logErrorAndExit(e: Error) {
process.exit(1);
}
function language(): string {
return process.env.PW_LANG_NAME || 'test';
function codegenId(): string {
return process.env.PW_LANG_NAME || 'playwright-test';
}
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 { LaunchServerOptions } from '../client/types';
import { createPlaywright, DispatcherConnection, RootDispatcher, PlaywrightDispatcher } from '../server';
import type { Playwright } from '../server/playwright';
import { IpcTransport, PipeTransport } from '../protocol/transport';
import { PlaywrightServer } from '../remote/playwrightServer';
import { gracefullyCloseAll } from '../utils/processLauncher';
import type { Mode } from '@recorder/recorderTypes';
import { DebugController } from '../server/debugController';
export function printApiJson() {
// 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 wsEndpoint = await server.listen(port);
process.on('exit', () => server.close().catch(console.error));
console.log('Listening on ' + wsEndpoint); // eslint-disable-line no-console
process.stdin.on('close', () => selfDestruct());
if (reuseBrowser && process.send)
wireController(server.preLaunchedPlaywright(), wsEndpoint);
}
export async function launchBrowserServer(browserName: string, configFile?: string) {
@ -76,67 +71,3 @@ function selfDestruct() {
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');
const serializedTokens = additionalTokens.length ? ' ' + additionalTokens.join(' ') : '';
const { langName, langVersion } = getClientLanguage();
return `Playwright/${getPlaywrightVersion()} (${os.arch()}; ${osIdentifier} ${osVersion}) ${langName}/${langVersion}${serializedTokens}`;
const { embedderName, embedderVersion } = getEmbedderName();
return `Playwright/${getPlaywrightVersion()} (${os.arch()}; ${osIdentifier} ${osVersion}) ${embedderName}/${embedderVersion}${serializedTokens}`;
}
export function getClientLanguage(): { langName: string, langVersion: string } {
let langName = 'unknown';
let langVersion = 'unknown';
export function getEmbedderName(): { embedderName: string, embedderVersion: string } {
let embedderName = 'unknown';
let embedderVersion = 'unknown';
if (!process.env.PW_LANG_NAME) {
langName = 'node';
langVersion = process.version.substring(1).split('.').slice(0, 2).join('.');
embedderName = 'node';
embedderVersion = process.version.substring(1).split('.').slice(0, 2).join('.');
} else if (['node', 'python', 'java', 'csharp'].includes(process.env.PW_LANG_NAME)) {
langName = process.env.PW_LANG_NAME;
langVersion = process.env.PW_LANG_NAME_VERSION ?? 'unknown';
embedderName = process.env.PW_LANG_NAME;
embedderVersion = process.env.PW_LANG_NAME_VERSION ?? 'unknown';
}
return { langName, langVersion };
return { embedderName, embedderVersion };
}
export function getPlaywrightVersion(majorMinorOnly = false): string {

View File

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

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ export class PythonLanguageGenerator implements LanguageGenerator {
private _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._isAsync = isAsync;
this._isPyTest = isPyTest;

View File

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

View File

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

View File

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

View File

@ -70,20 +70,10 @@ test('should pick element', async ({ backend, connectedBrowser }) => {
expect(events).toEqual([
{
selector: 'body',
locators: [
{ name: 'javascript', value: 'locator(\'body\')' },
{ name: 'python', value: 'locator("body")' },
{ name: 'java', value: 'locator("body")' },
{ name: 'csharp', value: 'Locator("body")' }
]
locator: 'locator(\'body\')',
}, {
selector: 'body',
locators: [
{ name: 'javascript', value: 'locator(\'body\')' },
{ name: 'python', value: 'locator("body")' },
{ name: 'java', value: 'locator("body")' },
{ name: 'csharp', value: 'Locator("body")' }
]
locator: 'locator(\'body\')',
},
]);
@ -156,3 +146,29 @@ test('should highlight all', async ({ backend, connectedBrowser }) => {
await expect(page1.getByText('locator(\'button\')')).toBeHidden({ timeout: 1000000 });
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();
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
@ -34,7 +34,7 @@ test('should print the correct context options when using a device and lang', as
test.skip(browserName !== 'webkit');
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;
const content = fs.readFileSync(tmpFile);
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) => {
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;
const content = fs.readFileSync(tmpFile);
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';
test('test', async ({ page }) => {
});`;
await cli.waitFor(expectedResult);
expect(cli.text()).toContain(expectedResult);
@ -93,7 +92,7 @@ test('test', async ({ page }) => {`;
test('should work with --save-har', async ({ runCLI }, testInfo) => {
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 = `
recordHar: {
mode: 'minimal',

View File

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