chore(internal): generate code in jsonl (#23124)

This commit is contained in:
Pavel Feldman 2023-05-20 10:15:33 -07:00 committed by GitHub
parent 33f3a6002d
commit 631edc9744
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 91 additions and 9 deletions

View file

@ -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');

View file

@ -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)

View 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 '';
}
}

View file

@ -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 {

View file

@ -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>

View file

@ -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;
};

View file

@ -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;

View file

@ -32,6 +32,7 @@ type CLITestArgs = {
};
const codegenLang2Id: Map<string, string> = new Map([
['JSON', 'jsonl'],
['JavaScript', 'javascript'],
['Java', 'java'],
['Python', 'python'],