chore: allow configuring test id attribute for codegen (#22716)
Fixes: https://github.com/microsoft/playwright/issues/22653
This commit is contained in:
parent
a01df2ff5b
commit
116fb349ce
|
|
@ -64,8 +64,9 @@ commandWithOpenOptions('codegen [url]', 'open page and generate code for user ac
|
|||
['-o, --output <file name>', 'saves the generated script to a file'],
|
||||
['--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'],
|
||||
['--test-id-attribute <attributeName>', 'use the specified attribute to generate data test ID selectors'],
|
||||
]).action(function(url, options) {
|
||||
codegen(options, url, options.target, options.output).catch(logErrorAndExit);
|
||||
codegen(options, url).catch(logErrorAndExit);
|
||||
}).addHelpText('afterAll', `
|
||||
Examples:
|
||||
|
||||
|
|
@ -527,7 +528,8 @@ async function open(options: Options, url: string | undefined, language: string)
|
|||
await openPage(context, url);
|
||||
}
|
||||
|
||||
async function codegen(options: Options, url: string | undefined, language: string, outputFile?: string) {
|
||||
async function codegen(options: Options & { target: string, output?: string, testIdAttribute?: string }, url: string | undefined) {
|
||||
const { target: language, output: outputFile, testIdAttribute: testIdAttributeName } = options;
|
||||
const { context, launchOptions, contextOptions } = await launchContext(options, !!process.env.PWTEST_CLI_HEADLESS, process.env.PWTEST_CLI_EXECUTABLE_PATH);
|
||||
await context._enableRecorder({
|
||||
language,
|
||||
|
|
@ -536,6 +538,7 @@ async function codegen(options: Options, url: string | undefined, language: stri
|
|||
device: options.device,
|
||||
saveStorage: options.saveStorage,
|
||||
mode: 'recording',
|
||||
testIdAttributeName,
|
||||
outputFile: outputFile ? path.resolve(outputFile) : undefined,
|
||||
handleSIGINT: false,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -375,6 +375,7 @@ export class BrowserContext extends ChannelOwner<channels.BrowserContextChannel>
|
|||
device?: string,
|
||||
saveStorage?: string,
|
||||
mode?: 'recording' | 'inspecting',
|
||||
testIdAttributeName?: string,
|
||||
outputFile?: string,
|
||||
handleSIGINT?: boolean,
|
||||
}) {
|
||||
|
|
|
|||
|
|
@ -881,6 +881,7 @@ scheme.BrowserContextRecorderSupplementEnableParams = tObject({
|
|||
language: tOptional(tString),
|
||||
mode: tOptional(tEnum(['inspecting', 'recording'])),
|
||||
pauseOnNextStatement: tOptional(tBoolean),
|
||||
testIdAttributeName: tOptional(tString),
|
||||
launchOptions: tOptional(tAny),
|
||||
contextOptions: tOptional(tAny),
|
||||
device: tOptional(tString),
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ export class InjectedScript {
|
|||
return result;
|
||||
}
|
||||
|
||||
generateSelector(targetElement: Element, testIdAttributeName: string): string {
|
||||
generateSelector(targetElement: Element, testIdAttributeName: string, omitInternalEngines?: boolean): string {
|
||||
return generateSelector(this, targetElement, testIdAttributeName).selector;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -74,11 +74,11 @@ export function querySelector(injectedScript: InjectedScript, selector: string,
|
|||
}
|
||||
}
|
||||
|
||||
export function generateSelector(injectedScript: InjectedScript, targetElement: Element, testIdAttributeName: string): { selector: string, elements: Element[] } {
|
||||
export function generateSelector(injectedScript: InjectedScript, targetElement: Element, testIdAttributeName: string, omitInternalEngines?: boolean): { selector: string, elements: Element[] } {
|
||||
injectedScript._evaluator.begin();
|
||||
try {
|
||||
targetElement = targetElement.closest('button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]') || targetElement;
|
||||
const targetTokens = generateSelectorFor(injectedScript, targetElement, testIdAttributeName);
|
||||
const targetTokens = generateSelectorFor(injectedScript, targetElement, testIdAttributeName, omitInternalEngines);
|
||||
const bestTokens = targetTokens || cssFallback(injectedScript, targetElement);
|
||||
const selector = joinTokens(bestTokens);
|
||||
const parsedSelector = injectedScript.parseSelector(selector);
|
||||
|
|
@ -98,7 +98,7 @@ function filterRegexTokens(textCandidates: SelectorToken[][]): SelectorToken[][]
|
|||
return textCandidates.filter(c => c[0].selector[0] !== '/');
|
||||
}
|
||||
|
||||
function generateSelectorFor(injectedScript: InjectedScript, targetElement: Element, testIdAttributeName: string): SelectorToken[] | null {
|
||||
function generateSelectorFor(injectedScript: InjectedScript, targetElement: Element, testIdAttributeName: string, omitInternalEngines?: boolean): SelectorToken[] | null {
|
||||
if (targetElement.ownerDocument.documentElement === targetElement)
|
||||
return [{ engine: 'css', selector: 'html', score: 1 }];
|
||||
|
||||
|
|
@ -111,7 +111,9 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
|
|||
// Do not use regex for parent elements (for performance).
|
||||
textCandidates = filterRegexTokens(textCandidates);
|
||||
}
|
||||
const noTextCandidates = buildCandidates(injectedScript, element, testIdAttributeName, accessibleNameCache).map(token => [token]);
|
||||
const noTextCandidates = buildCandidates(injectedScript, element, testIdAttributeName, accessibleNameCache)
|
||||
.filter(token => !omitInternalEngines || !token.engine.startsWith('internal:'))
|
||||
.map(token => [token]);
|
||||
|
||||
// First check all text and non-text candidates for the element.
|
||||
let result = chooseFirstSelector(injectedScript, targetElement.ownerDocument, element, [...textCandidates, ...noTextCandidates], allowNthMatch);
|
||||
|
|
|
|||
|
|
@ -173,7 +173,7 @@ export class Recorder implements InstrumentationListener {
|
|||
actionPoint,
|
||||
actionSelector,
|
||||
language: this._currentLanguage,
|
||||
testIdAttributeName: this._context.selectors().testIdAttributeName(),
|
||||
testIdAttributeName: this._contextRecorder.testIdAttributeName(),
|
||||
};
|
||||
return uiState;
|
||||
});
|
||||
|
|
@ -349,7 +349,6 @@ class ContextRecorder extends EventEmitter {
|
|||
private _recorderSources: Source[];
|
||||
private _throttledOutputFile: ThrottledFile | null = null;
|
||||
private _orderedLanguages: LanguageGenerator[] = [];
|
||||
private _testIdAttributeName: string = 'data-testid';
|
||||
private _listeners: RegisteredListener[] = [];
|
||||
|
||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
||||
|
|
@ -542,6 +541,10 @@ class ContextRecorder extends EventEmitter {
|
|||
return fallback;
|
||||
}
|
||||
|
||||
testIdAttributeName(): string {
|
||||
return this._params.testIdAttributeName || this._context.selectors().testIdAttributeName() || 'data-testid';
|
||||
}
|
||||
|
||||
private async _findFrameSelector(frame: Frame, parent: Frame): Promise<string | undefined> {
|
||||
try {
|
||||
const frameElement = await frame.frameElement();
|
||||
|
|
@ -549,7 +552,9 @@ class ContextRecorder extends EventEmitter {
|
|||
return;
|
||||
const utility = await parent._utilityContext();
|
||||
const injected = await utility.injectedScript();
|
||||
const selector = await injected.evaluate((injected, element) => injected.generateSelector(element as Element, this._testIdAttributeName), frameElement);
|
||||
const selector = await injected.evaluate((injected, element) => {
|
||||
return injected.generateSelector(element as Element, '', true);
|
||||
}, frameElement);
|
||||
return selector;
|
||||
} catch (e) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1613,6 +1613,7 @@ export type BrowserContextRecorderSupplementEnableParams = {
|
|||
language?: string,
|
||||
mode?: 'inspecting' | 'recording',
|
||||
pauseOnNextStatement?: boolean,
|
||||
testIdAttributeName?: string,
|
||||
launchOptions?: any,
|
||||
contextOptions?: any,
|
||||
device?: string,
|
||||
|
|
@ -1625,6 +1626,7 @@ export type BrowserContextRecorderSupplementEnableOptions = {
|
|||
language?: string,
|
||||
mode?: 'inspecting' | 'recording',
|
||||
pauseOnNextStatement?: boolean,
|
||||
testIdAttributeName?: string,
|
||||
launchOptions?: any,
|
||||
contextOptions?: any,
|
||||
device?: string,
|
||||
|
|
|
|||
|
|
@ -1113,6 +1113,7 @@ BrowserContext:
|
|||
- inspecting
|
||||
- recording
|
||||
pauseOnNextStatement: boolean?
|
||||
testIdAttributeName: string?
|
||||
launchOptions: json?
|
||||
contextOptions: json?
|
||||
device: string?
|
||||
|
|
|
|||
|
|
@ -536,3 +536,17 @@ test.describe('cli codegen', () => {
|
|||
});
|
||||
|
||||
});
|
||||
|
||||
test('should --test-id-attribute', async ({ page, openRecorder }) => {
|
||||
const recorder = await openRecorder({ testIdAttributeName: 'my-test-id' });
|
||||
|
||||
await recorder.setContentAndWait(`<div my-test-id="foo">Hello</div>`);
|
||||
await page.click('[my-test-id=foo]');
|
||||
const sources = await recorder.waitForOutput('JavaScript', `page.getByTestId`);
|
||||
|
||||
expect.soft(sources.get('JavaScript').text).toContain(`await page.getByTestId('foo').click()`);
|
||||
expect.soft(sources.get('Java').text).toContain(`page.getByTestId("foo").click()`);
|
||||
expect.soft(sources.get('Python').text).toContain(`page.get_by_test_id("foo").click()`);
|
||||
expect.soft(sources.get('Python Async').text).toContain(`await page.get_by_test_id("foo").click()`);
|
||||
expect.soft(sources.get('C#').text).toContain(`await page.GetByTestId("foo").ClickAsync();`);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ export { expect } from '@playwright/test';
|
|||
type CLITestArgs = {
|
||||
recorderPageGetter: () => Promise<Page>;
|
||||
closeRecorder: () => Promise<void>;
|
||||
openRecorder: () => Promise<Recorder>;
|
||||
openRecorder: (options?: { testIdAttributeName: string }) => Promise<Recorder>;
|
||||
runCLI: (args: string[], options?: { autoExitWhen?: string }) => CLIMock;
|
||||
};
|
||||
|
||||
|
|
@ -76,8 +76,8 @@ export const test = contextTest.extend<CLITestArgs>({
|
|||
},
|
||||
|
||||
openRecorder: async ({ page, recorderPageGetter }, run) => {
|
||||
await run(async () => {
|
||||
await (page.context() as any)._enableRecorder({ language: 'javascript', mode: 'recording' });
|
||||
await run(async (options?: { testIdAttributeName?: string }) => {
|
||||
await (page.context() as any)._enableRecorder({ language: 'javascript', mode: 'recording', ...options });
|
||||
return new Recorder(page, await recorderPageGetter());
|
||||
});
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue