chore(internal): generate code in jsonl (#23124)
This commit is contained in:
parent
33f3a6002d
commit
631edc9744
|
|
@ -28,7 +28,7 @@ test('should render counters', async ({ mount }) => {
|
||||||
skipped: 10,
|
skipped: 10,
|
||||||
ok: false,
|
ok: false,
|
||||||
duration: 100000
|
duration: 100000
|
||||||
}} filterText='' setFilterText={() => {}} projectNames={[]}></HeaderView>);
|
}} filterText='' setFilterText={() => {}}></HeaderView>);
|
||||||
await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100');
|
await expect(component.locator('a', { hasText: 'All' }).locator('.counter')).toHaveText('100');
|
||||||
await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42');
|
await expect(component.locator('a', { hasText: 'Passed' }).locator('.counter')).toHaveText('42');
|
||||||
await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
|
await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import { Frame } from './frames';
|
||||||
import { BrowserContext } from './browserContext';
|
import { BrowserContext } from './browserContext';
|
||||||
import { JavaLanguageGenerator } from './recorder/java';
|
import { JavaLanguageGenerator } from './recorder/java';
|
||||||
import { JavaScriptLanguageGenerator } from './recorder/javascript';
|
import { JavaScriptLanguageGenerator } from './recorder/javascript';
|
||||||
|
import { JsonlLanguageGenerator } from './recorder/jsonl';
|
||||||
import { CSharpLanguageGenerator } from './recorder/csharp';
|
import { CSharpLanguageGenerator } from './recorder/csharp';
|
||||||
import { PythonLanguageGenerator } from './recorder/python';
|
import { PythonLanguageGenerator } from './recorder/python';
|
||||||
import * as recorderSource from '../generated/recorderSource';
|
import * as recorderSource from '../generated/recorderSource';
|
||||||
|
|
@ -406,6 +407,7 @@ class ContextRecorder extends EventEmitter {
|
||||||
new CSharpLanguageGenerator('mstest'),
|
new CSharpLanguageGenerator('mstest'),
|
||||||
new CSharpLanguageGenerator('nunit'),
|
new CSharpLanguageGenerator('nunit'),
|
||||||
new CSharpLanguageGenerator('library'),
|
new CSharpLanguageGenerator('library'),
|
||||||
|
new JsonlLanguageGenerator(),
|
||||||
]);
|
]);
|
||||||
const primaryLanguage = [...languages].find(l => l.id === codegenId);
|
const primaryLanguage = [...languages].find(l => l.id === codegenId);
|
||||||
if (!primaryLanguage)
|
if (!primaryLanguage)
|
||||||
|
|
|
||||||
44
packages/playwright-core/src/server/recorder/jsonl.ts
Normal file
44
packages/playwright-core/src/server/recorder/jsonl.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* 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 { asLocator } from '../../utils/isomorphic/locatorGenerators';
|
||||||
|
import type { ActionInContext } from './codeGenerator';
|
||||||
|
import type { Language, LanguageGenerator, LanguageGeneratorOptions } from './language';
|
||||||
|
|
||||||
|
export class JsonlLanguageGenerator implements LanguageGenerator {
|
||||||
|
id = 'jsonl';
|
||||||
|
groupName = '';
|
||||||
|
name = 'JSONL';
|
||||||
|
highlighter = 'javascript' as Language;
|
||||||
|
|
||||||
|
generateAction(actionInContext: ActionInContext): string {
|
||||||
|
const locator = (actionInContext.action as any).selector ? JSON.parse(asLocator('jsonl', (actionInContext.action as any).selector)) : undefined;
|
||||||
|
const entry = {
|
||||||
|
...actionInContext.action,
|
||||||
|
pageAlias: actionInContext.frame.pageAlias,
|
||||||
|
locator,
|
||||||
|
};
|
||||||
|
return JSON.stringify(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateHeader(options: LanguageGeneratorOptions): string {
|
||||||
|
return JSON.stringify(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateFooter(saveStorage: string | undefined): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,7 +18,7 @@ import { escapeWithQuotes, toSnakeCase, toTitleCase } from './stringUtils';
|
||||||
import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringifySelector } from './selectorParser';
|
import { type NestedSelectorBody, parseAttributeSelector, parseSelector, stringifySelector } from './selectorParser';
|
||||||
import type { ParsedSelector } from './selectorParser';
|
import type { ParsedSelector } from './selectorParser';
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||||
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'and' | 'or';
|
export type LocatorType = 'default' | 'role' | 'text' | 'label' | 'placeholder' | 'alt' | 'title' | 'test-id' | 'nth' | 'first' | 'last' | 'has-text' | 'has-not-text' | 'has' | 'hasNot' | 'frame' | 'and' | 'or';
|
||||||
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
export type LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||||
|
|
||||||
|
|
@ -31,6 +31,7 @@ type LocatorOptions = {
|
||||||
};
|
};
|
||||||
export interface LocatorFactory {
|
export interface LocatorFactory {
|
||||||
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: LocatorOptions): string;
|
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options?: LocatorOptions): string;
|
||||||
|
chainLocators(locators: string[]): string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false, playSafe: boolean = false): string {
|
export function asLocator(lang: Language, selector: string, isFrameLocator: boolean = false, playSafe: boolean = false): string {
|
||||||
|
|
@ -191,7 +192,7 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
||||||
// Two options:
|
// Two options:
|
||||||
// - locator('div').filter({ hasText: 'foo' })
|
// - locator('div').filter({ hasText: 'foo' })
|
||||||
// - locator('div', { hasText: 'foo' })
|
// - locator('div', { hasText: 'foo' })
|
||||||
tokens.push([locatorPart + '.' + nextLocatorPart, combinedPart]);
|
tokens.push([factory.chainLocators([locatorPart, nextLocatorPart]), combinedPart]);
|
||||||
index++;
|
index++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -200,16 +201,16 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
||||||
tokens.push([locatorPart]);
|
tokens.push([locatorPart]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return combineTokens(tokens, maxOutputSize);
|
return combineTokens(factory, tokens, maxOutputSize);
|
||||||
}
|
}
|
||||||
|
|
||||||
function combineTokens(tokens: string[][], maxOutputSize: number): string[] {
|
function combineTokens(factory: LocatorFactory, tokens: string[][], maxOutputSize: number): string[] {
|
||||||
const currentTokens = tokens.map(() => '');
|
const currentTokens = tokens.map(() => '');
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
|
|
||||||
const visit = (index: number) => {
|
const visit = (index: number) => {
|
||||||
if (index === tokens.length) {
|
if (index === tokens.length) {
|
||||||
result.push(currentTokens.join('.'));
|
result.push(factory.chainLocators(currentTokens));
|
||||||
return currentTokens.length < maxOutputSize;
|
return currentTokens.length < maxOutputSize;
|
||||||
}
|
}
|
||||||
for (const taken of tokens[index]) {
|
for (const taken of tokens[index]) {
|
||||||
|
|
@ -301,6 +302,10 @@ export class JavaScriptLocatorFactory implements LocatorFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chainLocators(locators: string[]): string {
|
||||||
|
return locators.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
private toCallWithExact(method: string, body: string | RegExp, exact?: boolean) {
|
private toCallWithExact(method: string, body: string | RegExp, exact?: boolean) {
|
||||||
if (isRegExp(body))
|
if (isRegExp(body))
|
||||||
return `${method}(${body})`;
|
return `${method}(${body})`;
|
||||||
|
|
@ -381,6 +386,10 @@ export class PythonLocatorFactory implements LocatorFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chainLocators(locators: string[]): string {
|
||||||
|
return locators.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
private regexToString(body: RegExp) {
|
private regexToString(body: RegExp) {
|
||||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||||
return `re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix})`;
|
return `re.compile(r"${body.source.replace(/\\\//, '/').replace(/"/g, '\\"')}"${suffix})`;
|
||||||
|
|
@ -470,6 +479,10 @@ export class JavaLocatorFactory implements LocatorFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chainLocators(locators: string[]): string {
|
||||||
|
return locators.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
private regexToString(body: RegExp) {
|
private regexToString(body: RegExp) {
|
||||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||||
return `Pattern.compile(${this.quote(body.source)}${suffix})`;
|
return `Pattern.compile(${this.quote(body.source)}${suffix})`;
|
||||||
|
|
@ -553,6 +566,10 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chainLocators(locators: string[]): string {
|
||||||
|
return locators.join('.');
|
||||||
|
}
|
||||||
|
|
||||||
private regexToString(body: RegExp): string {
|
private regexToString(body: RegExp): string {
|
||||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||||
return `new Regex(${this.quote(body.source)}${suffix})`;
|
return `new Regex(${this.quote(body.source)}${suffix})`;
|
||||||
|
|
@ -583,11 +600,29 @@ export class CSharpLocatorFactory implements LocatorFactory {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class JsonlLocatorFactory implements LocatorFactory {
|
||||||
|
generateLocator(base: LocatorBase, kind: LocatorType, body: string | RegExp, options: LocatorOptions = {}): string {
|
||||||
|
return JSON.stringify({
|
||||||
|
kind,
|
||||||
|
body,
|
||||||
|
options,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
chainLocators(locators: string[]): string {
|
||||||
|
const objects = locators.map(l => JSON.parse(l));
|
||||||
|
for (let i = 0; i < objects.length - 1; ++i)
|
||||||
|
objects[i].next = objects[i + 1];
|
||||||
|
return JSON.stringify(objects[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const generators: Record<Language, LocatorFactory> = {
|
const generators: Record<Language, LocatorFactory> = {
|
||||||
javascript: new JavaScriptLocatorFactory(),
|
javascript: new JavaScriptLocatorFactory(),
|
||||||
python: new PythonLocatorFactory(),
|
python: new PythonLocatorFactory(),
|
||||||
java: new JavaLocatorFactory(),
|
java: new JavaLocatorFactory(),
|
||||||
csharp: new CSharpLocatorFactory(),
|
csharp: new CSharpLocatorFactory(),
|
||||||
|
jsonl: new JsonlLocatorFactory(),
|
||||||
};
|
};
|
||||||
|
|
||||||
function isRegExp(obj: any): obj is RegExp {
|
function isRegExp(obj: any): obj is RegExp {
|
||||||
|
|
|
||||||
|
|
@ -165,7 +165,7 @@ function renderSourceOptions(sources: Source[]): React.ReactNode {
|
||||||
const hasGroup = sources.some(s => s.group);
|
const hasGroup = sources.some(s => s.group);
|
||||||
if (hasGroup) {
|
if (hasGroup) {
|
||||||
const groups = new Set(sources.map(s => s.group));
|
const groups = new Set(sources.map(s => s.group));
|
||||||
return Array.from(groups).map(group => (
|
return [...groups].filter(Boolean).map(group => (
|
||||||
<optgroup label={group} key={group}>
|
<optgroup label={group} key={group}>
|
||||||
{sources.filter(s => s.group === group).map(source => renderOption(source))}
|
{sources.filter(s => s.group === group).map(source => renderOption(source))}
|
||||||
</optgroup>
|
</optgroup>
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ export type UIState = {
|
||||||
mode: Mode;
|
mode: Mode;
|
||||||
actionPoint?: Point;
|
actionPoint?: Point;
|
||||||
actionSelector?: string;
|
actionSelector?: string;
|
||||||
language: 'javascript' | 'python' | 'java' | 'csharp';
|
language: 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||||
testIdAttributeName: string;
|
testIdAttributeName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export type SourceHighlight = {
|
||||||
message?: string;
|
message?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||||
|
|
||||||
export interface SourceProps {
|
export interface SourceProps {
|
||||||
text: string;
|
text: string;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ type CLITestArgs = {
|
||||||
};
|
};
|
||||||
|
|
||||||
const codegenLang2Id: Map<string, string> = new Map([
|
const codegenLang2Id: Map<string, string> = new Map([
|
||||||
|
['JSON', 'jsonl'],
|
||||||
['JavaScript', 'javascript'],
|
['JavaScript', 'javascript'],
|
||||||
['Java', 'java'],
|
['Java', 'java'],
|
||||||
['Python', 'python'],
|
['Python', 'python'],
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue