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,
|
||||
ok: false,
|
||||
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: 'Passed' }).locator('.counter')).toHaveText('42');
|
||||
await expect(component.locator('a', { hasText: 'Failed' }).locator('.counter')).toHaveText('31');
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import { Frame } from './frames';
|
|||
import { BrowserContext } from './browserContext';
|
||||
import { JavaLanguageGenerator } from './recorder/java';
|
||||
import { JavaScriptLanguageGenerator } from './recorder/javascript';
|
||||
import { JsonlLanguageGenerator } from './recorder/jsonl';
|
||||
import { CSharpLanguageGenerator } from './recorder/csharp';
|
||||
import { PythonLanguageGenerator } from './recorder/python';
|
||||
import * as recorderSource from '../generated/recorderSource';
|
||||
|
|
@ -406,6 +407,7 @@ class ContextRecorder extends EventEmitter {
|
|||
new CSharpLanguageGenerator('mstest'),
|
||||
new CSharpLanguageGenerator('nunit'),
|
||||
new CSharpLanguageGenerator('library'),
|
||||
new JsonlLanguageGenerator(),
|
||||
]);
|
||||
const primaryLanguage = [...languages].find(l => l.id === codegenId);
|
||||
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 { 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 LocatorBase = 'page' | 'locator' | 'frame-locator';
|
||||
|
||||
|
|
@ -31,6 +31,7 @@ type LocatorOptions = {
|
|||
};
|
||||
export interface LocatorFactory {
|
||||
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 {
|
||||
|
|
@ -191,7 +192,7 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
|||
// Two options:
|
||||
// - locator('div').filter({ hasText: 'foo' })
|
||||
// - locator('div', { hasText: 'foo' })
|
||||
tokens.push([locatorPart + '.' + nextLocatorPart, combinedPart]);
|
||||
tokens.push([factory.chainLocators([locatorPart, nextLocatorPart]), combinedPart]);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
|
@ -200,16 +201,16 @@ function innerAsLocators(factory: LocatorFactory, parsed: ParsedSelector, isFram
|
|||
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 result: string[] = [];
|
||||
|
||||
const visit = (index: number) => {
|
||||
if (index === tokens.length) {
|
||||
result.push(currentTokens.join('.'));
|
||||
result.push(factory.chainLocators(currentTokens));
|
||||
return currentTokens.length < maxOutputSize;
|
||||
}
|
||||
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) {
|
||||
if (isRegExp(body))
|
||||
return `${method}(${body})`;
|
||||
|
|
@ -381,6 +386,10 @@ export class PythonLocatorFactory implements LocatorFactory {
|
|||
}
|
||||
}
|
||||
|
||||
chainLocators(locators: string[]): string {
|
||||
return locators.join('.');
|
||||
}
|
||||
|
||||
private regexToString(body: RegExp) {
|
||||
const suffix = body.flags.includes('i') ? ', re.IGNORECASE' : '';
|
||||
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) {
|
||||
const suffix = body.flags.includes('i') ? ', Pattern.CASE_INSENSITIVE' : '';
|
||||
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 {
|
||||
const suffix = body.flags.includes('i') ? ', RegexOptions.IgnoreCase' : '';
|
||||
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> = {
|
||||
javascript: new JavaScriptLocatorFactory(),
|
||||
python: new PythonLocatorFactory(),
|
||||
java: new JavaLocatorFactory(),
|
||||
csharp: new CSharpLocatorFactory(),
|
||||
jsonl: new JsonlLocatorFactory(),
|
||||
};
|
||||
|
||||
function isRegExp(obj: any): obj is RegExp {
|
||||
|
|
|
|||
|
|
@ -165,7 +165,7 @@ function renderSourceOptions(sources: Source[]): React.ReactNode {
|
|||
const hasGroup = sources.some(s => s.group);
|
||||
if (hasGroup) {
|
||||
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}>
|
||||
{sources.filter(s => s.group === group).map(source => renderOption(source))}
|
||||
</optgroup>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export type UIState = {
|
|||
mode: Mode;
|
||||
actionPoint?: Point;
|
||||
actionSelector?: string;
|
||||
language: 'javascript' | 'python' | 'java' | 'csharp';
|
||||
language: 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||
testIdAttributeName: string;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export type SourceHighlight = {
|
|||
message?: string;
|
||||
};
|
||||
|
||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp';
|
||||
export type Language = 'javascript' | 'python' | 'java' | 'csharp' | 'jsonl';
|
||||
|
||||
export interface SourceProps {
|
||||
text: string;
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ type CLITestArgs = {
|
|||
};
|
||||
|
||||
const codegenLang2Id: Map<string, string> = new Map([
|
||||
['JSON', 'jsonl'],
|
||||
['JavaScript', 'javascript'],
|
||||
['Java', 'java'],
|
||||
['Python', 'python'],
|
||||
|
|
|
|||
Loading…
Reference in a new issue