fix(codegen): update priorites in selector generator (#18688)

- prefer `role=checkbox` over `input[type=checkbox]`
- prefer `#id` over `input[type=checkbox]` and `role=checkbox`
- prefer `text=foo` over `internal:has-text=foo`
- ignore `none` and `presentation` roles
- remove non-strict support
This commit is contained in:
Dmitry Gozman 2022-11-09 17:22:13 -08:00 committed by GitHub
parent 6d491f928d
commit cafa558845
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 153 additions and 159 deletions

View File

@ -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, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector; return generateSelector(this._injectedScript, element, 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, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector; const selector = generateSelector(this._injectedScript, element, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector;
return asLocator(language || 'javascript', selector); return asLocator(language || 'javascript', selector);
} }

View File

@ -149,7 +149,7 @@ export class InjectedScript {
} }
generateSelector(targetElement: Element, testIdAttributeName: string): string { generateSelector(targetElement: Element, testIdAttributeName: string): string {
return generateSelector(this, targetElement, true, testIdAttributeName).selector; return generateSelector(this, targetElement, testIdAttributeName).selector;
} }
querySelector(selector: ParsedSelector, root: Node, strict: boolean): Element | undefined { querySelector(selector: ParsedSelector, root: Node, strict: boolean): Element | undefined {

View File

@ -240,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, this._testIdAttributeName) : null; const result = activeElement ? generateSelector(this._injectedScript, activeElement, 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;
@ -254,7 +254,7 @@ class Recorder {
return; return;
} }
const hoveredElement = this._hoveredElement; const hoveredElement = this._hoveredElement;
const { selector, elements } = generateSelector(this._injectedScript, hoveredElement, true, this._testIdAttributeName); const { selector, elements } = generateSelector(this._injectedScript, hoveredElement, 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;

View File

@ -27,7 +27,20 @@ type SelectorToken = {
const cacheAllowText = new Map<Element, SelectorToken[] | null>(); const cacheAllowText = new Map<Element, SelectorToken[] | null>();
const cacheDisallowText = new Map<Element, SelectorToken[] | null>(); const cacheDisallowText = new Map<Element, SelectorToken[] | null>();
const kTestIdScore = 1; // testIdAttributeName
const kOtherTestIdScore = 2; // other data-test* attributes
const kPlaceholderScore = 3;
const kLabelScore = 3;
const kRoleWithNameScore = 5;
const kAltTextScore = 10;
const kTextScore = 15;
const kCSSIdScore = 100;
const kRoleWithoutNameScore = 140;
const kCSSInputTypeNameScore = 150;
const kCSSTagNameScore = 200;
const kNthScore = 1000; const kNthScore = 1000;
const kCSSFallbackScore = 10000000;
export function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } { export function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } {
try { try {
@ -44,12 +57,12 @@ export function querySelector(injectedScript: InjectedScript, selector: string,
} }
} }
export function generateSelector(injectedScript: InjectedScript, targetElement: Element, strict: boolean, testIdAttributeName: string): { selector: string, elements: Element[] } { export function generateSelector(injectedScript: InjectedScript, targetElement: Element, 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, testIdAttributeName); const targetTokens = generateSelectorFor(injectedScript, targetElement, testIdAttributeName);
const bestTokens = targetTokens || cssFallback(injectedScript, targetElement, strict); const bestTokens = targetTokens || cssFallback(injectedScript, targetElement);
const selector = joinTokens(bestTokens); const selector = joinTokens(bestTokens);
const parsedSelector = injectedScript.parseSelector(selector); const parsedSelector = injectedScript.parseSelector(selector);
return { return {
@ -68,7 +81,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, testIdAttributeName: string): SelectorToken[] | null { function generateSelectorFor(injectedScript: InjectedScript, targetElement: Element, 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 }];
@ -84,7 +97,7 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
const noTextCandidates = buildCandidates(injectedScript, element, testIdAttributeName, 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);
// Do not use regex for chained selectors (for performance). // Do not use regex for chained selectors (for performance).
textCandidates = filterRegexTokens(textCandidates); textCandidates = filterRegexTokens(textCandidates);
@ -114,7 +127,7 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
if (result && combineScores([...parentTokens, ...bestPossibleInParent]) >= combineScores(result)) if (result && combineScores([...parentTokens, ...bestPossibleInParent]) >= combineScores(result))
continue; continue;
// Update the best candidate that finds "element" in the "parent". // Update the best candidate that finds "element" in the "parent".
bestPossibleInParent = chooseFirstSelector(injectedScript, parent, element, candidates, allowNthMatch, strict); bestPossibleInParent = chooseFirstSelector(injectedScript, parent, element, candidates, allowNthMatch);
if (!bestPossibleInParent) if (!bestPossibleInParent)
return; return;
const combined = [...parentTokens, ...bestPossibleInParent]; const combined = [...parentTokens, ...bestPossibleInParent];
@ -147,52 +160,52 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
function buildCandidates(injectedScript: InjectedScript, element: Element, testIdAttributeName: string, 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)) if (element.getAttribute(testIdAttributeName))
candidates.push({ engine: 'internal:testid', selector: `[${testIdAttributeName}=${escapeForAttributeSelector(element.getAttribute(testIdAttributeName)!, true)}]`, score: 1 }); candidates.push({ engine: 'internal:testid', selector: `[${testIdAttributeName}=${escapeForAttributeSelector(element.getAttribute(testIdAttributeName)!, true)}]`, score: kTestIdScore });
for (const attr of ['data-testid', 'data-test-id', 'data-test']) { for (const attr of ['data-testid', 'data-test-id', 'data-test']) {
if (attr !== testIdAttributeName && element.getAttribute(attr)) if (attr !== testIdAttributeName && 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: kOtherTestIdScore });
} }
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') { if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
const input = element as HTMLInputElement | HTMLTextAreaElement; const input = element as HTMLInputElement | HTMLTextAreaElement;
if (input.placeholder) if (input.placeholder)
candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, false)}]`, score: 3 }); candidates.push({ engine: 'internal:attr', selector: `[placeholder=${escapeForAttributeSelector(input.placeholder, false)}]`, score: kPlaceholderScore });
const label = input.labels?.[0]; const label = input.labels?.[0];
if (label) { if (label) {
const labelText = elementText(injectedScript._evaluator._cacheText, label).full.trim(); const labelText = elementText(injectedScript._evaluator._cacheText, label).full.trim();
candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, false), score: 3 }); candidates.push({ engine: 'internal:label', selector: escapeForTextSelector(labelText, false), score: kLabelScore });
} }
} }
const ariaRole = getAriaRole(element); const ariaRole = getAriaRole(element);
if (ariaRole) { if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
const ariaName = getElementAccessibleName(element, false, accessibleNameCache); const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
if (ariaName) if (ariaName)
candidates.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: 3 }); candidates.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScore });
else else
candidates.push({ engine: 'internal:role', selector: ariaRole, score: 150 }); candidates.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore });
} }
if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName)) if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName))
candidates.push({ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!, false)}]`, score: 10 }); candidates.push({ engine: 'internal:attr', selector: `[alt=${escapeForAttributeSelector(element.getAttribute('alt')!, false)}]`, score: kAltTextScore });
if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName)) if (element.getAttribute('name') && ['BUTTON', 'FORM', 'FIELDSET', 'FRAME', 'IFRAME', 'INPUT', 'KEYGEN', 'OBJECT', 'OUTPUT', 'SELECT', 'TEXTAREA', 'MAP', 'META', 'PARAM'].includes(element.nodeName))
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: 50 }); candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[name=${quoteAttributeValue(element.getAttribute('name')!)}]`, score: kCSSInputTypeNameScore });
if (['INPUT', 'TEXTAREA'].includes(element.nodeName) && element.getAttribute('type') !== 'hidden') { if (['INPUT', 'TEXTAREA'].includes(element.nodeName) && element.getAttribute('type') !== 'hidden') {
if (element.getAttribute('type')) if (element.getAttribute('type'))
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[type=${quoteAttributeValue(element.getAttribute('type')!)}]`, score: 50 }); candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[type=${quoteAttributeValue(element.getAttribute('type')!)}]`, score: kCSSInputTypeNameScore });
} }
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName)) if (['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && element.getAttribute('type') !== 'hidden')
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: 50 }); candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSInputTypeNameScore + 1 });
const idAttr = element.getAttribute('id'); const idAttr = element.getAttribute('id');
if (idAttr && !isGuidLike(idAttr)) if (idAttr && !isGuidLike(idAttr))
candidates.push({ engine: 'css', selector: makeSelectorForId(idAttr), score: 100 }); candidates.push({ engine: 'css', selector: makeSelectorForId(idAttr), score: kCSSIdScore });
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: 200 }); candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore });
return candidates; return candidates;
} }
@ -207,20 +220,20 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
const escaped = escapeForTextSelector(text, false); const escaped = escapeForTextSelector(text, false);
if (isTargetNode) if (isTargetNode)
candidates.push([{ engine: 'internal:text', selector: escaped, score: 10 }]); candidates.push([{ engine: 'internal:text', selector: escaped, score: kTextScore }]);
const ariaRole = getAriaRole(element); const ariaRole = getAriaRole(element);
const candidate: SelectorToken[] = []; const candidate: SelectorToken[] = [];
if (ariaRole) { if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
const ariaName = getElementAccessibleName(element, false, accessibleNameCache); const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
if (ariaName) if (ariaName)
candidate.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: 10 }); candidate.push({ engine: 'internal:role', selector: `${ariaRole}[name=${escapeForAttributeSelector(ariaName, true)}]`, score: kRoleWithNameScore });
else else
candidate.push({ engine: 'internal:role', selector: ariaRole, score: 10 }); candidate.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore });
} else { } else {
candidate.push({ engine: 'css', selector: element.nodeName.toLowerCase(), score: 10 }); candidate.push({ engine: 'css', selector: element.nodeName.toLowerCase(), score: kCSSTagNameScore });
} }
candidate.push({ engine: 'internal:has-text', selector: escaped, score: 0 }); candidate.push({ engine: 'internal:has-text', selector: escaped, score: kTextScore });
candidates.push(candidate); candidates.push(candidate);
return candidates; return candidates;
} }
@ -239,8 +252,7 @@ function makeSelectorForId(id: string) {
return /^[a-zA-Z][a-zA-Z0-9\-\_]+$/.test(id) ? '#' + id : `[id="${cssEscape(id)}"]`; return /^[a-zA-Z][a-zA-Z0-9\-\_]+$/.test(id) ? '#' + id : `[id="${cssEscape(id)}"]`;
} }
function cssFallback(injectedScript: InjectedScript, targetElement: Element, strict: boolean): SelectorToken[] { function cssFallback(injectedScript: InjectedScript, targetElement: Element): SelectorToken[] {
const kFallbackScore = 10000000;
const root: Node = targetElement.ownerDocument; const root: Node = targetElement.ownerDocument;
const tokens: string[] = []; const tokens: string[] = [];
@ -255,9 +267,7 @@ function cssFallback(injectedScript: InjectedScript, targetElement: Element, str
} }
function makeStrict(selector: string): SelectorToken[] { function makeStrict(selector: string): SelectorToken[] {
const token = { engine: 'css', selector, score: kFallbackScore }; const token = { engine: 'css', selector, score: kCSSFallbackScore };
if (!strict)
return [token];
const parsedSelector = injectedScript.parseSelector(selector); const parsedSelector = injectedScript.parseSelector(selector);
const elements = injectedScript.querySelectorAll(parsedSelector, targetElement.ownerDocument); const elements = injectedScript.querySelectorAll(parsedSelector, targetElement.ownerDocument);
if (elements.length === 1) if (elements.length === 1)
@ -340,7 +350,7 @@ function combineScores(tokens: SelectorToken[]): number {
return score; return score;
} }
function chooseFirstSelector(injectedScript: InjectedScript, scope: Element | Document, targetElement: Element, selectors: SelectorToken[][], allowNthMatch: boolean, strict: boolean): SelectorToken[] | null { function chooseFirstSelector(injectedScript: InjectedScript, scope: Element | Document, targetElement: Element, selectors: SelectorToken[][], allowNthMatch: boolean): SelectorToken[] | null {
const joined = selectors.map(tokens => ({ tokens, score: combineScores(tokens) })); const joined = selectors.map(tokens => ({ tokens, score: combineScores(tokens) }));
joined.sort((a, b) => a.score - b.score); joined.sort((a, b) => a.score - b.score);
@ -348,14 +358,13 @@ function chooseFirstSelector(injectedScript: InjectedScript, scope: Element | Do
for (const { tokens } of joined) { for (const { tokens } of joined) {
const parsedSelector = injectedScript.parseSelector(joinTokens(tokens)); const parsedSelector = injectedScript.parseSelector(joinTokens(tokens));
const result = injectedScript.querySelectorAll(parsedSelector, scope); const result = injectedScript.querySelectorAll(parsedSelector, scope);
const isStrictEnough = !strict || result.length === 1; if (result[0] === targetElement && result.length === 1) {
const index = result.indexOf(targetElement); // We are the only match - found the best selector.
if (index === 0 && isStrictEnough) {
// We are the first match - found the best selector.
return tokens; return tokens;
} }
// Otherwise, perhaps we can use nth=? // Otherwise, perhaps we can use nth=?
const index = result.indexOf(targetElement);
if (!allowNthMatch || bestWithIndex || index === -1 || result.length > 5) if (!allowNthMatch || bestWithIndex || index === -1 || result.length > 5)
continue; continue;

View File

@ -212,7 +212,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="input" name="name" oninput="console.log(input.value)"></input>`); await recorder.setContentAndWait(`<input id="input" name="name" oninput="console.log(input.value)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`); expect(locator).toBe(`locator('#input')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -221,18 +221,18 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="name"]').fill('John');`); await page.locator('#input').fill('John');`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[name=\\\"name\\\"]").fill("John");`); page.locator("#input").fill("John");`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[name=\\\"name\\\"]\").fill(\"John\")`); page.locator("#input").fill(\"John\")`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[name=\\\"name\\\"]\").fill(\"John\")`); await page.locator("#input").fill(\"John\")`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[name=\\\"name\\\"]\").FillAsync(\"John\");`); await page.Locator("#input").FillAsync(\"John\");`);
expect(message.text()).toBe('John'); expect(message.text()).toBe('John');
}); });
@ -243,7 +243,7 @@ test.describe('cli codegen', () => {
// In Japanese, "てすと" or "テスト" means "test". // In Japanese, "てすと" or "テスト" means "test".
await recorder.setContentAndWait(`<input id="input" name="name" oninput="input.value === 'てすと' && console.log(input.value)"></input>`); await recorder.setContentAndWait(`<input id="input" name="name" oninput="input.value === 'てすと' && console.log(input.value)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`); expect(locator).toBe(`locator('#input')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -255,18 +255,18 @@ test.describe('cli codegen', () => {
})() })()
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="name"]').fill('てすと');`); await page.locator('#input').fill('てすと');`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[name=\\\"name\\\"]").fill("てすと");`); page.locator("#input").fill("てすと");`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[name=\\\"name\\\"]\").fill(\"てすと\")`); page.locator("#input").fill(\"てすと\")`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[name=\\\"name\\\"]\").fill(\"てすと\")`); await page.locator("#input").fill(\"てすと\")`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[name=\\\"name\\\"]\").FillAsync(\"てすと\");`); await page.Locator("#input").FillAsync(\"てすと\");`);
expect(message.text()).toBe('てすと'); expect(message.text()).toBe('てすと');
}); });
@ -276,7 +276,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<textarea id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`); await recorder.setContentAndWait(`<textarea id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
const locator = await recorder.focusElement('textarea'); const locator = await recorder.focusElement('textarea');
expect(locator).toBe(`locator('textarea[name="name"]')`); expect(locator).toBe(`locator('#textarea')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -284,7 +284,7 @@ test.describe('cli codegen', () => {
page.fill('textarea', 'John') page.fill('textarea', 'John')
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('textarea[name="name"]').fill('John');`); await page.locator('#textarea').fill('John');`);
expect(message.text()).toBe('John'); expect(message.text()).toBe('John');
}); });
@ -294,7 +294,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input name="name" onkeypress="console.log('press')"></input>`); await recorder.setContentAndWait(`<input name="name" onkeypress="console.log('press')"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`); expect(locator).toBe(`getByRole('textbox')`);
const messages: any[] = []; const messages: any[] = [];
page.on('console', message => messages.push(message)); page.on('console', message => messages.push(message));
@ -305,19 +305,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="name"]').press('Shift+Enter');`); await page.getByRole('textbox').press('Shift+Enter');`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[name=\\\"name\\\"]").press("Shift+Enter");`); page.getByRole(AriaRole.TEXTBOX).press("Shift+Enter");`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[name=\\\"name\\\"]\").press(\"Shift+Enter\")`); page.get_by_role("textbox").press("Shift+Enter")`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[name=\\\"name\\\"]\").press(\"Shift+Enter\")`); await page.get_by_role("textbox").press("Shift+Enter")`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[name=\\\"name\\\"]\").PressAsync(\"Shift+Enter\");`); await page.GetByRole(AriaRole.Textbox).PressAsync("Shift+Enter");`);
expect(messages[0].text()).toBe('press'); expect(messages[0].text()).toBe('press');
}); });
@ -357,7 +357,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('press:' + event.key)"></input>`); await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('press:' + event.key)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`); expect(locator).toBe(`getByRole('textbox')`);
const messages: any[] = []; const messages: any[] = [];
page.on('console', message => { page.on('console', message => {
@ -369,7 +369,7 @@ test.describe('cli codegen', () => {
page.press('input', 'ArrowDown') page.press('input', 'ArrowDown')
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="name"]').press('ArrowDown');`); await page.getByRole('textbox').press('ArrowDown');`);
expect(messages[0].text()).toBe('press:ArrowDown'); expect(messages[0].text()).toBe('press:ArrowDown');
}); });
@ -379,7 +379,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('down:' + event.key)" onkeyup="console.log('up:' + event.key)"></input>`); await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('down:' + event.key)" onkeyup="console.log('up:' + event.key)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="name"]')`); expect(locator).toBe(`getByRole('textbox')`);
const messages: any[] = []; const messages: any[] = [];
page.on('console', message => { page.on('console', message => {
@ -392,7 +392,7 @@ test.describe('cli codegen', () => {
page.press('input', 'ArrowDown') page.press('input', 'ArrowDown')
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="name"]').press('ArrowDown');`); await page.getByRole('textbox').press('ArrowDown');`);
expect(messages.length).toBe(2); expect(messages.length).toBe(2);
expect(messages[0].text()).toBe('down:ArrowDown'); expect(messages[0].text()).toBe('down:ArrowDown');
expect(messages[1].text()).toBe('up:ArrowDown'); expect(messages[1].text()).toBe('up:ArrowDown');
@ -404,7 +404,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`); expect(locator).toBe(`locator('#checkbox')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -413,19 +413,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="accept"]').check();`); await page.locator('#checkbox').check();`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[name=\\\"accept\\\"]").check();`); page.locator("#checkbox").check();`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[name=\\\"accept\\\"]\").check()`); page.locator("#checkbox").check()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[name=\\\"accept\\\"]\").check()`); await page.locator("#checkbox").check()`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[name=\\\"accept\\\"]\").CheckAsync();`); await page.Locator("#checkbox").CheckAsync();`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
}); });
@ -436,7 +436,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="radio" name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="radio" name="accept" onchange="console.log(checkbox.checked)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`); expect(locator).toBe(`locator('#checkbox')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -445,7 +445,7 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="accept"]').check();`); await page.locator('#checkbox').check();`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
}); });
@ -455,7 +455,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" name="accept" onchange="console.log(checkbox.checked)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`); expect(locator).toBe(`locator('#checkbox')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -464,7 +464,7 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="accept"]').check();`); await page.locator('#checkbox').check();`);
expect(message.text()).toBe('true'); expect(message.text()).toBe('true');
}); });
@ -474,7 +474,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" checked name="accept" onchange="console.log(checkbox.checked)"></input>`); await recorder.setContentAndWait(`<input id="checkbox" type="checkbox" checked name="accept" onchange="console.log(checkbox.checked)"></input>`);
const locator = await recorder.focusElement('input'); const locator = await recorder.focusElement('input');
expect(locator).toBe(`locator('input[name="accept"]')`); expect(locator).toBe(`locator('#checkbox')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -483,19 +483,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[name="accept"]').uncheck();`); await page.locator('#checkbox').uncheck();`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[name=\\\"accept\\\"]").uncheck();`); page.locator("#checkbox").uncheck();`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[name=\\\"accept\\\"]\").uncheck()`); page.locator("#checkbox").uncheck()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[name=\\\"accept\\\"]\").uncheck()`); await page.locator("#checkbox").uncheck()`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[name=\\\"accept\\\"]\").UncheckAsync();`); await page.Locator("#checkbox").UncheckAsync();`);
expect(message.text()).toBe('false'); expect(message.text()).toBe('false');
}); });
@ -506,7 +506,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait('<select id="age" onchange="console.log(age.selectedOptions[0].value)"><option value="1"><option value="2"></select>'); await recorder.setContentAndWait('<select id="age" onchange="console.log(age.selectedOptions[0].value)"><option value="1"><option value="2"></select>');
const locator = await recorder.hoverOverElement('select'); const locator = await recorder.hoverOverElement('select');
expect(locator).toBe(`locator('select')`); expect(locator).toBe(`locator('#age')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -515,19 +515,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('select').selectOption('2');`); await page.locator('#age').selectOption('2');`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("select").selectOption("2");`); page.locator("#age").selectOption("2");`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"select\").select_option(\"2\")`); page.locator("#age").select_option("2")`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"select\").select_option(\"2\")`); await page.locator("#age").select_option("2")`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"select\").SelectOptionAsync(new[] { \"2\" });`); await page.Locator("#age").SelectOptionAsync(new[] { "2" });`);
expect(message.text()).toBe('2'); expect(message.text()).toBe('2');
}); });
@ -624,8 +624,8 @@ test.describe('cli codegen', () => {
await recorder.page.keyboard.insertText('@'); await recorder.page.keyboard.insertText('@');
await recorder.page.keyboard.type('example.com'); await recorder.page.keyboard.type('example.com');
await recorder.waitForOutput('JavaScript', 'example.com'); await recorder.waitForOutput('JavaScript', 'example.com');
expect(recorder.sources().get('JavaScript').text).not.toContain(`await page.locator('input').press('AltGraph');`); expect(recorder.sources().get('JavaScript').text).not.toContain(`await page.getByRole('textbox').press('AltGraph');`);
expect(recorder.sources().get('JavaScript').text).toContain(`await page.locator('input').fill('playwright@example.com');`); expect(recorder.sources().get('JavaScript').text).toContain(`await page.getByRole('textbox').fill('playwright@example.com');`);
}); });
test('should middle click', async ({ page, openRecorder, server }) => { test('should middle click', async ({ page, openRecorder, server }) => {

View File

@ -121,19 +121,19 @@ test.describe('cli codegen', () => {
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles'); const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[type="file"]').setInputFiles('file-to-upload.txt');`); await page.getByRole('textbox').setInputFiles('file-to-upload.txt');`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[type=\\\"file\\\"]").setInputFiles(Paths.get("file-to-upload.txt"));`); page.getByRole(AriaRole.TEXTBOX).setInputFiles(Paths.get("file-to-upload.txt"));`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[type=\\\"file\\\"]\").set_input_files(\"file-to-upload.txt\")`); page.get_by_role("textbox").set_input_files(\"file-to-upload.txt\")`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[type=\\\"file\\\"]\").set_input_files(\"file-to-upload.txt\")`); await page.get_by_role("textbox").set_input_files(\"file-to-upload.txt\")`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[type=\\\"file\\\"]\").SetInputFilesAsync(new[] { \"file-to-upload.txt\" });`); await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\" });`);
}); });
test('should upload multiple files', async ({ page, openRecorder, browserName, asset }) => { test('should upload multiple files', async ({ page, openRecorder, browserName, asset }) => {
@ -153,19 +153,19 @@ test.describe('cli codegen', () => {
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles'); const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[type=\"file\"]').setInputFiles(['file-to-upload.txt', 'file-to-upload-2.txt']);`); await page.getByRole('textbox').setInputFiles(['file-to-upload.txt', 'file-to-upload-2.txt']);`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[type=\\\"file\\\"]").setInputFiles(new Path[] {Paths.get("file-to-upload.txt"), Paths.get("file-to-upload-2.txt")});`); page.getByRole(AriaRole.TEXTBOX).setInputFiles(new Path[] {Paths.get("file-to-upload.txt"), Paths.get("file-to-upload-2.txt")});`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[type=\\\"file\\\"]\").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`); page.get_by_role("textbox").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[type=\\\"file\\\"]\").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`); await page.get_by_role("textbox").set_input_files([\"file-to-upload.txt\", \"file-to-upload-2.txt\"]`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[type=\\\"file\\\"]\").SetInputFilesAsync(new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`); await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { \"file-to-upload.txt\", \"file-to-upload-2.txt\" });`);
}); });
test('should clear files', async ({ page, openRecorder, browserName, asset }) => { test('should clear files', async ({ page, openRecorder, browserName, asset }) => {
@ -185,20 +185,19 @@ test.describe('cli codegen', () => {
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles'); const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('input[type=\"file\"]').setInputFiles([]);`); await page.getByRole('textbox').setInputFiles([]);`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("input[type=\\\"file\\\"]").setInputFiles(new Path[0]);`); page.getByRole(AriaRole.TEXTBOX).setInputFiles(new Path[0]);`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"input[type=\\\"file\\\"]\").set_input_files([])`); page.get_by_role("textbox").set_input_files([])`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"input[type=\\\"file\\\"]\").set_input_files([])`); await page.get_by_role("textbox").set_input_files([])`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"input[type=\\\"file\\\"]\").SetInputFilesAsync(new[] { });`); await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { });`);
}); });
test('should download files', async ({ page, openRecorder, server }) => { test('should download files', async ({ page, openRecorder, server }) => {
@ -381,20 +380,20 @@ test.describe('cli codegen', () => {
await recorder.waitForOutput('JavaScript', 'TextB'); await recorder.waitForOutput('JavaScript', 'TextB');
const sources = recorder.sources(); const sources = recorder.sources();
expect(sources.get('JavaScript').text).toContain(`await page1.locator('input').fill('TextA');`); expect(sources.get('JavaScript').text).toContain(`await page1.locator('#name').fill('TextA');`);
expect(sources.get('JavaScript').text).toContain(`await page2.locator('input').fill('TextB');`); expect(sources.get('JavaScript').text).toContain(`await page2.locator('#name').fill('TextB');`);
expect(sources.get('Java').text).toContain(`page1.locator("input").fill("TextA");`); expect(sources.get('Java').text).toContain(`page1.locator("#name").fill("TextA");`);
expect(sources.get('Java').text).toContain(`page2.locator("input").fill("TextB");`); expect(sources.get('Java').text).toContain(`page2.locator("#name").fill("TextB");`);
expect(sources.get('Python').text).toContain(`page1.locator(\"input\").fill(\"TextA\")`); expect(sources.get('Python').text).toContain(`page1.locator("#name").fill("TextA")`);
expect(sources.get('Python').text).toContain(`page2.locator(\"input\").fill(\"TextB\")`); expect(sources.get('Python').text).toContain(`page2.locator("#name").fill("TextB")`);
expect(sources.get('Python Async').text).toContain(`await page1.locator(\"input\").fill(\"TextA\")`); expect(sources.get('Python Async').text).toContain(`await page1.locator("#name").fill("TextA")`);
expect(sources.get('Python Async').text).toContain(`await page2.locator(\"input\").fill(\"TextB\")`); expect(sources.get('Python Async').text).toContain(`await page2.locator("#name").fill("TextB")`);
expect(sources.get('C#').text).toContain(`await page1.Locator(\"input\").FillAsync(\"TextA\");`); expect(sources.get('C#').text).toContain(`await page1.Locator("#name").FillAsync("TextA");`);
expect(sources.get('C#').text).toContain(`await page2.Locator(\"input\").FillAsync(\"TextB\");`); expect(sources.get('C#').text).toContain(`await page2.Locator("#name").FillAsync("TextB");`);
}); });
test('click should emit events in order', async ({ page, openRecorder }) => { test('click should emit events in order', async ({ page, openRecorder }) => {
@ -429,7 +428,7 @@ test.describe('cli codegen', () => {
recorder.waitForActionPerformed(), recorder.waitForActionPerformed(),
page.click('input') page.click('input')
]); ]);
expect(models.hovered).toBe('input[name="updated"]'); expect(models.hovered).toBe('#checkbox');
}); });
test('should update active model on action', async ({ page, openRecorder, browserName, headless }) => { test('should update active model on action', async ({ page, openRecorder, browserName, headless }) => {
@ -441,7 +440,7 @@ test.describe('cli codegen', () => {
recorder.waitForActionPerformed(), recorder.waitForActionPerformed(),
page.click('input') page.click('input')
]); ]);
expect(models.active).toBe('input[name="updated"]'); expect(models.active).toBe('#checkbox');
}); });
test('should check input with chaning id', async ({ page, openRecorder }) => { test('should check input with chaning id', async ({ page, openRecorder }) => {
@ -502,7 +501,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`); await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></textarea>`);
const locator = await recorder.focusElement('textarea'); const locator = await recorder.focusElement('textarea');
expect(locator).toBe(`locator('textarea[name="name"]')`); expect(locator).toBe(`locator('#textarea')`);
const [message, sources] = await Promise.all([ const [message, sources] = await Promise.all([
page.waitForEvent('console', msg => msg.type() !== 'error'), page.waitForEvent('console', msg => msg.type() !== 'error'),
@ -511,19 +510,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('textarea[name="name"]').fill('Hello\\'"\`\\nWorld');`); await page.locator('#textarea').fill('Hello\\'"\`\\nWorld');`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("textarea[name=\\\"name\\\"]").fill("Hello'\\"\`\\nWorld");`); page.locator("#textarea").fill("Hello'\\"\`\\nWorld");`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator(\"textarea[name=\\\"name\\\"]\").fill(\"Hello'\\"\`\\nWorld\")`); page.locator("#textarea").fill(\"Hello'\\"\`\\nWorld\")`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator(\"textarea[name=\\\"name\\\"]\").fill(\"Hello'\\"\`\\nWorld\")`); await page.locator("#textarea").fill(\"Hello'\\"\`\\nWorld\")`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator(\"textarea[name=\\\"name\\\"]\").FillAsync(\"Hello'\\"\`\\nWorld\");`); await page.Locator("#textarea").FillAsync(\"Hello'\\"\`\\nWorld\");`);
expect(message.text()).toBe('Hello\'\"\`\nWorld'); expect(message.text()).toBe('Hello\'\"\`\nWorld');
}); });

View File

@ -304,29 +304,6 @@ it.describe(() => {
}); });
it('reverse engineer internal:has-text locators', async ({ page }) => { it('reverse engineer internal:has-text locators', async ({ page }) => {
await page.setContent(`
<div>Hello world</div>
<a>Hello <span>world</span></a>
<a>Goodbye <span>world</span></a>
`);
expect.soft(await generateForNode(page, 'a:has-text("Hello")')).toEqual({
csharp: 'Locator("a").Filter(new() { HasTextString = "Hello world" })',
java: 'locator("a").filter(new Locator.LocatorOptions().setHasText("Hello world"))',
javascript: `locator('a').filter({ hasText: 'Hello world' })`,
python: 'locator("a").filter(has_text="Hello world")',
});
await page.setContent(`
<div>Hello <span>world</span></div>
<b>Hello <span mark=1>world</span></b>
`);
expect.soft(await generateForNode(page, '[mark="1"]')).toEqual({
csharp: 'Locator("b").Filter(new() { HasTextString = "Hello world" }).Locator("span")',
java: 'locator("b").filter(new Locator.LocatorOptions().setHasText("Hello world")).locator("span")',
javascript: `locator('b').filter({ hasText: 'Hello world' }).locator('span')`,
python: 'locator("b").filter(has_text="Hello world").locator("span")',
});
await page.setContent(` await page.setContent(`
<div>Hello <span>world</span></div> <div>Hello <span>world</span></div>
<div>Goodbye <span mark=1>world</span></div> <div>Goodbye <span mark=1>world</span></div>

View File

@ -78,7 +78,7 @@ it.describe('selector generator', () => {
<select><option>foo</option></select> <select><option>foo</option></select>
<select mark=1><option>bar</option></select> <select mark=1><option>bar</option></select>
`); `);
expect(await generate(page, '[mark="1"]')).toBe('select >> nth=1'); expect(await generate(page, '[mark="1"]')).toBe('internal:role=combobox >> nth=1');
}); });
it('should use ordinal for identical nodes', async ({ page }) => { it('should use ordinal for identical nodes', async ({ page }) => {
@ -167,7 +167,7 @@ it.describe('selector generator', () => {
<div>Hello <span>world</span></div> <div>Hello <span>world</span></div>
<b>Hello <span mark=1>world</span></b> <b>Hello <span mark=1>world</span></b>
`); `);
expect(await generate(page, '[mark="1"]')).toBe(`b >> internal:has-text="Hello world"i >> span`); expect(await generate(page, '[mark="1"]')).toBe(`b >> internal:text="world"i`);
}); });
it('should use parent text', async ({ page }) => { it('should use parent text', async ({ page }) => {
@ -246,21 +246,25 @@ it.describe('selector generator', () => {
<input value="two" mark="1"> <input value="two" mark="1">
<input value="three"> <input value="three">
`); `);
expect(await generate(page, 'input[mark="1"]')).toBe('input >> nth=1'); expect(await generate(page, 'input[mark="1"]')).toBe('internal:role=textbox >> nth=1');
}); });
it.describe('should prioritise input element attributes correctly', () => { it.describe('should prioritise attributes correctly', () => {
it('name', async ({ page }) => { it('role', async ({ page }) => {
await page.setContent(`<input name="foobar" type="text"/>`); await page.setContent(`<input name="foobar" type="text"/>`);
expect(await generate(page, 'input')).toBe('input[name="foobar"]'); expect(await generate(page, 'input')).toBe('internal:role=textbox');
}); });
it('placeholder', async ({ page }) => { it('placeholder', async ({ page }) => {
await page.setContent(`<input placeholder="foobar" type="text"/>`); await page.setContent(`<input placeholder="foobar" type="text"/>`);
expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"i]'); expect(await generate(page, 'input')).toBe('internal:attr=[placeholder=\"foobar\"i]');
}); });
it('name', async ({ page }) => {
await page.setContent(`<input role="presentation" aria-hidden="false" name="foobar" type="date"/>`);
expect(await generate(page, 'input')).toBe('input[name="foobar"]');
});
it('type', async ({ page }) => { it('type', async ({ page }) => {
await page.setContent(`<input type="text"/>`); await page.setContent(`<input role="presentation" aria-hidden="false" type="checkbox"/>`);
expect(await generate(page, 'input')).toBe('input[type="text"]'); expect(await generate(page, 'input')).toBe('input[type="checkbox"]');
}); });
}); });
@ -282,7 +286,7 @@ it.describe('selector generator', () => {
const input = document.createElement('input'); const input = document.createElement('input');
shadowRoot.appendChild(input); shadowRoot.appendChild(input);
}); });
expect(await generate(page, 'input')).toBe('input'); expect(await generate(page, 'input')).toBe('internal:role=textbox');
}); });
it('should match in deep shadow dom', async ({ page }) => { it('should match in deep shadow dom', async ({ page }) => {
@ -300,7 +304,7 @@ it.describe('selector generator', () => {
input2.setAttribute('value', 'foo'); input2.setAttribute('value', 'foo');
shadowRoot2.appendChild(input2); shadowRoot2.appendChild(input2);
}); });
expect(await generate(page, 'input[value=foo]')).toBe('input >> nth=2'); expect(await generate(page, 'input[value=foo]')).toBe('internal:role=textbox >> nth=2');
}); });
it('should work in dynamic iframes without navigation', async ({ page }) => { it('should work in dynamic iframes without navigation', async ({ page }) => {
@ -352,7 +356,7 @@ it.describe('selector generator', () => {
}); });
it('should work without CSS.escape', async ({ page }) => { it('should work without CSS.escape', async ({ page }) => {
await page.setContent(`<button></button>`); await page.setContent(`<button role="presentation" aria-hidden="false"></button>`);
await page.$eval('button', button => { await page.$eval('button', button => {
delete window.CSS.escape; delete window.CSS.escape;
button.setAttribute('name', '-tricky\u0001name'); button.setAttribute('name', '-tricky\u0001name');
@ -397,4 +401,9 @@ it.describe('selector generator', () => {
await page.setContent(`<label for=target>Coun"try</label><input id=target>`); await page.setContent(`<label for=target>Coun"try</label><input id=target>`);
expect(await generate(page, 'input')).toBe('internal:label="Coun\\\"try"i'); expect(await generate(page, 'input')).toBe('internal:label="Coun\\\"try"i');
}); });
it('should prefer role other input[type]', async ({ page }) => {
await page.setContent(`<input type=checkbox><div data-testid=wrapper><input type=checkbox></div>`);
expect(await generate(page, '[data-testid=wrapper] > input')).toBe('internal:testid=[data-testid="wrapper"s] >> internal:role=checkbox');
});
}); });

View File

@ -34,8 +34,8 @@ it('should fail page.fill in strict mode', async ({ page }) => {
await page.setContent(`<input></input><div><input></input></div>`); await page.setContent(`<input></input><div><input></input></div>`);
const error = await page.fill('input', 'text', { strict: true }).catch(e => e); const error = await page.fill('input', 'text', { strict: true }).catch(e => e);
expect(error.message).toContain('strict mode violation'); expect(error.message).toContain('strict mode violation');
expect(error.message).toContain(`1) <input/> aka locator('input').first()`); expect(error.message).toContain(`1) <input/> aka getByRole('textbox').first()`);
expect(error.message).toContain(`2) <input/> aka locator('div input')`); expect(error.message).toContain(`2) <input/> aka locator('div').getByRole('textbox')`);
}); });
it('should fail page.$ in strict mode', async ({ page }) => { it('should fail page.$ in strict mode', async ({ page }) => {