From 631edc97442d2b216ccd5fc9c93eaf0c6ad9ab09 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Sat, 20 May 2023 10:15:33 -0700 Subject: [PATCH] chore(internal): generate code in jsonl (#23124) --- .../html-reporter/src/headerView.spec.tsx | 2 +- .../playwright-core/src/server/recorder.ts | 2 + .../src/server/recorder/jsonl.ts | 44 ++++++++++++++++++ .../src/utils/isomorphic/locatorGenerators.ts | 45 ++++++++++++++++--- packages/recorder/src/recorder.tsx | 2 +- packages/recorder/src/recorderTypes.ts | 2 +- .../web/src/components/codeMirrorWrapper.tsx | 2 +- tests/library/inspector/inspectorTest.ts | 1 + 8 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 packages/playwright-core/src/server/recorder/jsonl.ts diff --git a/packages/html-reporter/src/headerView.spec.tsx b/packages/html-reporter/src/headerView.spec.tsx index 5c05ec1e29..707ba73cc1 100644 --- a/packages/html-reporter/src/headerView.spec.tsx +++ b/packages/html-reporter/src/headerView.spec.tsx @@ -28,7 +28,7 @@ test('should render counters', async ({ mount }) => { skipped: 10, ok: false, duration: 100000 - }} filterText='' setFilterText={() => {}} projectNames={[]}>); + }} filterText='' setFilterText={() => {}}>); 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'); diff --git a/packages/playwright-core/src/server/recorder.ts b/packages/playwright-core/src/server/recorder.ts index cc9227241a..d2e53e9d62 100644 --- a/packages/playwright-core/src/server/recorder.ts +++ b/packages/playwright-core/src/server/recorder.ts @@ -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) diff --git a/packages/playwright-core/src/server/recorder/jsonl.ts b/packages/playwright-core/src/server/recorder/jsonl.ts new file mode 100644 index 0000000000..636aaae70f --- /dev/null +++ b/packages/playwright-core/src/server/recorder/jsonl.ts @@ -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 ''; + } +} diff --git a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts index 6f0920299e..50d0595f3d 100644 --- a/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts +++ b/packages/playwright-core/src/utils/isomorphic/locatorGenerators.ts @@ -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 = { javascript: new JavaScriptLocatorFactory(), python: new PythonLocatorFactory(), java: new JavaLocatorFactory(), csharp: new CSharpLocatorFactory(), + jsonl: new JsonlLocatorFactory(), }; function isRegExp(obj: any): obj is RegExp { diff --git a/packages/recorder/src/recorder.tsx b/packages/recorder/src/recorder.tsx index 055073bb39..ba6d217ff9 100644 --- a/packages/recorder/src/recorder.tsx +++ b/packages/recorder/src/recorder.tsx @@ -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 => ( {sources.filter(s => s.group === group).map(source => renderOption(source))} diff --git a/packages/recorder/src/recorderTypes.ts b/packages/recorder/src/recorderTypes.ts index 302164321c..4820a8abee 100644 --- a/packages/recorder/src/recorderTypes.ts +++ b/packages/recorder/src/recorderTypes.ts @@ -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; }; diff --git a/packages/web/src/components/codeMirrorWrapper.tsx b/packages/web/src/components/codeMirrorWrapper.tsx index f072530f48..c8c96b2fd5 100644 --- a/packages/web/src/components/codeMirrorWrapper.tsx +++ b/packages/web/src/components/codeMirrorWrapper.tsx @@ -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; diff --git a/tests/library/inspector/inspectorTest.ts b/tests/library/inspector/inspectorTest.ts index 04111a39d6..2b4b790c1d 100644 --- a/tests/library/inspector/inspectorTest.ts +++ b/tests/library/inspector/inspectorTest.ts @@ -32,6 +32,7 @@ type CLITestArgs = { }; const codegenLang2Id: Map = new Map([ + ['JSON', 'jsonl'], ['JavaScript', 'javascript'], ['Java', 'java'], ['Python', 'python'],