chore: use provided value for the generated test id (#18631)
This commit is contained in:
parent
05b623e6b0
commit
0355d8618f
|
|
@ -308,7 +308,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTestId(testId: string): Locator {
|
getByTestId(testId: string): Locator {
|
||||||
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
|
return this.locator(getByTestIdSelector(testIdAttributeName(), testId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ export class Locator implements api.Locator {
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTestId(testId: string): Locator {
|
getByTestId(testId: string): Locator {
|
||||||
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
|
return this.locator(getByTestIdSelector(testIdAttributeName(), testId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
||||||
|
|
@ -340,7 +340,7 @@ export class FrameLocator implements api.FrameLocator {
|
||||||
}
|
}
|
||||||
|
|
||||||
getByTestId(testId: string): Locator {
|
getByTestId(testId: string): Locator {
|
||||||
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
|
return this.locator(getByTestIdSelector(testIdAttributeName(), testId));
|
||||||
}
|
}
|
||||||
|
|
||||||
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
|
||||||
|
|
@ -384,8 +384,12 @@ export class FrameLocator implements api.FrameLocator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export let testIdAttributeName: string = 'data-testid';
|
let _testIdAttributeName: string = 'data-testid';
|
||||||
|
|
||||||
|
export function testIdAttributeName(): string {
|
||||||
|
return _testIdAttributeName;
|
||||||
|
}
|
||||||
|
|
||||||
export function setTestIdAttribute(attributeName: string) {
|
export function setTestIdAttribute(attributeName: string) {
|
||||||
testIdAttributeName = attributeName;
|
_testIdAttributeName = attributeName;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ import type * as channels from '@protocol/channels';
|
||||||
import { ChannelOwner } from './channelOwner';
|
import { ChannelOwner } from './channelOwner';
|
||||||
import type { SelectorEngine } from './types';
|
import type { SelectorEngine } from './types';
|
||||||
import type * as api from '../../types/types';
|
import type * as api from '../../types/types';
|
||||||
import { setTestIdAttribute } from './locator';
|
import { setTestIdAttribute, testIdAttributeName } from './locator';
|
||||||
|
|
||||||
export class Selectors implements api.Selectors {
|
export class Selectors implements api.Selectors {
|
||||||
private _channels = new Set<SelectorsOwner>();
|
private _channels = new Set<SelectorsOwner>();
|
||||||
|
|
@ -35,13 +35,16 @@ export class Selectors implements api.Selectors {
|
||||||
|
|
||||||
setTestIdAttribute(attributeName: string) {
|
setTestIdAttribute(attributeName: string) {
|
||||||
setTestIdAttribute(attributeName);
|
setTestIdAttribute(attributeName);
|
||||||
|
for (const channel of this._channels)
|
||||||
|
channel._channel.setTestIdAttributeName({ testIdAttributeName: attributeName }).catch(() => {});
|
||||||
}
|
}
|
||||||
|
|
||||||
_addChannel(channel: SelectorsOwner) {
|
_addChannel(channel: SelectorsOwner) {
|
||||||
this._channels.add(channel);
|
this._channels.add(channel);
|
||||||
for (const params of this._registrations) {
|
for (const params of this._registrations) {
|
||||||
// This should not fail except for connection closure, but just in case we catch.
|
// This should not fail except for connection closure, but just in case we catch.
|
||||||
channel._channel.register(params).catch(e => {});
|
channel._channel.register(params).catch(() => {});
|
||||||
|
channel._channel.setTestIdAttributeName({ testIdAttributeName: testIdAttributeName() }).catch(() => {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -368,6 +368,7 @@ scheme.DebugControllerNavigateParams = tObject({
|
||||||
scheme.DebugControllerNavigateResult = tOptional(tObject({}));
|
scheme.DebugControllerNavigateResult = tOptional(tObject({}));
|
||||||
scheme.DebugControllerSetRecorderModeParams = tObject({
|
scheme.DebugControllerSetRecorderModeParams = tObject({
|
||||||
mode: tEnum(['inspecting', 'recording', 'none']),
|
mode: tEnum(['inspecting', 'recording', 'none']),
|
||||||
|
testIdAttributeName: tOptional(tString),
|
||||||
});
|
});
|
||||||
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
|
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
|
||||||
scheme.DebugControllerHighlightParams = tObject({
|
scheme.DebugControllerHighlightParams = tObject({
|
||||||
|
|
@ -425,6 +426,10 @@ scheme.SelectorsRegisterParams = tObject({
|
||||||
contentScript: tOptional(tBoolean),
|
contentScript: tOptional(tBoolean),
|
||||||
});
|
});
|
||||||
scheme.SelectorsRegisterResult = tOptional(tObject({}));
|
scheme.SelectorsRegisterResult = tOptional(tObject({}));
|
||||||
|
scheme.SelectorsSetTestIdAttributeNameParams = tObject({
|
||||||
|
testIdAttributeName: tString,
|
||||||
|
});
|
||||||
|
scheme.SelectorsSetTestIdAttributeNameResult = tOptional(tObject({}));
|
||||||
scheme.BrowserTypeInitializer = tObject({
|
scheme.BrowserTypeInitializer = tObject({
|
||||||
executablePath: tString,
|
executablePath: tString,
|
||||||
name: tString,
|
name: tString,
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,7 @@ export class DebugController extends SdkObject {
|
||||||
await p.mainFrame().goto(internalMetadata, url);
|
await p.mainFrame().goto(internalMetadata, url);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setRecorderMode(params: { mode: Mode, file?: string }) {
|
async setRecorderMode(params: { mode: Mode, file?: string, testIdAttributeName?: string }) {
|
||||||
// TODO: |file| is only used in the legacy mode.
|
// TODO: |file| is only used in the legacy mode.
|
||||||
await this._closeBrowsersWithoutPages();
|
await this._closeBrowsersWithoutPages();
|
||||||
|
|
||||||
|
|
@ -114,8 +114,11 @@ export class DebugController extends SdkObject {
|
||||||
// Toggle the mode.
|
// Toggle the mode.
|
||||||
for (const recorder of await this._allRecorders()) {
|
for (const recorder of await this._allRecorders()) {
|
||||||
recorder.hideHighlightedSelecor();
|
recorder.hideHighlightedSelecor();
|
||||||
if (params.mode === 'recording')
|
if (params.mode === 'recording') {
|
||||||
recorder.setOutput(this._codegenId, params.file);
|
recorder.setOutput(this._codegenId, params.file);
|
||||||
|
if (params.testIdAttributeName)
|
||||||
|
recorder.setTestIdAttributeName(params.testIdAttributeName);
|
||||||
|
}
|
||||||
recorder.setMode(params.mode);
|
recorder.setMode(params.mode);
|
||||||
}
|
}
|
||||||
this.setAutoCloseEnabled(true);
|
this.setAutoCloseEnabled(true);
|
||||||
|
|
|
||||||
|
|
@ -29,4 +29,8 @@ export class SelectorsDispatcher extends Dispatcher<Selectors, channels.Selector
|
||||||
async register(params: channels.SelectorsRegisterParams): Promise<void> {
|
async register(params: channels.SelectorsRegisterParams): Promise<void> {
|
||||||
await this._object.register(params.name, params.source, params.contentScript);
|
await this._object.register(params.name, params.source, params.contentScript);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async setTestIdAttributeName(params: channels.SelectorsSetTestIdAttributeNameParams, metadata?: channels.Metadata | undefined): Promise<void> {
|
||||||
|
this._object.setTestIdAttributeName(params.testIdAttributeName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -106,6 +106,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
|
||||||
return new module.exports(
|
return new module.exports(
|
||||||
${isUnderTest()},
|
${isUnderTest()},
|
||||||
"${sdkLanguage}",
|
"${sdkLanguage}",
|
||||||
|
${JSON.stringify(this.frame._page.selectors.testIdAttributeName())},
|
||||||
${this.frame._page._delegate.rafCountForStablePosition()},
|
${this.frame._page._delegate.rafCountForStablePosition()},
|
||||||
"${this.frame._page._browserContext._browser.options.name}",
|
"${this.frame._page._browserContext._browser.options.name}",
|
||||||
[${custom.join(',\n')}]
|
[${custom.join(',\n')}]
|
||||||
|
|
|
||||||
|
|
@ -46,7 +46,7 @@ class Locator {
|
||||||
self.locator = (selector: string, options?: { hasText?: string | RegExp, has?: Locator }): Locator => {
|
self.locator = (selector: string, options?: { hasText?: string | RegExp, has?: Locator }): Locator => {
|
||||||
return new Locator(injectedScript, selectorBase ? selectorBase + ' >> ' + selector : selector, options);
|
return new Locator(injectedScript, selectorBase ? selectorBase + ' >> ' + selector : selector, options);
|
||||||
};
|
};
|
||||||
self.getByTestId = (testId: string): Locator => self.locator(getByTestIdSelector('data-testid', testId));
|
self.getByTestId = (testId: string): Locator => self.locator(getByTestIdSelector(injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen(), testId));
|
||||||
self.getByAltText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByAltTextSelector(text, options));
|
self.getByAltText = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByAltTextSelector(text, options));
|
||||||
self.getByLabel = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByLabelSelector(text, options));
|
self.getByLabel = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByLabelSelector(text, options));
|
||||||
self.getByPlaceholder = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByPlaceholderSelector(text, options));
|
self.getByPlaceholder = (text: string | RegExp, options?: { exact?: boolean }): Locator => self.locator(getByPlaceholderSelector(text, options));
|
||||||
|
|
@ -114,13 +114,13 @@ class ConsoleAPI {
|
||||||
private _selector(element: Element) {
|
private _selector(element: Element) {
|
||||||
if (!(element instanceof Element))
|
if (!(element instanceof Element))
|
||||||
throw new Error(`Usage: playwright.selector(element).`);
|
throw new Error(`Usage: playwright.selector(element).`);
|
||||||
return generateSelector(this._injectedScript, element, true).selector;
|
return generateSelector(this._injectedScript, element, true, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _generateLocator(element: Element, language?: Language) {
|
private _generateLocator(element: Element, language?: Language) {
|
||||||
if (!(element instanceof Element))
|
if (!(element instanceof Element))
|
||||||
throw new Error(`Usage: playwright.locator(element).`);
|
throw new Error(`Usage: playwright.locator(element).`);
|
||||||
const selector = generateSelector(this._injectedScript, element, true).selector;
|
const selector = generateSelector(this._injectedScript, element, true, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector;
|
||||||
return asLocator(language || 'javascript', selector);
|
return asLocator(language || 'javascript', selector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,12 @@ export class InjectedScript {
|
||||||
private _highlight: Highlight | undefined;
|
private _highlight: Highlight | undefined;
|
||||||
readonly isUnderTest: boolean;
|
readonly isUnderTest: boolean;
|
||||||
private _sdkLanguage: Language;
|
private _sdkLanguage: Language;
|
||||||
|
private _testIdAttributeNameForStrictErrorAndConsoleCodegen: string = 'data-testid';
|
||||||
|
|
||||||
constructor(isUnderTest: boolean, sdkLanguage: Language, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) {
|
constructor(isUnderTest: boolean, sdkLanguage: Language, testIdAttributeNameForStrictErrorAndConsoleCodegen: string, stableRafCount: number, browserName: string, customEngines: { name: string, engine: SelectorEngine }[]) {
|
||||||
this.isUnderTest = isUnderTest;
|
this.isUnderTest = isUnderTest;
|
||||||
this._sdkLanguage = sdkLanguage;
|
this._sdkLanguage = sdkLanguage;
|
||||||
|
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen;
|
||||||
this._evaluator = new SelectorEvaluatorImpl(new Map());
|
this._evaluator = new SelectorEvaluatorImpl(new Map());
|
||||||
|
|
||||||
this._engines = new Map();
|
this._engines = new Map();
|
||||||
|
|
@ -113,6 +115,7 @@ export class InjectedScript {
|
||||||
this._engines.set('internal:text', this._createTextEngine(true, true));
|
this._engines.set('internal:text', this._createTextEngine(true, true));
|
||||||
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
|
||||||
this._engines.set('internal:attr', this._createNamedAttributeEngine());
|
this._engines.set('internal:attr', this._createNamedAttributeEngine());
|
||||||
|
this._engines.set('internal:testid', this._createNamedAttributeEngine());
|
||||||
this._engines.set('internal:role', RoleEngine);
|
this._engines.set('internal:role', RoleEngine);
|
||||||
|
|
||||||
for (const { name, engine } of customEngines)
|
for (const { name, engine } of customEngines)
|
||||||
|
|
@ -132,6 +135,10 @@ export class InjectedScript {
|
||||||
return globalThis.eval(expression);
|
return globalThis.eval(expression);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testIdAttributeNameForStrictErrorAndConsoleCodegen(): string {
|
||||||
|
return this._testIdAttributeNameForStrictErrorAndConsoleCodegen;
|
||||||
|
}
|
||||||
|
|
||||||
parseSelector(selector: string): ParsedSelector {
|
parseSelector(selector: string): ParsedSelector {
|
||||||
const result = parseSelector(selector);
|
const result = parseSelector(selector);
|
||||||
for (const name of allEngineNames(result)) {
|
for (const name of allEngineNames(result)) {
|
||||||
|
|
@ -141,8 +148,8 @@ export class InjectedScript {
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
generateSelector(targetElement: Element): string {
|
generateSelector(targetElement: Element, testIdAttributeName: string): string {
|
||||||
return generateSelector(this, targetElement, true).selector;
|
return generateSelector(this, targetElement, true, testIdAttributeName).selector;
|
||||||
}
|
}
|
||||||
|
|
||||||
querySelector(selector: ParsedSelector, root: Node, strict: boolean): Element | undefined {
|
querySelector(selector: ParsedSelector, root: Node, strict: boolean): Element | undefined {
|
||||||
|
|
@ -1016,7 +1023,7 @@ export class InjectedScript {
|
||||||
strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error {
|
strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error {
|
||||||
const infos = matches.slice(0, 10).map(m => ({
|
const infos = matches.slice(0, 10).map(m => ({
|
||||||
preview: this.previewNode(m),
|
preview: this.previewNode(m),
|
||||||
selector: this.generateSelector(m),
|
selector: this.generateSelector(m, this._testIdAttributeNameForStrictErrorAndConsoleCodegen),
|
||||||
}));
|
}));
|
||||||
const lines = infos.map((info, i) => `\n ${i + 1}) ${info.preview} aka ${asLocator(this._sdkLanguage, info.selector)}`);
|
const lines = infos.map((info, i) => `\n ${i + 1}) ${info.preview} aka ${asLocator(this._sdkLanguage, info.selector)}`);
|
||||||
if (infos.length < matches.length)
|
if (infos.length < matches.length)
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ class Recorder {
|
||||||
private _actionPoint: Point | undefined;
|
private _actionPoint: Point | undefined;
|
||||||
private _actionSelector: string | undefined;
|
private _actionSelector: string | undefined;
|
||||||
private _highlight: Highlight;
|
private _highlight: Highlight;
|
||||||
|
private _testIdAttributeName: string = 'data-testid';
|
||||||
|
|
||||||
constructor(injectedScript: InjectedScript) {
|
constructor(injectedScript: InjectedScript) {
|
||||||
this._injectedScript = injectedScript;
|
this._injectedScript = injectedScript;
|
||||||
|
|
@ -94,7 +95,8 @@ class Recorder {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mode, actionPoint, actionSelector, language } = state;
|
const { mode, actionPoint, actionSelector, language, testIdAttributeName } = state;
|
||||||
|
this._testIdAttributeName = testIdAttributeName;
|
||||||
this._highlight.setLanguage(language);
|
this._highlight.setLanguage(language);
|
||||||
if (mode !== this._mode) {
|
if (mode !== this._mode) {
|
||||||
this._mode = mode;
|
this._mode = mode;
|
||||||
|
|
@ -238,7 +240,7 @@ class Recorder {
|
||||||
if (this._mode === 'none')
|
if (this._mode === 'none')
|
||||||
return;
|
return;
|
||||||
const activeElement = this._deepActiveElement(document);
|
const activeElement = this._deepActiveElement(document);
|
||||||
const result = activeElement ? generateSelector(this._injectedScript, activeElement, true) : null;
|
const result = activeElement ? generateSelector(this._injectedScript, activeElement, true, this._testIdAttributeName) : null;
|
||||||
this._activeModel = result && result.selector ? result : null;
|
this._activeModel = result && result.selector ? result : null;
|
||||||
if (userGesture)
|
if (userGesture)
|
||||||
this._hoveredElement = activeElement as HTMLElement | null;
|
this._hoveredElement = activeElement as HTMLElement | null;
|
||||||
|
|
@ -252,7 +254,7 @@ class Recorder {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const hoveredElement = this._hoveredElement;
|
const hoveredElement = this._hoveredElement;
|
||||||
const { selector, elements } = generateSelector(this._injectedScript, hoveredElement, true);
|
const { selector, elements } = generateSelector(this._injectedScript, hoveredElement, true, this._testIdAttributeName);
|
||||||
if ((this._hoveredModel && this._hoveredModel.selector === selector))
|
if ((this._hoveredModel && this._hoveredModel.selector === selector))
|
||||||
return;
|
return;
|
||||||
this._hoveredModel = selector ? { selector, elements } : null;
|
this._hoveredModel = selector ? { selector, elements } : null;
|
||||||
|
|
|
||||||
|
|
@ -44,11 +44,11 @@ export function querySelector(injectedScript: InjectedScript, selector: string,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateSelector(injectedScript: InjectedScript, targetElement: Element, strict: boolean): { selector: string, elements: Element[] } {
|
export function generateSelector(injectedScript: InjectedScript, targetElement: Element, strict: boolean, testIdAttributeName: string): { selector: string, elements: Element[] } {
|
||||||
injectedScript._evaluator.begin();
|
injectedScript._evaluator.begin();
|
||||||
try {
|
try {
|
||||||
targetElement = targetElement.closest('button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]') || targetElement;
|
targetElement = targetElement.closest('button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]') || targetElement;
|
||||||
const targetTokens = generateSelectorFor(injectedScript, targetElement, strict);
|
const targetTokens = generateSelectorFor(injectedScript, targetElement, strict, testIdAttributeName);
|
||||||
const bestTokens = targetTokens || cssFallback(injectedScript, targetElement, strict);
|
const bestTokens = targetTokens || cssFallback(injectedScript, targetElement, strict);
|
||||||
const selector = joinTokens(bestTokens);
|
const selector = joinTokens(bestTokens);
|
||||||
const parsedSelector = injectedScript.parseSelector(selector);
|
const parsedSelector = injectedScript.parseSelector(selector);
|
||||||
|
|
@ -68,7 +68,7 @@ function filterRegexTokens(textCandidates: SelectorToken[][]): SelectorToken[][]
|
||||||
return textCandidates.filter(c => c[0].selector[0] !== '/');
|
return textCandidates.filter(c => c[0].selector[0] !== '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSelectorFor(injectedScript: InjectedScript, targetElement: Element, strict: boolean): SelectorToken[] | null {
|
function generateSelectorFor(injectedScript: InjectedScript, targetElement: Element, strict: boolean, testIdAttributeName: string): SelectorToken[] | null {
|
||||||
if (targetElement.ownerDocument.documentElement === targetElement)
|
if (targetElement.ownerDocument.documentElement === targetElement)
|
||||||
return [{ engine: 'css', selector: 'html', score: 1 }];
|
return [{ engine: 'css', selector: 'html', score: 1 }];
|
||||||
|
|
||||||
|
|
@ -81,7 +81,7 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
|
||||||
// Do not use regex for parent elements (for performance).
|
// Do not use regex for parent elements (for performance).
|
||||||
textCandidates = filterRegexTokens(textCandidates);
|
textCandidates = filterRegexTokens(textCandidates);
|
||||||
}
|
}
|
||||||
const noTextCandidates = buildCandidates(injectedScript, element, accessibleNameCache).map(token => [token]);
|
const noTextCandidates = buildCandidates(injectedScript, element, testIdAttributeName, accessibleNameCache).map(token => [token]);
|
||||||
|
|
||||||
// First check all text and non-text candidates for the element.
|
// First check all text and non-text candidates for the element.
|
||||||
let result = chooseFirstSelector(injectedScript, targetElement.ownerDocument, element, [...textCandidates, ...noTextCandidates], allowNthMatch, strict);
|
let result = chooseFirstSelector(injectedScript, targetElement.ownerDocument, element, [...textCandidates, ...noTextCandidates], allowNthMatch, strict);
|
||||||
|
|
@ -144,14 +144,13 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
|
||||||
return calculateCached(targetElement, true);
|
return calculateCached(targetElement, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCandidates(injectedScript: InjectedScript, element: Element, accessibleNameCache: Map<Element, boolean>): SelectorToken[] {
|
function buildCandidates(injectedScript: InjectedScript, element: Element, testIdAttributeName: string, accessibleNameCache: Map<Element, boolean>): SelectorToken[] {
|
||||||
const candidates: SelectorToken[] = [];
|
const candidates: SelectorToken[] = [];
|
||||||
|
if (element.getAttribute(testIdAttributeName))
|
||||||
|
candidates.push({ engine: 'internal:testid', selector: `[${testIdAttributeName}=${escapeForAttributeSelector(element.getAttribute(testIdAttributeName)!, true)}]`, score: 1 });
|
||||||
|
|
||||||
if (element.getAttribute('data-testid'))
|
for (const attr of ['data-testid', 'data-test-id', 'data-test']) {
|
||||||
candidates.push({ engine: 'internal:attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!, true)}]`, score: 1 });
|
if (attr !== testIdAttributeName && element.getAttribute(attr))
|
||||||
|
|
||||||
for (const attr of ['data-test-id', 'data-test']) {
|
|
||||||
if (element.getAttribute(attr))
|
|
||||||
candidates.push({ engine: 'css', selector: `[${attr}=${quoteAttributeValue(element.getAttribute(attr)!)}]`, score: 2 });
|
candidates.push({ engine: 'css', selector: `[${attr}=${quoteAttributeValue(element.getAttribute(attr)!)}]`, score: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,14 +71,15 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||||
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, { attrs }));
|
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, { attrs }));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (part.name === 'internal:testid') {
|
||||||
|
const attrSelector = parseAttributeSelector(part.body as string, true);
|
||||||
|
const { value } = attrSelector.attributes[0];
|
||||||
|
tokens.push(factory.generateLocator(base, 'test-id', value));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (part.name === 'internal:attr') {
|
if (part.name === 'internal:attr') {
|
||||||
const attrSelector = parseAttributeSelector(part.body as string, true);
|
const attrSelector = parseAttributeSelector(part.body as string, true);
|
||||||
const { name, value, caseSensitive } = attrSelector.attributes[0];
|
const { name, value, caseSensitive } = attrSelector.attributes[0];
|
||||||
if (name === 'data-testid') {
|
|
||||||
tokens.push(factory.generateLocator(base, 'test-id', value));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const text = value as string | RegExp;
|
const text = value as string | RegExp;
|
||||||
const exact = !!caseSensitive;
|
const exact = !!caseSensitive;
|
||||||
if (name === 'placeholder') {
|
if (name === 'placeholder') {
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import type { Language } from './locatorGenerators';
|
||||||
import { parseSelector } from './selectorParser';
|
import { parseSelector } from './selectorParser';
|
||||||
|
|
||||||
type TemplateParams = { quote: string, text: string }[];
|
type TemplateParams = { quote: string, text: string }[];
|
||||||
function parseLocator(locator: string): string {
|
function parseLocator(locator: string, testIdAttributeName: string): string {
|
||||||
locator = locator
|
locator = locator
|
||||||
.replace(/AriaRole\s*\.\s*([\w]+)/g, (_, group) => group.toLowerCase())
|
.replace(/AriaRole\s*\.\s*([\w]+)/g, (_, group) => group.toLowerCase())
|
||||||
.replace(/(get_by_role|getByRole)\s*\(\s*(?:["'`])([^'"`]+)['"`]/g, (_, group1, group2) => `${group1}(${group2.toLowerCase()}`);
|
.replace(/(get_by_role|getByRole)\s*\(\s*(?:["'`])([^'"`]+)['"`]/g, (_, group1, group2) => `${group1}(${group2.toLowerCase()}`);
|
||||||
|
|
@ -87,7 +87,7 @@ function parseLocator(locator: string): string {
|
||||||
.replace(/regex=/g, '=')
|
.replace(/regex=/g, '=')
|
||||||
.replace(/,,/g, ',');
|
.replace(/,,/g, ',');
|
||||||
|
|
||||||
return transform(template, params);
|
return transform(template, params, testIdAttributeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
function countParams(template: string) {
|
function countParams(template: string) {
|
||||||
|
|
@ -98,7 +98,7 @@ function shiftParams(template: string, sub: number) {
|
||||||
return template.replace(/\$(\d+)/g, (_, ordinal) => `$${ordinal - sub}`);
|
return template.replace(/\$(\d+)/g, (_, ordinal) => `$${ordinal - sub}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transform(template: string, params: TemplateParams): string {
|
function transform(template: string, params: TemplateParams, testIdAttributeName: string): string {
|
||||||
// Recursively handle filter(has=).
|
// Recursively handle filter(has=).
|
||||||
while (true) {
|
while (true) {
|
||||||
const hasMatch = template.match(/filter\(,?has=/);
|
const hasMatch = template.match(/filter\(,?has=/);
|
||||||
|
|
@ -122,7 +122,7 @@ function transform(template: string, params: TemplateParams): string {
|
||||||
const hasTemplate = shiftParams(template.substring(start, end), paramsCountBeforeHas);
|
const hasTemplate = shiftParams(template.substring(start, end), paramsCountBeforeHas);
|
||||||
const paramsCountInHas = countParams(hasTemplate);
|
const paramsCountInHas = countParams(hasTemplate);
|
||||||
const hasParams = params.slice(paramsCountBeforeHas, paramsCountBeforeHas + paramsCountInHas);
|
const hasParams = params.slice(paramsCountBeforeHas, paramsCountBeforeHas + paramsCountInHas);
|
||||||
const hasSelector = JSON.stringify(transform(hasTemplate, hasParams));
|
const hasSelector = JSON.stringify(transform(hasTemplate, hasParams, testIdAttributeName));
|
||||||
|
|
||||||
// Replace filter(has=...) with filter(has2=$5). Use has2 to avoid matching the same filter again.
|
// Replace filter(has=...) with filter(has2=$5). Use has2 to avoid matching the same filter again.
|
||||||
template = template.substring(0, start - 1) + `2=$${paramsCountBeforeHas + 1}` + shiftParams(template.substring(end), paramsCountInHas - 1);
|
template = template.substring(0, start - 1) + `2=$${paramsCountBeforeHas + 1}` + shiftParams(template.substring(end), paramsCountInHas - 1);
|
||||||
|
|
@ -139,7 +139,7 @@ function transform(template: string, params: TemplateParams): string {
|
||||||
.replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1')
|
.replace(/getbyrole\(([^)]+)\)/g, 'internal:role=$1')
|
||||||
.replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1')
|
.replace(/getbytext\(([^)]+)\)/g, 'internal:text=$1')
|
||||||
.replace(/getbylabel\(([^)]+)\)/g, 'internal:label=$1')
|
.replace(/getbylabel\(([^)]+)\)/g, 'internal:label=$1')
|
||||||
.replace(/getbytestid\(([^)]+)\)/g, 'internal:attr=[data-testid=$1s]')
|
.replace(/getbytestid\(([^)]+)\)/g, `internal:testid=[${testIdAttributeName}=$1s]`)
|
||||||
.replace(/getby(placeholder|alt|title)(?:text)?\(([^)]+)\)/g, 'internal:attr=[$1=$2]')
|
.replace(/getby(placeholder|alt|title)(?:text)?\(([^)]+)\)/g, 'internal:attr=[$1=$2]')
|
||||||
.replace(/first(\(\))?/g, 'nth=0')
|
.replace(/first(\(\))?/g, 'nth=0')
|
||||||
.replace(/last(\(\))?/g, 'nth=-1')
|
.replace(/last(\(\))?/g, 'nth=-1')
|
||||||
|
|
@ -158,7 +158,7 @@ function transform(template: string, params: TemplateParams): string {
|
||||||
t = t
|
t = t
|
||||||
.replace(/(?:r)\$(\d+)(i)?/g, (_, ordinal, suffix) => {
|
.replace(/(?:r)\$(\d+)(i)?/g, (_, ordinal, suffix) => {
|
||||||
const param = params[+ordinal - 1];
|
const param = params[+ordinal - 1];
|
||||||
if (t.startsWith('internal:attr') || t.startsWith('internal:role'))
|
if (t.startsWith('internal:attr') || t.startsWith('internal:testid') || t.startsWith('internal:role'))
|
||||||
return new RegExp(param.text) + (suffix || '');
|
return new RegExp(param.text) + (suffix || '');
|
||||||
return escapeForTextSelector(new RegExp(param.text, suffix), false);
|
return escapeForTextSelector(new RegExp(param.text, suffix), false);
|
||||||
})
|
})
|
||||||
|
|
@ -166,7 +166,7 @@ function transform(template: string, params: TemplateParams): string {
|
||||||
const param = params[+ordinal - 1];
|
const param = params[+ordinal - 1];
|
||||||
if (t.startsWith('internal:has='))
|
if (t.startsWith('internal:has='))
|
||||||
return param.text;
|
return param.text;
|
||||||
if (t.startsWith('internal:attr') || t.startsWith('internal:role'))
|
if (t.startsWith('internal:attr') || t.startsWith('internal:testid') || t.startsWith('internal:role'))
|
||||||
return escapeForAttributeSelector(param.text, suffix === 's');
|
return escapeForAttributeSelector(param.text, suffix === 's');
|
||||||
return escapeForTextSelector(param.text, suffix === 's');
|
return escapeForTextSelector(param.text, suffix === 's');
|
||||||
});
|
});
|
||||||
|
|
@ -174,14 +174,14 @@ function transform(template: string, params: TemplateParams): string {
|
||||||
}).join(' >> ');
|
}).join(' >> ');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function locatorOrSelectorAsSelector(language: Language, locator: string): string {
|
export function locatorOrSelectorAsSelector(language: Language, locator: string, testIdAttributeName: string): string {
|
||||||
try {
|
try {
|
||||||
parseSelector(locator);
|
parseSelector(locator);
|
||||||
return locator;
|
return locator;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const selector = parseLocator(locator);
|
const selector = parseLocator(locator, testIdAttributeName);
|
||||||
if (digestForComparison(asLocator(language, selector)) === digestForComparison(locator))
|
if (digestForComparison(asLocator(language, selector)) === digestForComparison(locator))
|
||||||
return selector;
|
return selector;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -167,7 +167,8 @@ export class Recorder implements InstrumentationListener {
|
||||||
mode: this._mode,
|
mode: this._mode,
|
||||||
actionPoint,
|
actionPoint,
|
||||||
actionSelector,
|
actionSelector,
|
||||||
language: this._currentLanguage
|
language: this._currentLanguage,
|
||||||
|
testIdAttributeName: this._contextRecorder.testIdAttributeName(),
|
||||||
};
|
};
|
||||||
return uiState;
|
return uiState;
|
||||||
});
|
});
|
||||||
|
|
@ -215,7 +216,7 @@ export class Recorder implements InstrumentationListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
setHighlightedSelector(language: Language, selector: string) {
|
setHighlightedSelector(language: Language, selector: string) {
|
||||||
this._highlightedSelector = locatorOrSelectorAsSelector(language, selector);
|
this._highlightedSelector = locatorOrSelectorAsSelector(language, selector, this._contextRecorder.testIdAttributeName());
|
||||||
this._refreshOverlay();
|
this._refreshOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -224,6 +225,10 @@ export class Recorder implements InstrumentationListener {
|
||||||
this._refreshOverlay();
|
this._refreshOverlay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setTestIdAttributeName(testIdAttributeName: string) {
|
||||||
|
this._contextRecorder.setTestIdAttributeName(testIdAttributeName);
|
||||||
|
}
|
||||||
|
|
||||||
setOutput(codegenId: string, outputFile: string | undefined) {
|
setOutput(codegenId: string, outputFile: string | undefined) {
|
||||||
this._contextRecorder.setOutput(codegenId, outputFile);
|
this._contextRecorder.setOutput(codegenId, outputFile);
|
||||||
}
|
}
|
||||||
|
|
@ -339,6 +344,7 @@ class ContextRecorder extends EventEmitter {
|
||||||
private _recorderSources: Source[];
|
private _recorderSources: Source[];
|
||||||
private _throttledOutputFile: ThrottledFile | null = null;
|
private _throttledOutputFile: ThrottledFile | null = null;
|
||||||
private _orderedLanguages: LanguageGenerator[] = [];
|
private _orderedLanguages: LanguageGenerator[] = [];
|
||||||
|
private _testIdAttributeName: string = 'data-testid';
|
||||||
|
|
||||||
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
constructor(context: BrowserContext, params: channels.BrowserContextRecorderSupplementEnableParams) {
|
||||||
super();
|
super();
|
||||||
|
|
@ -383,6 +389,14 @@ class ContextRecorder extends EventEmitter {
|
||||||
this._generator = generator;
|
this._generator = generator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testIdAttributeName() {
|
||||||
|
return this._testIdAttributeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTestIdAttributeName(testIdAttributeName: string) {
|
||||||
|
this._testIdAttributeName = testIdAttributeName;
|
||||||
|
}
|
||||||
|
|
||||||
setOutput(codegenId: string, outputFile?: string) {
|
setOutput(codegenId: string, outputFile?: string) {
|
||||||
const languages = new Set([
|
const languages = new Set([
|
||||||
new JavaLanguageGenerator(),
|
new JavaLanguageGenerator(),
|
||||||
|
|
@ -536,7 +550,7 @@ class ContextRecorder extends EventEmitter {
|
||||||
return;
|
return;
|
||||||
const utility = await parent._utilityContext();
|
const utility = await parent._utilityContext();
|
||||||
const injected = await utility.injectedScript();
|
const injected = await utility.injectedScript();
|
||||||
const selector = await injected.evaluate((injected, element) => injected.generateSelector(element as Element), frameElement);
|
const selector = await injected.evaluate((injected, element) => injected.generateSelector(element as Element, this._testIdAttributeName), frameElement);
|
||||||
return selector;
|
return selector;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,7 @@ export class Selectors {
|
||||||
readonly _builtinEnginesInMainWorld: Set<string>;
|
readonly _builtinEnginesInMainWorld: Set<string>;
|
||||||
readonly _engines: Map<string, { source: string, contentScript: boolean }>;
|
readonly _engines: Map<string, { source: string, contentScript: boolean }>;
|
||||||
readonly guid = `selectors@${createGuid()}`;
|
readonly guid = `selectors@${createGuid()}`;
|
||||||
|
private _testIdAttributeName: string = 'data-testid';
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
// Note: keep in sync with InjectedScript class.
|
// Note: keep in sync with InjectedScript class.
|
||||||
|
|
@ -46,7 +47,7 @@ export class Selectors {
|
||||||
'data-test-id', 'data-test-id:light',
|
'data-test-id', 'data-test-id:light',
|
||||||
'data-test', 'data-test:light',
|
'data-test', 'data-test:light',
|
||||||
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-text',
|
'nth', 'visible', 'internal:control', 'internal:has', 'internal:has-text',
|
||||||
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role',
|
'role', 'internal:attr', 'internal:label', 'internal:text', 'internal:role', 'internal:testid',
|
||||||
]);
|
]);
|
||||||
this._builtinEnginesInMainWorld = new Set([
|
this._builtinEnginesInMainWorld = new Set([
|
||||||
'_react', '_vue',
|
'_react', '_vue',
|
||||||
|
|
@ -65,6 +66,14 @@ export class Selectors {
|
||||||
this._engines.set(name, { source, contentScript });
|
this._engines.set(name, { source, contentScript });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testIdAttributeName(): string {
|
||||||
|
return this._testIdAttributeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTestIdAttributeName(testIdAttributeName: string) {
|
||||||
|
this._testIdAttributeName = testIdAttributeName;
|
||||||
|
}
|
||||||
|
|
||||||
unregisterAll() {
|
unregisterAll() {
|
||||||
this._engines.clear();
|
this._engines.clear();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ function getByAttributeTextSelector(attrName: string, text: string | RegExp, opt
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getByTestIdSelector(testIdAttributeName: string, testId: string): string {
|
export function getByTestIdSelector(testIdAttributeName: string, testId: string): string {
|
||||||
return getByAttributeTextSelector(testIdAttributeName, testId, { exact: true });
|
return `internal:testid=[${testIdAttributeName}=${escapeForAttributeSelector(testId, true)}]`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
export function getByLabelSelector(text: string | RegExp, options?: { exact?: boolean }): string {
|
||||||
|
|
|
||||||
|
|
@ -653,9 +653,10 @@ export type DebugControllerNavigateOptions = {
|
||||||
export type DebugControllerNavigateResult = void;
|
export type DebugControllerNavigateResult = void;
|
||||||
export type DebugControllerSetRecorderModeParams = {
|
export type DebugControllerSetRecorderModeParams = {
|
||||||
mode: 'inspecting' | 'recording' | 'none',
|
mode: 'inspecting' | 'recording' | 'none',
|
||||||
|
testIdAttributeName?: string,
|
||||||
};
|
};
|
||||||
export type DebugControllerSetRecorderModeOptions = {
|
export type DebugControllerSetRecorderModeOptions = {
|
||||||
|
testIdAttributeName?: string,
|
||||||
};
|
};
|
||||||
export type DebugControllerSetRecorderModeResult = void;
|
export type DebugControllerSetRecorderModeResult = void;
|
||||||
export type DebugControllerHighlightParams = {
|
export type DebugControllerHighlightParams = {
|
||||||
|
|
@ -763,6 +764,7 @@ export interface SelectorsEventTarget {
|
||||||
export interface SelectorsChannel extends SelectorsEventTarget, Channel {
|
export interface SelectorsChannel extends SelectorsEventTarget, Channel {
|
||||||
_type_Selectors: boolean;
|
_type_Selectors: boolean;
|
||||||
register(params: SelectorsRegisterParams, metadata?: Metadata): Promise<SelectorsRegisterResult>;
|
register(params: SelectorsRegisterParams, metadata?: Metadata): Promise<SelectorsRegisterResult>;
|
||||||
|
setTestIdAttributeName(params: SelectorsSetTestIdAttributeNameParams, metadata?: Metadata): Promise<SelectorsSetTestIdAttributeNameResult>;
|
||||||
}
|
}
|
||||||
export type SelectorsRegisterParams = {
|
export type SelectorsRegisterParams = {
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -773,6 +775,13 @@ export type SelectorsRegisterOptions = {
|
||||||
contentScript?: boolean,
|
contentScript?: boolean,
|
||||||
};
|
};
|
||||||
export type SelectorsRegisterResult = void;
|
export type SelectorsRegisterResult = void;
|
||||||
|
export type SelectorsSetTestIdAttributeNameParams = {
|
||||||
|
testIdAttributeName: string,
|
||||||
|
};
|
||||||
|
export type SelectorsSetTestIdAttributeNameOptions = {
|
||||||
|
|
||||||
|
};
|
||||||
|
export type SelectorsSetTestIdAttributeNameResult = void;
|
||||||
|
|
||||||
export interface SelectorsEvents {
|
export interface SelectorsEvents {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -692,6 +692,7 @@ DebugController:
|
||||||
- inspecting
|
- inspecting
|
||||||
- recording
|
- recording
|
||||||
- none
|
- none
|
||||||
|
testIdAttributeName: string?
|
||||||
|
|
||||||
highlight:
|
highlight:
|
||||||
parameters:
|
parameters:
|
||||||
|
|
@ -796,6 +797,9 @@ Selectors:
|
||||||
source: string
|
source: string
|
||||||
contentScript: boolean?
|
contentScript: boolean?
|
||||||
|
|
||||||
|
setTestIdAttributeName:
|
||||||
|
parameters:
|
||||||
|
testIdAttributeName: string
|
||||||
|
|
||||||
BrowserType:
|
BrowserType:
|
||||||
type: interface
|
type: interface
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ export type UIState = {
|
||||||
actionPoint?: Point;
|
actionPoint?: Point;
|
||||||
actionSelector?: string;
|
actionSelector?: string;
|
||||||
language: 'javascript' | 'python' | 'java' | 'csharp';
|
language: 'javascript' | 'python' | 'java' | 'csharp';
|
||||||
|
testIdAttributeName: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';
|
export type CallLogStatus = 'in-progress' | 'done' | 'error' | 'paused';
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ export class Backend extends EventEmitter {
|
||||||
await this._send('navigate', params);
|
await this._send('navigate', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
async setMode(params: { mode: 'none' | 'inspecting' | 'recording', language?: string, file?: string }) {
|
async setMode(params: { mode: 'none' | 'inspecting' | 'recording', language?: string, file?: string, testIdAttributeName?: string }) {
|
||||||
await this._send('setRecorderMode', params);
|
await this._send('setRecorderMode', params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -182,3 +182,33 @@ test('test', async ({ page }) => {
|
||||||
await page.getByRole('button').click();
|
await page.getByRole('button').click();
|
||||||
expect(events).toHaveLength(length);
|
expect(events).toHaveLength(length);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should record custom data-testid', async ({ backend, connectedBrowser }) => {
|
||||||
|
const events = [];
|
||||||
|
backend.on('sourceChanged', event => events.push(event));
|
||||||
|
|
||||||
|
await backend.setMode({ mode: 'recording', testIdAttributeName: 'data-custom-id' });
|
||||||
|
|
||||||
|
const context = await connectedBrowser._newContextForReuse();
|
||||||
|
const [page] = context.pages();
|
||||||
|
|
||||||
|
await page.setContent(`<div data-custom-id='one'>One</div>`);
|
||||||
|
await page.locator('div').click();
|
||||||
|
|
||||||
|
await expect.poll(() => events[events.length - 1]).toEqual({
|
||||||
|
header: `import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('test', async ({ page }) => {`,
|
||||||
|
footer: `});`,
|
||||||
|
actions: [
|
||||||
|
` await page.goto('about:blank');`,
|
||||||
|
` await page.getByTestId('one').click();`,
|
||||||
|
],
|
||||||
|
text: `import { test, expect } from '@playwright/test';
|
||||||
|
|
||||||
|
test('test', async ({ page }) => {
|
||||||
|
await page.goto('about:blank');
|
||||||
|
await page.getByTestId('one').click();
|
||||||
|
});`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ function generateForSelector(selector: string) {
|
||||||
const result: any = {};
|
const result: any = {};
|
||||||
for (const lang of ['javascript', 'python', 'java', 'csharp']) {
|
for (const lang of ['javascript', 'python', 'java', 'csharp']) {
|
||||||
const locatorString = asLocator(lang, selector, false);
|
const locatorString = asLocator(lang, selector, false);
|
||||||
expect.soft(parseLocator(lang, locatorString), lang + ' mismatch').toBe(selector);
|
expect.soft(parseLocator(lang, locatorString, 'data-testid'), lang + ' mismatch').toBe(selector);
|
||||||
result[lang] = locatorString;
|
result[lang] = locatorString;
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,31 @@ it.describe('selector generator', () => {
|
||||||
|
|
||||||
it('should prefer data-testid', async ({ page }) => {
|
it('should prefer data-testid', async ({ page }) => {
|
||||||
await page.setContent(`<div>Text</div><div>Text</div><div data-testid=a>Text</div><div>Text</div>`);
|
await page.setContent(`<div>Text</div><div>Text</div><div data-testid=a>Text</div><div>Text</div>`);
|
||||||
expect(await generate(page, '[data-testid="a"]')).toBe('internal:attr=[data-testid=\"a\"s]');
|
expect(await generate(page, '[data-testid="a"]')).toBe('internal:testid=[data-testid=\"a\"s]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use data-testid in strict errors', async ({ page, playwright }) => {
|
||||||
|
playwright.selectors.setTestIdAttribute('data-custom-id');
|
||||||
|
await page.setContent(`
|
||||||
|
<div>
|
||||||
|
<div></div>
|
||||||
|
<div>
|
||||||
|
<div></div>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class='foo bar:0' data-custom-id='One'>
|
||||||
|
</div>
|
||||||
|
<div class='foo bar:1' data-custom-id='Two'>
|
||||||
|
</div>
|
||||||
|
</div>`);
|
||||||
|
const error = await page.locator('.foo').hover().catch(e => e);
|
||||||
|
expect(error.message).toContain('strict mode violation');
|
||||||
|
expect(error.message).toContain('<div class=\"foo bar:0');
|
||||||
|
expect(error.message).toContain('<div class=\"foo bar:1');
|
||||||
|
expect(error.message).toContain(`aka getByTestId('One')`);
|
||||||
|
expect(error.message).toContain(`aka getByTestId('Two')`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle first non-unique data-testid', async ({ page }) => {
|
it('should handle first non-unique data-testid', async ({ page }) => {
|
||||||
|
|
@ -99,7 +123,7 @@ it.describe('selector generator', () => {
|
||||||
<div data-testid=a>
|
<div data-testid=a>
|
||||||
Text
|
Text
|
||||||
</div>`);
|
</div>`);
|
||||||
expect(await generate(page, 'div[mark="1"]')).toBe('internal:attr=[data-testid=\"a\"s] >> nth=0');
|
expect(await generate(page, 'div[mark="1"]')).toBe('internal:testid=[data-testid=\"a\"s] >> nth=0');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle second non-unique data-testid', async ({ page }) => {
|
it('should handle second non-unique data-testid', async ({ page }) => {
|
||||||
|
|
@ -110,7 +134,7 @@ it.describe('selector generator', () => {
|
||||||
<div data-testid=a mark=1>
|
<div data-testid=a mark=1>
|
||||||
Text
|
Text
|
||||||
</div>`);
|
</div>`);
|
||||||
expect(await generate(page, 'div[mark="1"]')).toBe(`internal:attr=[data-testid=\"a\"s] >> nth=1`);
|
expect(await generate(page, 'div[mark="1"]')).toBe(`internal:testid=[data-testid=\"a\"s] >> nth=1`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use readable id', async ({ page }) => {
|
it('should use readable id', async ({ page }) => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue