Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/playwright-core/src/client/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export class Frame extends ChannelOwner<channels.FrameChannel> implements api.Fr
}

getByTestId(testId: string): Locator {
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
return this.locator(getByTestIdSelector(testIdAttributeName(), testId));
}

getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
Expand Down
12 changes: 8 additions & 4 deletions packages/playwright-core/src/client/locator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ export class Locator implements api.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 {
Expand Down Expand Up @@ -340,7 +340,7 @@ export class FrameLocator implements api.FrameLocator {
}

getByTestId(testId: string): Locator {
return this.locator(getByTestIdSelector(testIdAttributeName, testId));
return this.locator(getByTestIdSelector(testIdAttributeName(), testId));
}

getByAltText(text: string | RegExp, options?: { exact?: boolean }): Locator {
Expand Down Expand Up @@ -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) {
testIdAttributeName = attributeName;
_testIdAttributeName = attributeName;
}
7 changes: 5 additions & 2 deletions packages/playwright-core/src/client/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import type * as channels from '@protocol/channels';
import { ChannelOwner } from './channelOwner';
import type { SelectorEngine } from './types';
import type * as api from '../../types/types';
import { setTestIdAttribute } from './locator';
import { setTestIdAttribute, testIdAttributeName } from './locator';

export class Selectors implements api.Selectors {
private _channels = new Set<SelectorsOwner>();
Expand All @@ -35,13 +35,16 @@ export class Selectors implements api.Selectors {

setTestIdAttribute(attributeName: string) {
setTestIdAttribute(attributeName);
for (const channel of this._channels)
channel._channel.setTestIdAttributeName({ testIdAttributeName: attributeName }).catch(() => {});
}

_addChannel(channel: SelectorsOwner) {
this._channels.add(channel);
for (const params of this._registrations) {
// 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(() => {});
}
}

Expand Down
5 changes: 5 additions & 0 deletions packages/playwright-core/src/protocol/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ scheme.DebugControllerNavigateParams = tObject({
scheme.DebugControllerNavigateResult = tOptional(tObject({}));
scheme.DebugControllerSetRecorderModeParams = tObject({
mode: tEnum(['inspecting', 'recording', 'none']),
testIdAttributeName: tOptional(tString),
});
scheme.DebugControllerSetRecorderModeResult = tOptional(tObject({}));
scheme.DebugControllerHighlightParams = tObject({
Expand Down Expand Up @@ -425,6 +426,10 @@ scheme.SelectorsRegisterParams = tObject({
contentScript: tOptional(tBoolean),
});
scheme.SelectorsRegisterResult = tOptional(tObject({}));
scheme.SelectorsSetTestIdAttributeNameParams = tObject({
testIdAttributeName: tString,
});
scheme.SelectorsSetTestIdAttributeNameResult = tOptional(tObject({}));
scheme.BrowserTypeInitializer = tObject({
executablePath: tString,
name: tString,
Expand Down
7 changes: 5 additions & 2 deletions packages/playwright-core/src/server/debugController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ export class DebugController extends SdkObject {
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.
await this._closeBrowsersWithoutPages();

Expand All @@ -114,8 +114,11 @@ export class DebugController extends SdkObject {
// Toggle the mode.
for (const recorder of await this._allRecorders()) {
recorder.hideHighlightedSelecor();
if (params.mode === 'recording')
if (params.mode === 'recording') {
recorder.setOutput(this._codegenId, params.file);
if (params.testIdAttributeName)
recorder.setTestIdAttributeName(params.testIdAttributeName);
}
recorder.setMode(params.mode);
}
this.setAutoCloseEnabled(true);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,8 @@ export class SelectorsDispatcher extends Dispatcher<Selectors, channels.Selector
async register(params: channels.SelectorsRegisterParams): Promise<void> {
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);
}
}
1 change: 1 addition & 0 deletions packages/playwright-core/src/server/dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export class FrameExecutionContext extends js.ExecutionContext {
return new module.exports(
${isUnderTest()},
"${sdkLanguage}",
${JSON.stringify(this.frame._page.selectors.testIdAttributeName())},
${this.frame._page._delegate.rafCountForStablePosition()},
"${this.frame._page._browserContext._browser.options.name}",
[${custom.join(',\n')}]
Expand Down
6 changes: 3 additions & 3 deletions packages/playwright-core/src/server/injected/consoleApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ class Locator {
self.locator = (selector: string, options?: { hasText?: string | RegExp, has?: Locator }): Locator => {
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.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));
Expand Down Expand Up @@ -114,13 +114,13 @@ class ConsoleAPI {
private _selector(element: Element) {
if (!(element instanceof 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) {
if (!(element instanceof 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);
}

Expand Down
15 changes: 11 additions & 4 deletions packages/playwright-core/src/server/injected/injectedScript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,12 @@ export class InjectedScript {
private _highlight: Highlight | undefined;
readonly isUnderTest: boolean;
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._sdkLanguage = sdkLanguage;
this._testIdAttributeNameForStrictErrorAndConsoleCodegen = testIdAttributeNameForStrictErrorAndConsoleCodegen;
this._evaluator = new SelectorEvaluatorImpl(new Map());

this._engines = new Map();
Expand Down Expand Up @@ -113,6 +115,7 @@ export class InjectedScript {
this._engines.set('internal:text', this._createTextEngine(true, true));
this._engines.set('internal:has-text', this._createInternalHasTextEngine());
this._engines.set('internal:attr', this._createNamedAttributeEngine());
this._engines.set('internal:testid', this._createNamedAttributeEngine());
this._engines.set('internal:role', RoleEngine);

for (const { name, engine } of customEngines)
Expand All @@ -132,6 +135,10 @@ export class InjectedScript {
return globalThis.eval(expression);
}

testIdAttributeNameForStrictErrorAndConsoleCodegen(): string {
return this._testIdAttributeNameForStrictErrorAndConsoleCodegen;
}

parseSelector(selector: string): ParsedSelector {
const result = parseSelector(selector);
for (const name of allEngineNames(result)) {
Expand All @@ -141,8 +148,8 @@ export class InjectedScript {
return result;
}

generateSelector(targetElement: Element): string {
return generateSelector(this, targetElement, true).selector;
generateSelector(targetElement: Element, testIdAttributeName: string): string {
return generateSelector(this, targetElement, true, testIdAttributeName).selector;
}

querySelector(selector: ParsedSelector, root: Node, strict: boolean): Element | undefined {
Expand Down Expand Up @@ -1016,7 +1023,7 @@ export class InjectedScript {
strictModeViolationError(selector: ParsedSelector, matches: Element[]): Error {
const infos = matches.slice(0, 10).map(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)}`);
if (infos.length < matches.length)
Expand Down
8 changes: 5 additions & 3 deletions packages/playwright-core/src/server/injected/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class Recorder {
private _actionPoint: Point | undefined;
private _actionSelector: string | undefined;
private _highlight: Highlight;
private _testIdAttributeName: string = 'data-testid';

constructor(injectedScript: InjectedScript) {
this._injectedScript = injectedScript;
Expand Down Expand Up @@ -94,7 +95,8 @@ class Recorder {
return;
}

const { mode, actionPoint, actionSelector, language } = state;
const { mode, actionPoint, actionSelector, language, testIdAttributeName } = state;
this._testIdAttributeName = testIdAttributeName;
this._highlight.setLanguage(language);
if (mode !== this._mode) {
this._mode = mode;
Expand Down Expand Up @@ -238,7 +240,7 @@ class Recorder {
if (this._mode === 'none')
return;
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;
if (userGesture)
this._hoveredElement = activeElement as HTMLElement | null;
Expand All @@ -252,7 +254,7 @@ class Recorder {
return;
}
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))
return;
this._hoveredModel = selector ? { selector, elements } : null;
Expand Down
19 changes: 9 additions & 10 deletions packages/playwright-core/src/server/injected/selectorGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
try {
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 selector = joinTokens(bestTokens);
const parsedSelector = injectedScript.parseSelector(selector);
Expand All @@ -68,7 +68,7 @@ function filterRegexTokens(textCandidates: SelectorToken[][]): SelectorToken[][]
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)
return [{ engine: 'css', selector: 'html', score: 1 }];

Expand All @@ -81,7 +81,7 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
// Do not use regex for parent elements (for performance).
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.
let result = chooseFirstSelector(injectedScript, targetElement.ownerDocument, element, [...textCandidates, ...noTextCandidates], allowNthMatch, strict);
Expand Down Expand Up @@ -144,14 +144,13 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
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[] = [];
if (element.getAttribute(testIdAttributeName))
candidates.push({ engine: 'internal:testid', selector: `[${testIdAttributeName}=${escapeForAttributeSelector(element.getAttribute(testIdAttributeName)!, true)}]`, score: 1 });

if (element.getAttribute('data-testid'))
candidates.push({ engine: 'internal:attr', selector: `[data-testid=${escapeForAttributeSelector(element.getAttribute('data-testid')!, true)}]`, score: 1 });

for (const attr of ['data-test-id', 'data-test']) {
if (element.getAttribute(attr))
for (const attr of ['data-testid', 'data-test-id', 'data-test']) {
if (attr !== testIdAttributeName && element.getAttribute(attr))
candidates.push({ engine: 'css', selector: `[${attr}=${quoteAttributeValue(element.getAttribute(attr)!)}]`, score: 2 });
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
tokens.push(factory.generateLocator(base, 'role', attrSelector.name, { attrs }));
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') {
const attrSelector = parseAttributeSelector(part.body as string, true);
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 exact = !!caseSensitive;
if (name === 'placeholder') {
Expand Down
Loading