chore(codegen): prioritize role selectors (#17750)

This commit is contained in:
Pavel Feldman 2022-10-03 07:44:24 -08:00 committed by GitHub
parent ff6d240e83
commit 42a4d8a829
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 103 additions and 88 deletions

View file

@ -15,6 +15,7 @@
*/ */
import { type InjectedScript } from './injectedScript'; import { type InjectedScript } from './injectedScript';
import { getAriaRole, getElementAccessibleName } from './roleUtils';
import { elementText } from './selectorUtils'; import { elementText } from './selectorUtils';
type SelectorToken = { type SelectorToken = {
@ -45,7 +46,7 @@ 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): { selector: string, elements: Element[] } {
injectedScript._evaluator.begin(); injectedScript._evaluator.begin();
try { try {
targetElement = targetElement.closest('button,select,input,[role=button],[role=checkbox],[role=radio]') || 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);
const bestTokens = targetTokens || cssFallback(injectedScript, targetElement, strict); const bestTokens = targetTokens || cssFallback(injectedScript, targetElement, strict);
const selector = joinTokens(bestTokens); const selector = joinTokens(bestTokens);
@ -70,6 +71,7 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
if (targetElement.ownerDocument.documentElement === targetElement) if (targetElement.ownerDocument.documentElement === targetElement)
return [{ engine: 'css', selector: 'html', score: 1 }]; return [{ engine: 'css', selector: 'html', score: 1 }];
const accessibleNameCache = new Map();
const calculate = (element: Element, allowText: boolean): SelectorToken[] | null => { const calculate = (element: Element, allowText: boolean): SelectorToken[] | null => {
const allowNthMatch = element === targetElement; const allowNthMatch = element === targetElement;
@ -78,7 +80,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).map(token => [token]); const noTextCandidates = buildCandidates(element, 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);
@ -141,32 +143,43 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
return calculateCached(targetElement, true); return calculateCached(targetElement, true);
} }
function buildCandidates(injectedScript: InjectedScript, element: Element): SelectorToken[] { function buildCandidates(element: Element, accessibleNameCache: Map<Element, boolean>): SelectorToken[] {
const candidates: SelectorToken[] = []; const candidates: SelectorToken[] = [];
for (const attribute of ['data-testid', 'data-test-id', 'data-test']) {
if (element.getAttribute(attribute)) if (element.getAttribute('data-testid'))
candidates.push({ engine: 'css', selector: `[${attribute}=${quoteAttributeValue(element.getAttribute(attribute)!)}]`, score: 1 }); candidates.push({ engine: 'attr', selector: `[data-testid=${quoteAttributeValue(element.getAttribute('data-testid')!)}]`, score: 1 });
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 });
} }
if (element.nodeName === 'INPUT') { if (element.nodeName === 'INPUT') {
const input = element as HTMLInputElement; const input = element as HTMLInputElement;
if (input.placeholder) if (input.placeholder)
candidates.push({ engine: 'css', selector: `[placeholder=${quoteAttributeValue(input.placeholder)}]`, score: 10 }); candidates.push({ engine: 'attr', selector: `[placeholder=${quoteAttributeValue(input.placeholder)}]`, score: 3 });
} }
if (element.getAttribute('aria-label'))
candidates.push({ engine: 'css', selector: `[aria-label=${quoteAttributeValue(element.getAttribute('aria-label')!)}]`, score: 10 });
if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName))
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[alt=${quoteAttributeValue(element.getAttribute('alt')!)}]`, score: 10 });
if (element.getAttribute('role')) const ariaRole = getAriaRole(element);
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[role=${quoteAttributeValue(element.getAttribute('role')!)}]`, score: 50 }); if (ariaRole) {
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
if (ariaName)
candidates.push({ engine: 'role', selector: `${ariaRole}[name=${quoteAttributeValue(ariaName)}]`, score: 3 });
else
candidates.push({ engine: 'role', selector: ariaRole, score: 150 });
}
if (element.getAttribute('alt') && ['APPLET', 'AREA', 'IMG', 'INPUT'].includes(element.nodeName))
candidates.push({ engine: 'attr', selector: `[alt=${quoteAttributeValue(element.getAttribute('alt')!)}]`, score: 10 });
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: 50 });
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: 50 });
} }
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName)) if (['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName))
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: 50 }); candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: 50 });
@ -174,12 +187,11 @@ function buildCandidates(injectedScript: InjectedScript, element: Element): Sele
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: 100 });
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: 200 }); candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: 200 });
return candidates; return candidates;
} }
function buildTextCandidates(injectedScript: InjectedScript, element: Element, allowHasText: boolean): SelectorToken[] { function buildTextCandidates(injectedScript: InjectedScript, element: Element, isTargetNode: boolean): SelectorToken[] {
if (element.nodeName === 'SELECT') if (element.nodeName === 'SELECT')
return []; return [];
const text = elementText(injectedScript._evaluator._cacheText, element).full.trim().replace(/\s+/g, ' ').substring(0, 80); const text = elementText(injectedScript._evaluator._cacheText, element).full.trim().replace(/\s+/g, ' ').substring(0, 80);
@ -191,12 +203,14 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, a
if (text.includes('"') || text.includes('>>') || text[0] === '/') if (text.includes('"') || text.includes('>>') || text[0] === '/')
escaped = `/.*${escapeForRegex(text)}.*/`; escaped = `/.*${escapeForRegex(text)}.*/`;
if (isTargetNode)
candidates.push({ engine: 'text', selector: escaped, score: 10 }); candidates.push({ engine: 'text', selector: escaped, score: 10 });
if (allowHasText && escaped === text) {
if (escaped === text) {
let prefix = element.nodeName.toLowerCase(); let prefix = element.nodeName.toLowerCase();
if (element.hasAttribute('role')) if (element.hasAttribute('role'))
prefix += `[role=${quoteAttributeValue(element.getAttribute('role')!)}]`; prefix += `[role=${quoteAttributeValue(element.getAttribute('role')!)}]`;
candidates.push({ engine: 'css', selector: `${prefix}:has-text("${text}")`, score: 30 }); candidates.push({ engine: 'css', selector: `${prefix}:has-text("${text}")`, score: 10 });
} }
return candidates; return candidates;
} }

View file

@ -26,7 +26,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`); await recorder.setContentAndWait(`<button onclick="console.log('click')">Submit</button>`);
const selector = await recorder.hoverOverElement('button'); const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('text=Submit'); expect(selector).toBe('role=button[name=\"Submit\"]');
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'),
@ -35,19 +35,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('text=Submit').click();`); await page.locator('role=button[name=\"Submit\"]').click();`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator("text=Submit").click()`); page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator("text=Submit").click()`); await page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("text=Submit").click();`); page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator("text=Submit").ClickAsync();`); await page.Locator(\"role=button[name=\\\"Submit\\\"]\").ClickAsync();`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
@ -69,7 +69,7 @@ test.describe('cli codegen', () => {
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
const selector = await recorder.hoverOverElement('button'); const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('text=Submit'); expect(selector).toBe('role=button[name=\"Submit\"]');
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'),
@ -78,7 +78,7 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('text=Submit').click();`); await page.locator('role=button[name=\"Submit\"]').click();`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
@ -149,7 +149,7 @@ test.describe('cli codegen', () => {
</body>`); </body>`);
const selector = await recorder.hoverOverElement('button'); const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('text=Submit'); expect(selector).toBe('role=button[name=\"Submit\"]');
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'),
@ -158,19 +158,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('text=Submit').click();`); await page.locator('role=button[name=\"Submit\"]').click();`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator("text=Submit").click()`); page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator("text=Submit").click()`); await page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("text=Submit").click();`); page.locator(\"role=button[name=\\\"Submit\\\"]\").click()`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator("text=Submit").ClickAsync();`); await page.Locator(\"role=button[name=\\\"Submit\\\"]\").ClickAsync();`);
expect(message.text()).toBe('click'); expect(message.text()).toBe('click');
}); });
@ -540,7 +540,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait('<a target=_blank rel=noopener href="about:blank">link</a>'); await recorder.setContentAndWait('<a target=_blank rel=noopener href="about:blank">link</a>');
const selector = await recorder.hoverOverElement('a'); const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('text=link'); expect(selector).toBe('role=link[name=\"link\"]');
const [popup, sources] = await Promise.all([ const [popup, sources] = await Promise.all([
page.context().waitForEvent('page'), page.context().waitForEvent('page'),
@ -551,28 +551,28 @@ test.describe('cli codegen', () => {
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
const [page1] = await Promise.all([ const [page1] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.locator('text=link').click() page.locator('role=link[name=\"link\"]').click()
]);`); ]);`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
Page page1 = page.waitForPopup(() -> { Page page1 = page.waitForPopup(() -> {
page.locator("text=link").click(); page.locator("role=link[name=\\\"link\\\"]").click();
});`); });`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
with page.expect_popup() as popup_info: with page.expect_popup() as popup_info:
page.locator(\"text=link\").click() page.locator(\"role=link[name=\\\"link\\\"]\").click()
page1 = popup_info.value`); page1 = popup_info.value`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
async with page.expect_popup() as popup_info: async with page.expect_popup() as popup_info:
await page.locator(\"text=link\").click() await page.locator(\"role=link[name=\\\"link\\\"]\").click()
page1 = await popup_info.value`); page1 = await popup_info.value`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
var page1 = await page.RunAndWaitForPopupAsync(async () => var page1 = await page.RunAndWaitForPopupAsync(async () =>
{ {
await page.Locator(\"text=link\").ClickAsync(); await page.Locator(\"role=link[name=\\\"link\\\"]\").ClickAsync();
});`); });`);
expect(popup.url()).toBe('about:blank'); expect(popup.url()).toBe('about:blank');

View file

@ -231,28 +231,28 @@ test.describe('cli codegen', () => {
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
const [download] = await Promise.all([ const [download] = await Promise.all([
page.waitForEvent('download'), page.waitForEvent('download'),
page.locator('text=Download').click() page.locator('role=link[name=\"Download\"]').click()
]);`); ]);`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
BrowserContext context = browser.newContext();`); BrowserContext context = browser.newContext();`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
Download download = page.waitForDownload(() -> { Download download = page.waitForDownload(() -> {
page.locator("text=Download").click(); page.locator("role=link[name=\\\"Download\\\"]").click();
});`); });`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
context = browser.new_context()`); context = browser.new_context()`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
with page.expect_download() as download_info: with page.expect_download() as download_info:
page.locator(\"text=Download\").click() page.locator(\"role=link[name=\\\"Download\\\"]\").click()
download = download_info.value`); download = download_info.value`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
context = await browser.new_context()`); context = await browser.new_context()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
async with page.expect_download() as download_info: async with page.expect_download() as download_info:
await page.locator(\"text=Download\").click() await page.locator(\"role=link[name=\\\"Download\\\"]\").click()
download = await download_info.value`); download = await download_info.value`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
@ -260,7 +260,7 @@ test.describe('cli codegen', () => {
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
var download1 = await page.RunAndWaitForDownloadAsync(async () => var download1 = await page.RunAndWaitForDownloadAsync(async () =>
{ {
await page.Locator(\"text=Download\").ClickAsync(); await page.Locator(\"role=link[name=\\\"Download\\\"]\").ClickAsync();
});`); });`);
}); });
@ -283,22 +283,22 @@ test.describe('cli codegen', () => {
console.log(\`Dialog message: \${dialog.message()}\`); console.log(\`Dialog message: \${dialog.message()}\`);
dialog.dismiss().catch(() => {}); dialog.dismiss().catch(() => {});
}); });
await page.locator('text=click me').click();`); await page.locator('role=button[name=\"click me\"]').click();`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.onceDialog(dialog -> { page.onceDialog(dialog -> {
System.out.println(String.format("Dialog message: %s", dialog.message())); System.out.println(String.format("Dialog message: %s", dialog.message()));
dialog.dismiss(); dialog.dismiss();
}); });
page.locator("text=click me").click();`); page.locator("role=button[name=\\\"click me\\\"]").click();`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.once(\"dialog\", lambda dialog: dialog.dismiss()) page.once(\"dialog\", lambda dialog: dialog.dismiss())
page.locator(\"text=click me\").click()`); page.locator(\"role=button[name=\\\"click me\\\"]\").click()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
page.once(\"dialog\", lambda dialog: dialog.dismiss()) page.once(\"dialog\", lambda dialog: dialog.dismiss())
await page.locator(\"text=click me\").click()`); await page.locator(\"role=button[name=\\\"click me\\\"]\").click()`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
void page_Dialog1_EventHandler(object sender, IDialog dialog) void page_Dialog1_EventHandler(object sender, IDialog dialog)
@ -308,7 +308,7 @@ test.describe('cli codegen', () => {
page.Dialog -= page_Dialog1_EventHandler; page.Dialog -= page_Dialog1_EventHandler;
} }
page.Dialog += page_Dialog1_EventHandler; page.Dialog += page_Dialog1_EventHandler;
await page.Locator(\"text=click me\").ClickAsync();`); await page.Locator(\"role=button[name=\\\"click me\\\"]\").ClickAsync();`);
}); });
@ -333,7 +333,7 @@ test.describe('cli codegen', () => {
await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`); await recorder.setContentAndWait(`<a href="about:blank?foo">link</a>`);
const selector = await recorder.hoverOverElement('a'); const selector = await recorder.hoverOverElement('a');
expect(selector).toBe('text=link'); expect(selector).toBe('role=link[name=\"link\"]');
await page.click('a', { modifiers: [platform === 'darwin' ? 'Meta' : 'Control'] }); await page.click('a', { modifiers: [platform === 'darwin' ? 'Meta' : 'Control'] });
const sources = await recorder.waitForOutput('JavaScript', 'page1'); const sources = await recorder.waitForOutput('JavaScript', 'page1');
@ -352,7 +352,7 @@ test.describe('cli codegen', () => {
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
const [page1] = await Promise.all([ const [page1] = await Promise.all([
page.waitForEvent('popup'), page.waitForEvent('popup'),
page.locator('text=link').click({ page.locator('role=link[name=\"link\"]').click({
modifiers: ['${platform === 'darwin' ? 'Meta' : 'Control'}'] modifiers: ['${platform === 'darwin' ? 'Meta' : 'Control'}']
}) })
]);`); ]);`);

View file

@ -28,7 +28,7 @@ test.describe('cli codegen', () => {
`); `);
const selector = await recorder.hoverOverElement('button'); const selector = await recorder.hoverOverElement('button');
expect(selector).toBe('text=Submit >> nth=0'); expect(selector).toBe('role=button[name=\"Submit\"] >> nth=0');
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'),
@ -37,19 +37,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('text=Submit').first().click();`); await page.locator('role=button[name=\"Submit\"]').first().click();`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator("text=Submit").first.click()`); page.locator("role=button[name=\\\"Submit\\\"]").first.click()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator("text=Submit").first.click()`); await page.locator("role=button[name=\\\"Submit\\\"]").first.click()`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("text=Submit").first().click();`); page.locator("role=button[name=\\\"Submit\\\"]").first().click();`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator("text=Submit").First.ClickAsync();`); await page.Locator("role=button[name=\\\"Submit\\\"]").First.ClickAsync();`);
expect(message.text()).toBe('click1'); expect(message.text()).toBe('click1');
}); });
@ -63,7 +63,7 @@ test.describe('cli codegen', () => {
`); `);
const selector = await recorder.hoverOverElement('button >> nth=1'); const selector = await recorder.hoverOverElement('button >> nth=1');
expect(selector).toBe('text=Submit >> nth=1'); expect(selector).toBe('role=button[name=\"Submit\"] >> nth=1');
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'),
@ -72,19 +72,19 @@ test.describe('cli codegen', () => {
]); ]);
expect(sources.get('JavaScript').text).toContain(` expect(sources.get('JavaScript').text).toContain(`
await page.locator('text=Submit').nth(1).click();`); await page.locator('role=button[name=\"Submit\"]').nth(1).click();`);
expect(sources.get('Python').text).toContain(` expect(sources.get('Python').text).toContain(`
page.locator("text=Submit").nth(1).click()`); page.locator("role=button[name=\\\"Submit\\\"]").nth(1).click()`);
expect(sources.get('Python Async').text).toContain(` expect(sources.get('Python Async').text).toContain(`
await page.locator("text=Submit").nth(1).click()`); await page.locator("role=button[name=\\\"Submit\\\"]").nth(1).click()`);
expect(sources.get('Java').text).toContain(` expect(sources.get('Java').text).toContain(`
page.locator("text=Submit").nth(1).click();`); page.locator("role=button[name=\\\"Submit\\\"]").nth(1).click();`);
expect(sources.get('C#').text).toContain(` expect(sources.get('C#').text).toContain(`
await page.Locator("text=Submit").Nth(1).ClickAsync();`); await page.Locator("role=button[name=\\\"Submit\\\"]").Nth(1).ClickAsync();`);
expect(message.text()).toBe('click2'); expect(message.text()).toBe('click2');
}); });

View file

@ -35,7 +35,7 @@ it.describe('selector generator', () => {
it('should prefer role=button over inner span', async ({ page }) => { it('should prefer role=button over inner span', async ({ page }) => {
await page.setContent(`<div role=button><span></span></div>`); await page.setContent(`<div role=button><span></span></div>`);
expect(await generate(page, 'div')).toBe('div[role="button"]'); expect(await generate(page, 'div')).toBe('role=button');
}); });
it('should generate text and normalize whitespace', async ({ page }) => { it('should generate text and normalize whitespace', async ({ page }) => {
@ -43,14 +43,14 @@ it.describe('selector generator', () => {
expect(await generate(page, 'div')).toBe('text=Text some more text'); expect(await generate(page, 'div')).toBe('text=Text some more text');
}); });
it('should not escape spaces inside attribute selectors', async ({ page }) => { it('should not escape spaces inside named attr selectors', async ({ page }) => {
await page.setContent(`<input placeholder="Foo b ar"/>`); await page.setContent(`<input placeholder="Foo b ar"/>`);
expect(await generate(page, 'input')).toBe('[placeholder="Foo b ar"]'); expect(await generate(page, 'input')).toBe('attr=[placeholder=\"Foo b ar\"]');
}); });
it('should generate text for <input type=button>', async ({ page }) => { it('should generate text for <input type=button>', async ({ page }) => {
await page.setContent(`<input type=button value="Click me">`); await page.setContent(`<input type=button value="Click me">`);
expect(await generate(page, 'input')).toBe('text=Click me'); expect(await generate(page, 'input')).toBe('role=button[name=\"Click me\"]');
}); });
it('should trim text', async ({ page }) => { it('should trim text', async ({ page }) => {
@ -88,7 +88,7 @@ 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('[data-testid="a"]'); expect(await generate(page, '[data-testid="a"]')).toBe('attr=[data-testid=\"a\"]');
}); });
it('should handle first non-unique data-testid', async ({ page }) => { it('should handle first non-unique data-testid', async ({ page }) => {
@ -99,7 +99,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('[data-testid="a"] >> nth=0'); expect(await generate(page, 'div[mark="1"]')).toBe('attr=[data-testid=\"a\"] >> nth=0');
}); });
it('should handle second non-unique data-testid', async ({ page }) => { it('should handle second non-unique data-testid', async ({ page }) => {
@ -110,7 +110,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(`[data-testid="a"] >> nth=1`); expect(await generate(page, 'div[mark="1"]')).toBe(`attr=[data-testid=\"a\"] >> nth=1`);
}); });
it('should use readable id', async ({ page }) => { it('should use readable id', async ({ page }) => {
@ -133,16 +133,17 @@ it.describe('selector generator', () => {
await page.setContent(` await page.setContent(`
<div>Hello world</div> <div>Hello world</div>
<a>Hello <span>world</span></a> <a>Hello <span>world</span></a>
<a>Goodbye <span>world</span></a>
`); `);
expect(await generate(page, 'a')).toBe(`a:has-text("Hello world")`); expect(await generate(page, 'a:has-text("Hello")')).toBe(`a:has-text("Hello world")`);
}); });
it('should chain text after parent', async ({ page }) => { it('should chain text after parent', async ({ page }) => {
await page.setContent(` await page.setContent(`
<div>Hello <span>world</span></div> <div>Hello <span>world</span></div>
<a>Hello <span mark=1>world</span></a> <b>Hello <span mark=1>world</span></b>
`); `);
expect(await generate(page, '[mark="1"]')).toBe(`a >> text=world`); expect(await generate(page, '[mark="1"]')).toBe(`b:has-text(\"Hello world\") span`);
}); });
it('should use parent text', async ({ page }) => { it('should use parent text', async ({ page }) => {
@ -150,7 +151,7 @@ it.describe('selector generator', () => {
<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>
`); `);
expect(await generate(page, '[mark="1"]')).toBe(`text=Goodbye world >> span`); expect(await generate(page, '[mark="1"]')).toBe(`div:has-text(\"Goodbye world\") span`);
}); });
it('should separate selectors by >>', async ({ page }) => { it('should separate selectors by >>', async ({ page }) => {
@ -179,8 +180,8 @@ it.describe('selector generator', () => {
it('should use nested ordinals', async ({ page }) => { it('should use nested ordinals', async ({ page }) => {
await page.setContent(` await page.setContent(`
<a><c></c><c></c><c></c><c></c><c></c><b></b></a> <div><c></c><c></c><c></c><c></c><c></c><b></b></div>
<a> <div>
<b> <b>
<c> <c>
</c> </c>
@ -188,16 +189,16 @@ it.describe('selector generator', () => {
<b> <b>
<c mark=1></c> <c mark=1></c>
</b> </b>
</a> </div>
<a><b></b></a> <div><b></b></div>
`); `);
expect(await generate(page, 'c[mark="1"]')).toBe('b:nth-child(2) > c'); expect(await generate(page, 'c[mark="1"]')).toBe('b:nth-child(2) > c');
}); });
it('should properly join child selectors under nested ordinals', async ({ page }) => { it('should properly join child selectors under nested ordinals', async ({ page }) => {
await page.setContent(` await page.setContent(`
<a><c></c><c></c><c></c><c></c><c></c><b></b></a> <div><c></c><c></c><c></c><c></c><c></c><b></b></div>
<a> <div>
<b> <b>
<div> <div>
<c> <c>
@ -209,8 +210,8 @@ it.describe('selector generator', () => {
<c mark=1></c> <c mark=1></c>
</div> </div>
</b> </b>
</a> </div>
<a><b></b></a> <div><b></b></div>
`); `);
expect(await generate(page, 'c[mark="1"]')).toBe('b:nth-child(2) > div > c'); expect(await generate(page, 'c[mark="1"]')).toBe('b:nth-child(2) > div > c');
}); });
@ -231,7 +232,7 @@ it.describe('selector generator', () => {
}); });
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('[placeholder="foobar"]'); expect(await generate(page, 'input')).toBe('attr=[placeholder=\"foobar\"]');
}); });
it('type', async ({ page }) => { it('type', async ({ page }) => {
await page.setContent(`<input type="text"/>`); await page.setContent(`<input type="text"/>`);
@ -316,9 +317,9 @@ it.describe('selector generator', () => {
await page.setContent(`<ng:switch><span></span></ng:switch>`); await page.setContent(`<ng:switch><span></span></ng:switch>`);
expect(await generate(page, 'ng\\:switch')).toBe('ng\\:switch'); expect(await generate(page, 'ng\\:switch')).toBe('ng\\:switch');
await page.setContent(`<div><span></span></div>`); await page.setContent(`<button><span></span></button><button></button>`);
await page.$eval('div', div => div.setAttribute('aria-label', `!#'!?:`)); await page.$eval('button', button => button.setAttribute('aria-label', `!#'!?:`));
expect(await generate(page, 'div')).toBe("[aria-label=\"\\!\\#\\'\\!\\?\\:\"]"); expect(await generate(page, 'button')).toBe("role=button[name=\"\\!\\#\\'\\!\\?\\:\"]");
await page.setContent(`<div><span></span></div>`); await page.setContent(`<div><span></span></div>`);
await page.$eval('div', div => div.id = `!#'!?:`); await page.$eval('div', div => div.id = `!#'!?:`);
@ -341,7 +342,7 @@ it.describe('selector generator', () => {
it('should accept valid aria-label for candidate consideration', async ({ page }) => { it('should accept valid aria-label for candidate consideration', async ({ page }) => {
await page.setContent(`<button aria-label="ariaLabel" id="buttonId"></button>`); await page.setContent(`<button aria-label="ariaLabel" id="buttonId"></button>`);
expect(await generate(page, 'button')).toBe('[aria-label="ariaLabel"]'); expect(await generate(page, 'button')).toBe('role=button[name=\"ariaLabel\"]');
}); });
it('should ignore empty role for candidate consideration', async ({ page }) => { it('should ignore empty role for candidate consideration', async ({ page }) => {
@ -349,9 +350,9 @@ it.describe('selector generator', () => {
expect(await generate(page, 'button')).toBe('#buttonId'); expect(await generate(page, 'button')).toBe('#buttonId');
}); });
it('should accept valid role for candidate consideration', async ({ page }) => { it('should not accept invalid role for candidate consideration', async ({ page }) => {
await page.setContent(`<button role="roleDescription" id="buttonId"></button>`); await page.setContent(`<button role="roleDescription" id="buttonId"></button>`);
expect(await generate(page, 'button')).toBe('button[role="roleDescription"]'); expect(await generate(page, 'button')).toBe('#buttonId');
}); });
it('should ignore empty data-test-id for candidate consideration', async ({ page }) => { it('should ignore empty data-test-id for candidate consideration', async ({ page }) => {