mirror of
https://github.com/microsoft/playwright.git
synced 2024-12-03 07:51:12 +03:00
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:
parent
6d491f928d
commit
cafa558845
@ -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, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector;
|
||||
return generateSelector(this._injectedScript, element, 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, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector;
|
||||
const selector = generateSelector(this._injectedScript, element, this._injectedScript.testIdAttributeNameForStrictErrorAndConsoleCodegen()).selector;
|
||||
return asLocator(language || 'javascript', selector);
|
||||
}
|
||||
|
||||
|
@ -149,7 +149,7 @@ export class InjectedScript {
|
||||
}
|
||||
|
||||
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 {
|
||||
|
@ -240,7 +240,7 @@ class Recorder {
|
||||
if (this._mode === 'none')
|
||||
return;
|
||||
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;
|
||||
if (userGesture)
|
||||
this._hoveredElement = activeElement as HTMLElement | null;
|
||||
@ -254,7 +254,7 @@ class Recorder {
|
||||
return;
|
||||
}
|
||||
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))
|
||||
return;
|
||||
this._hoveredModel = selector ? { selector, elements } : null;
|
||||
|
@ -27,7 +27,20 @@ type SelectorToken = {
|
||||
|
||||
const cacheAllowText = 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 kCSSFallbackScore = 10000000;
|
||||
|
||||
export function querySelector(injectedScript: InjectedScript, selector: string, ownerDocument: Document): { selector: string, elements: Element[] } {
|
||||
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();
|
||||
try {
|
||||
targetElement = targetElement.closest('button,select,input,[role=button],[role=checkbox],[role=radio],a,[role=link]') || targetElement;
|
||||
const targetTokens = generateSelectorFor(injectedScript, targetElement, strict, testIdAttributeName);
|
||||
const bestTokens = targetTokens || cssFallback(injectedScript, targetElement, strict);
|
||||
const targetTokens = generateSelectorFor(injectedScript, targetElement, testIdAttributeName);
|
||||
const bestTokens = targetTokens || cssFallback(injectedScript, targetElement);
|
||||
const selector = joinTokens(bestTokens);
|
||||
const parsedSelector = injectedScript.parseSelector(selector);
|
||||
return {
|
||||
@ -68,7 +81,7 @@ function filterRegexTokens(textCandidates: SelectorToken[][]): SelectorToken[][]
|
||||
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)
|
||||
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]);
|
||||
|
||||
// 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).
|
||||
textCandidates = filterRegexTokens(textCandidates);
|
||||
@ -114,7 +127,7 @@ function generateSelectorFor(injectedScript: InjectedScript, targetElement: Elem
|
||||
if (result && combineScores([...parentTokens, ...bestPossibleInParent]) >= combineScores(result))
|
||||
continue;
|
||||
// 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)
|
||||
return;
|
||||
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[] {
|
||||
const candidates: SelectorToken[] = [];
|
||||
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']) {
|
||||
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') {
|
||||
const input = element as HTMLInputElement | HTMLTextAreaElement;
|
||||
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];
|
||||
if (label) {
|
||||
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);
|
||||
if (ariaRole) {
|
||||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
|
||||
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
|
||||
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))
|
||||
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))
|
||||
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 (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))
|
||||
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: 50 });
|
||||
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(element.nodeName) && element.getAttribute('type') !== 'hidden')
|
||||
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSInputTypeNameScore + 1 });
|
||||
|
||||
const idAttr = element.getAttribute('id');
|
||||
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;
|
||||
}
|
||||
|
||||
@ -207,20 +220,20 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
||||
const escaped = escapeForTextSelector(text, false);
|
||||
|
||||
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 candidate: SelectorToken[] = [];
|
||||
if (ariaRole) {
|
||||
if (ariaRole && !['none', 'presentation'].includes(ariaRole)) {
|
||||
const ariaName = getElementAccessibleName(element, false, accessibleNameCache);
|
||||
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
|
||||
candidate.push({ engine: 'internal:role', selector: ariaRole, score: 10 });
|
||||
candidate.push({ engine: 'internal:role', selector: ariaRole, score: kRoleWithoutNameScore });
|
||||
} 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);
|
||||
return candidates;
|
||||
}
|
||||
@ -239,8 +252,7 @@ function makeSelectorForId(id: string) {
|
||||
return /^[a-zA-Z][a-zA-Z0-9\-\_]+$/.test(id) ? '#' + id : `[id="${cssEscape(id)}"]`;
|
||||
}
|
||||
|
||||
function cssFallback(injectedScript: InjectedScript, targetElement: Element, strict: boolean): SelectorToken[] {
|
||||
const kFallbackScore = 10000000;
|
||||
function cssFallback(injectedScript: InjectedScript, targetElement: Element): SelectorToken[] {
|
||||
const root: Node = targetElement.ownerDocument;
|
||||
const tokens: string[] = [];
|
||||
|
||||
@ -255,9 +267,7 @@ function cssFallback(injectedScript: InjectedScript, targetElement: Element, str
|
||||
}
|
||||
|
||||
function makeStrict(selector: string): SelectorToken[] {
|
||||
const token = { engine: 'css', selector, score: kFallbackScore };
|
||||
if (!strict)
|
||||
return [token];
|
||||
const token = { engine: 'css', selector, score: kCSSFallbackScore };
|
||||
const parsedSelector = injectedScript.parseSelector(selector);
|
||||
const elements = injectedScript.querySelectorAll(parsedSelector, targetElement.ownerDocument);
|
||||
if (elements.length === 1)
|
||||
@ -340,7 +350,7 @@ function combineScores(tokens: SelectorToken[]): number {
|
||||
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) }));
|
||||
joined.sort((a, b) => a.score - b.score);
|
||||
|
||||
@ -348,14 +358,13 @@ function chooseFirstSelector(injectedScript: InjectedScript, scope: Element | Do
|
||||
for (const { tokens } of joined) {
|
||||
const parsedSelector = injectedScript.parseSelector(joinTokens(tokens));
|
||||
const result = injectedScript.querySelectorAll(parsedSelector, scope);
|
||||
const isStrictEnough = !strict || result.length === 1;
|
||||
const index = result.indexOf(targetElement);
|
||||
if (index === 0 && isStrictEnough) {
|
||||
// We are the first match - found the best selector.
|
||||
if (result[0] === targetElement && result.length === 1) {
|
||||
// We are the only match - found the best selector.
|
||||
return tokens;
|
||||
}
|
||||
|
||||
// Otherwise, perhaps we can use nth=?
|
||||
const index = result.indexOf(targetElement);
|
||||
if (!allowNthMatch || bestWithIndex || index === -1 || result.length > 5)
|
||||
continue;
|
||||
|
||||
|
@ -212,7 +212,7 @@ test.describe('cli codegen', () => {
|
||||
|
||||
await recorder.setContentAndWait(`<input id="input" name="name" oninput="console.log(input.value)"></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([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -221,18 +221,18 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
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(`
|
||||
page.locator("input[name=\\\"name\\\"]").fill("John");`);
|
||||
page.locator("#input").fill("John");`);
|
||||
|
||||
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(`
|
||||
await page.locator(\"input[name=\\\"name\\\"]\").fill(\"John\")`);
|
||||
await page.locator("#input").fill(\"John\")`);
|
||||
|
||||
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');
|
||||
});
|
||||
@ -243,7 +243,7 @@ test.describe('cli codegen', () => {
|
||||
// In Japanese, "てすと" or "テスト" means "test".
|
||||
await recorder.setContentAndWait(`<input id="input" name="name" oninput="input.value === 'てすと' && console.log(input.value)"></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([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -255,18 +255,18 @@ test.describe('cli codegen', () => {
|
||||
})()
|
||||
]);
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.locator('input[name="name"]').fill('てすと');`);
|
||||
await page.locator('#input').fill('てすと');`);
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
page.locator("input[name=\\\"name\\\"]").fill("てすと");`);
|
||||
page.locator("#input").fill("てすと");`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
page.locator(\"input[name=\\\"name\\\"]\").fill(\"てすと\")`);
|
||||
page.locator("#input").fill(\"てすと\")`);
|
||||
|
||||
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(`
|
||||
await page.Locator(\"input[name=\\\"name\\\"]\").FillAsync(\"てすと\");`);
|
||||
await page.Locator("#input").FillAsync(\"てすと\");`);
|
||||
|
||||
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>`);
|
||||
const locator = await recorder.focusElement('textarea');
|
||||
expect(locator).toBe(`locator('textarea[name="name"]')`);
|
||||
expect(locator).toBe(`locator('#textarea')`);
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -284,7 +284,7 @@ test.describe('cli codegen', () => {
|
||||
page.fill('textarea', 'John')
|
||||
]);
|
||||
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');
|
||||
});
|
||||
|
||||
@ -294,7 +294,7 @@ test.describe('cli codegen', () => {
|
||||
await recorder.setContentAndWait(`<input name="name" onkeypress="console.log('press')"></input>`);
|
||||
|
||||
const locator = await recorder.focusElement('input');
|
||||
expect(locator).toBe(`locator('input[name="name"]')`);
|
||||
expect(locator).toBe(`getByRole('textbox')`);
|
||||
|
||||
const messages: any[] = [];
|
||||
page.on('console', message => messages.push(message));
|
||||
@ -305,19 +305,19 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
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(`
|
||||
page.locator("input[name=\\\"name\\\"]").press("Shift+Enter");`);
|
||||
page.getByRole(AriaRole.TEXTBOX).press("Shift+Enter");`);
|
||||
|
||||
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(`
|
||||
await page.locator(\"input[name=\\\"name\\\"]\").press(\"Shift+Enter\")`);
|
||||
await page.get_by_role("textbox").press("Shift+Enter")`);
|
||||
|
||||
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');
|
||||
});
|
||||
@ -357,7 +357,7 @@ test.describe('cli codegen', () => {
|
||||
await recorder.setContentAndWait(`<input name="name" onkeydown="console.log('press:' + event.key)"></input>`);
|
||||
|
||||
const locator = await recorder.focusElement('input');
|
||||
expect(locator).toBe(`locator('input[name="name"]')`);
|
||||
expect(locator).toBe(`getByRole('textbox')`);
|
||||
|
||||
const messages: any[] = [];
|
||||
page.on('console', message => {
|
||||
@ -369,7 +369,7 @@ test.describe('cli codegen', () => {
|
||||
page.press('input', 'ArrowDown')
|
||||
]);
|
||||
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');
|
||||
});
|
||||
|
||||
@ -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>`);
|
||||
|
||||
const locator = await recorder.focusElement('input');
|
||||
expect(locator).toBe(`locator('input[name="name"]')`);
|
||||
expect(locator).toBe(`getByRole('textbox')`);
|
||||
|
||||
const messages: any[] = [];
|
||||
page.on('console', message => {
|
||||
@ -392,7 +392,7 @@ test.describe('cli codegen', () => {
|
||||
page.press('input', 'ArrowDown')
|
||||
]);
|
||||
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[0].text()).toBe('down: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>`);
|
||||
|
||||
const locator = await recorder.focusElement('input');
|
||||
expect(locator).toBe(`locator('input[name="accept"]')`);
|
||||
expect(locator).toBe(`locator('#checkbox')`);
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -413,19 +413,19 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.locator('input[name="accept"]').check();`);
|
||||
await page.locator('#checkbox').check();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
page.locator("input[name=\\\"accept\\\"]").check();`);
|
||||
page.locator("#checkbox").check();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
page.locator(\"input[name=\\\"accept\\\"]\").check()`);
|
||||
page.locator("#checkbox").check()`);
|
||||
|
||||
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(`
|
||||
await page.Locator(\"input[name=\\\"accept\\\"]\").CheckAsync();`);
|
||||
await page.Locator("#checkbox").CheckAsync();`);
|
||||
|
||||
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>`);
|
||||
|
||||
const locator = await recorder.focusElement('input');
|
||||
expect(locator).toBe(`locator('input[name="accept"]')`);
|
||||
expect(locator).toBe(`locator('#checkbox')`);
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -445,7 +445,7 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.locator('input[name="accept"]').check();`);
|
||||
await page.locator('#checkbox').check();`);
|
||||
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>`);
|
||||
|
||||
const locator = await recorder.focusElement('input');
|
||||
expect(locator).toBe(`locator('input[name="accept"]')`);
|
||||
expect(locator).toBe(`locator('#checkbox')`);
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -464,7 +464,7 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.locator('input[name="accept"]').check();`);
|
||||
await page.locator('#checkbox').check();`);
|
||||
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>`);
|
||||
|
||||
const locator = await recorder.focusElement('input');
|
||||
expect(locator).toBe(`locator('input[name="accept"]')`);
|
||||
expect(locator).toBe(`locator('#checkbox')`);
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -483,19 +483,19 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.locator('input[name="accept"]').uncheck();`);
|
||||
await page.locator('#checkbox').uncheck();`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
page.locator("input[name=\\\"accept\\\"]").uncheck();`);
|
||||
page.locator("#checkbox").uncheck();`);
|
||||
|
||||
expect(sources.get('Python').text).toContain(`
|
||||
page.locator(\"input[name=\\\"accept\\\"]\").uncheck()`);
|
||||
page.locator("#checkbox").uncheck()`);
|
||||
|
||||
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(`
|
||||
await page.Locator(\"input[name=\\\"accept\\\"]\").UncheckAsync();`);
|
||||
await page.Locator("#checkbox").UncheckAsync();`);
|
||||
|
||||
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>');
|
||||
|
||||
const locator = await recorder.hoverOverElement('select');
|
||||
expect(locator).toBe(`locator('select')`);
|
||||
expect(locator).toBe(`locator('#age')`);
|
||||
|
||||
const [message, sources] = await Promise.all([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -515,19 +515,19 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.locator('select').selectOption('2');`);
|
||||
await page.locator('#age').selectOption('2');`);
|
||||
|
||||
expect(sources.get('Java').text).toContain(`
|
||||
page.locator("select").selectOption("2");`);
|
||||
page.locator("#age").selectOption("2");`);
|
||||
|
||||
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(`
|
||||
await page.locator(\"select\").select_option(\"2\")`);
|
||||
await page.locator("#age").select_option("2")`);
|
||||
|
||||
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');
|
||||
});
|
||||
@ -624,8 +624,8 @@ test.describe('cli codegen', () => {
|
||||
await recorder.page.keyboard.insertText('@');
|
||||
await recorder.page.keyboard.type('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).toContain(`await page.locator('input').fill('playwright@example.com');`);
|
||||
expect(recorder.sources().get('JavaScript').text).not.toContain(`await page.getByRole('textbox').press('AltGraph');`);
|
||||
expect(recorder.sources().get('JavaScript').text).toContain(`await page.getByRole('textbox').fill('playwright@example.com');`);
|
||||
});
|
||||
|
||||
test('should middle click', async ({ page, openRecorder, server }) => {
|
||||
|
@ -121,19 +121,19 @@ test.describe('cli codegen', () => {
|
||||
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
|
||||
|
||||
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(`
|
||||
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(`
|
||||
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(`
|
||||
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(`
|
||||
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 }) => {
|
||||
@ -153,19 +153,19 @@ test.describe('cli codegen', () => {
|
||||
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
|
||||
|
||||
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(`
|
||||
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(`
|
||||
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(`
|
||||
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(`
|
||||
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 }) => {
|
||||
@ -185,20 +185,19 @@ test.describe('cli codegen', () => {
|
||||
const sources = await recorder.waitForOutput('JavaScript', 'setInputFiles');
|
||||
|
||||
expect(sources.get('JavaScript').text).toContain(`
|
||||
await page.locator('input[type=\"file\"]').setInputFiles([]);`);
|
||||
await page.getByRole('textbox').setInputFiles([]);`);
|
||||
|
||||
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(`
|
||||
page.locator(\"input[type=\\\"file\\\"]\").set_input_files([])`);
|
||||
page.get_by_role("textbox").set_input_files([])`);
|
||||
|
||||
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(`
|
||||
await page.Locator(\"input[type=\\\"file\\\"]\").SetInputFilesAsync(new[] { });`);
|
||||
|
||||
await page.GetByRole(AriaRole.Textbox).SetInputFilesAsync(new[] { });`);
|
||||
});
|
||||
|
||||
test('should download files', async ({ page, openRecorder, server }) => {
|
||||
@ -381,20 +380,20 @@ test.describe('cli codegen', () => {
|
||||
await recorder.waitForOutput('JavaScript', 'TextB');
|
||||
|
||||
const sources = recorder.sources();
|
||||
expect(sources.get('JavaScript').text).toContain(`await page1.locator('input').fill('TextA');`);
|
||||
expect(sources.get('JavaScript').text).toContain(`await page2.locator('input').fill('TextB');`);
|
||||
expect(sources.get('JavaScript').text).toContain(`await page1.locator('#name').fill('TextA');`);
|
||||
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(`page2.locator("input").fill("TextB");`);
|
||||
expect(sources.get('Java').text).toContain(`page1.locator("#name").fill("TextA");`);
|
||||
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(`page2.locator(\"input\").fill(\"TextB\")`);
|
||||
expect(sources.get('Python').text).toContain(`page1.locator("#name").fill("TextA")`);
|
||||
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 page2.locator(\"input\").fill(\"TextB\")`);
|
||||
expect(sources.get('Python Async').text).toContain(`await page1.locator("#name").fill("TextA")`);
|
||||
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 page2.Locator(\"input\").FillAsync(\"TextB\");`);
|
||||
expect(sources.get('C#').text).toContain(`await page1.Locator("#name").FillAsync("TextA");`);
|
||||
expect(sources.get('C#').text).toContain(`await page2.Locator("#name").FillAsync("TextB");`);
|
||||
});
|
||||
|
||||
test('click should emit events in order', async ({ page, openRecorder }) => {
|
||||
@ -429,7 +428,7 @@ test.describe('cli codegen', () => {
|
||||
recorder.waitForActionPerformed(),
|
||||
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 }) => {
|
||||
@ -441,7 +440,7 @@ test.describe('cli codegen', () => {
|
||||
recorder.waitForActionPerformed(),
|
||||
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 }) => {
|
||||
@ -502,7 +501,7 @@ test.describe('cli codegen', () => {
|
||||
|
||||
await recorder.setContentAndWait(`<textarea spellcheck=false id="textarea" name="name" oninput="console.log(textarea.value)"></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([
|
||||
page.waitForEvent('console', msg => msg.type() !== 'error'),
|
||||
@ -511,19 +510,19 @@ test.describe('cli codegen', () => {
|
||||
]);
|
||||
|
||||
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(`
|
||||
page.locator("textarea[name=\\\"name\\\"]").fill("Hello'\\"\`\\nWorld");`);
|
||||
page.locator("#textarea").fill("Hello'\\"\`\\nWorld");`);
|
||||
|
||||
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(`
|
||||
await page.locator(\"textarea[name=\\\"name\\\"]\").fill(\"Hello'\\"\`\\nWorld\")`);
|
||||
await page.locator("#textarea").fill(\"Hello'\\"\`\\nWorld\")`);
|
||||
|
||||
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');
|
||||
});
|
||||
|
@ -304,29 +304,6 @@ it.describe(() => {
|
||||
});
|
||||
|
||||
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(`
|
||||
<div>Hello <span>world</span></div>
|
||||
<div>Goodbye <span mark=1>world</span></div>
|
||||
|
@ -78,7 +78,7 @@ it.describe('selector generator', () => {
|
||||
<select><option>foo</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 }) => {
|
||||
@ -167,7 +167,7 @@ it.describe('selector generator', () => {
|
||||
<div>Hello <span>world</span></div>
|
||||
<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 }) => {
|
||||
@ -246,21 +246,25 @@ it.describe('selector generator', () => {
|
||||
<input value="two" mark="1">
|
||||
<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('name', async ({ page }) => {
|
||||
it.describe('should prioritise attributes correctly', () => {
|
||||
it('role', async ({ page }) => {
|
||||
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 }) => {
|
||||
await page.setContent(`<input placeholder="foobar" type="text"/>`);
|
||||
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 }) => {
|
||||
await page.setContent(`<input type="text"/>`);
|
||||
expect(await generate(page, 'input')).toBe('input[type="text"]');
|
||||
await page.setContent(`<input role="presentation" aria-hidden="false" type="checkbox"/>`);
|
||||
expect(await generate(page, 'input')).toBe('input[type="checkbox"]');
|
||||
});
|
||||
});
|
||||
|
||||
@ -282,7 +286,7 @@ it.describe('selector generator', () => {
|
||||
const input = document.createElement('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 }) => {
|
||||
@ -300,7 +304,7 @@ it.describe('selector generator', () => {
|
||||
input2.setAttribute('value', 'foo');
|
||||
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 }) => {
|
||||
@ -352,7 +356,7 @@ it.describe('selector generator', () => {
|
||||
});
|
||||
|
||||
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 => {
|
||||
delete window.CSS.escape;
|
||||
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>`);
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
@ -34,8 +34,8 @@ it('should fail page.fill in strict mode', async ({ page }) => {
|
||||
await page.setContent(`<input></input><div><input></input></div>`);
|
||||
const error = await page.fill('input', 'text', { strict: true }).catch(e => e);
|
||||
expect(error.message).toContain('strict mode violation');
|
||||
expect(error.message).toContain(`1) <input/> aka locator('input').first()`);
|
||||
expect(error.message).toContain(`2) <input/> aka locator('div input')`);
|
||||
expect(error.message).toContain(`1) <input/> aka getByRole('textbox').first()`);
|
||||
expect(error.message).toContain(`2) <input/> aka locator('div').getByRole('textbox')`);
|
||||
});
|
||||
|
||||
it('should fail page.$ in strict mode', async ({ page }) => {
|
||||
|
Loading…
Reference in New Issue
Block a user