fix: use no internal selectors for frame locators (#19964)

Fixes https://github.com/microsoft/playwright/issues/19406
This commit is contained in:
Max Schmitt 2023-01-11 21:53:19 +01:00 committed by GitHub
parent 4f837690e2
commit a7495c3326
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 157 additions and 7 deletions

View File

@ -34,6 +34,7 @@ const kExactPenalty = kTextScoreRange / 2;
const kTestIdScore = 1; // testIdAttributeName
const kOtherTestIdScore = 2; // other data-test* attributes
const kIframeByAttributeScore = 10;
const kBeginPenalizedScore = 50;
const kPlaceholderScore = 100;
@ -174,14 +175,40 @@ 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: kTestIdScore });
// Start of generic candidates which are compatible for Locators and FrameLocators:
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: kOtherTestIdScore });
}
const idAttr = element.getAttribute('id');
if (idAttr && !isGuidLike(idAttr))
candidates.push({ engine: 'css', selector: makeSelectorForId(idAttr), score: kCSSIdScore });
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore });
if (element.nodeName === 'IFRAME') {
for (const attribute of ['name', 'title']) {
if (element.getAttribute(attribute))
candidates.push({ engine: 'css', selector: `${cssEscape(element.nodeName.toLowerCase())}[${attribute}=${quoteAttributeValue(element.getAttribute(attribute)!)}]`, score: kIframeByAttributeScore });
}
// Get via testIdAttributeName via CSS selector.
if (element.getAttribute(testIdAttributeName))
candidates.push({ engine: 'css', selector: `[${testIdAttributeName}=${escapeForAttributeSelector(element.getAttribute(testIdAttributeName)!, true!)}]`, score: kTestIdScore });
penalizeScoreForLength([candidates]);
return candidates;
}
// Everything after that are candidates that are not applicable to iframes and designed for Locators only(getBy* methods):
// Get via testIdAttributeName via GetByTestId().
if (element.getAttribute(testIdAttributeName))
candidates.push({ engine: 'internal:testid', selector: `[${testIdAttributeName}=${escapeForAttributeSelector(element.getAttribute(testIdAttributeName)!, true)}]`, score: kTestIdScore });
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA') {
const input = element as HTMLInputElement | HTMLTextAreaElement;
if (input.placeholder) {
@ -228,11 +255,6 @@ function buildCandidates(injectedScript: InjectedScript, element: Element, testI
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: kCSSIdScore });
candidates.push({ engine: 'css', selector: cssEscape(element.nodeName.toLowerCase()), score: kCSSTagNameScore });
penalizeScoreForLength([candidates]);
return candidates;
}

View File

@ -203,6 +203,134 @@ test.describe('cli codegen', () => {
await page.FrameByUrl(\"about:blank\").GetByText(\"HelloNameAnonymous\").ClickAsync();`);
});
test('should generate frame locators with title attribute', async ({ page, openRecorder, server }) => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`
<iframe title="hello world" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'Click me'),
page.frameLocator('[title="hello world"]').getByRole('button', { name: 'Click me' }).click(),
]);
expect(sources.get('JavaScript').text).toContain(
`await page.frameLocator('iframe[title="hello world"]').getByRole('button', { name: 'Click me' }).click();`
);
expect(sources.get('Java').text).toContain(
`page.frameLocator(\"iframe[title=\\\"hello world\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();`
);
expect(sources.get('Python').text).toContain(
`page.frame_locator(\"iframe[title=\\\"hello world\\\"]\").get_by_role(\"button\", name=\"Click me\").click()`
);
expect(sources.get('Python Async').text).toContain(
`await page.frame_locator("iframe[title=\\\"hello world\\\"]").get_by_role("button", name="Click me").click()`
);
expect(sources.get('C#').text).toContain(
`await page.FrameLocator("iframe[title=\\\"hello world\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
);
});
test('should generate frame locators with name attribute', async ({ page, openRecorder, server }) => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`
<iframe name="hello world" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'Click me'),
page.frameLocator('[name="hello world"]').getByRole('button', { name: 'Click me' }).click(),
]);
expect(sources.get('JavaScript').text).toContain(
`await page.frameLocator('iframe[name="hello world"]').getByRole('button', { name: 'Click me' }).click();`
);
expect(sources.get('Java').text).toContain(
`page.frameLocator(\"iframe[name=\\\"hello world\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();`
);
expect(sources.get('Python').text).toContain(
`page.frame_locator(\"iframe[name=\\\"hello world\\\"]\").get_by_role(\"button\", name=\"Click me\").click()`
);
expect(sources.get('Python Async').text).toContain(
`await page.frame_locator("iframe[name=\\\"hello world\\\"]").get_by_role("button", name="Click me").click()`
);
expect(sources.get('C#').text).toContain(
`await page.FrameLocator("iframe[name=\\\"hello world\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
);
});
test('should generate frame locators with id attribute', async ({ page, openRecorder, server }) => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`
<iframe id="hello-world" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'Click me'),
page.frameLocator('[id="hello-world"]').getByRole('button', { name: 'Click me' }).click(),
]);
expect(sources.get('JavaScript').text).toContain(
`await page.frameLocator('#hello-world').getByRole('button', { name: 'Click me' }).click();`
);
expect(sources.get('Java').text).toContain(
`page.frameLocator(\"#hello-world\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();`
);
expect(sources.get('Python').text).toContain(
`page.frame_locator(\"#hello-world\").get_by_role(\"button\", name=\"Click me\").click()`
);
expect(sources.get('Python Async').text).toContain(
`await page.frame_locator("#hello-world").get_by_role("button", name="Click me").click()`
);
expect(sources.get('C#').text).toContain(
`await page.FrameLocator("#hello-world").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
);
});
test('should generate frame locators with testId', async ({ page, openRecorder, server }) => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`
<iframe data-testid="my-testid" srcdoc="<button>Click me</button>"></iframe>
`, server.EMPTY_PAGE, 1);
const [sources] = await Promise.all([
recorder.waitForOutput('JavaScript', 'my-testid'),
page.frameLocator('iframe[data-testid="my-testid"]').getByRole('button', { name: 'Click me' }).click(),
]);
expect(sources.get('JavaScript').text).toContain(
`await page.frameLocator('[data-testid="my-testid"]').getByRole('button', { name: 'Click me' }).click();`
);
expect(sources.get('Java').text).toContain(
`page.frameLocator(\"[data-testid=\\\"my-testid\\\"]\").getByRole(AriaRole.BUTTON, new FrameLocator.GetByRoleOptions().setName(\"Click me\")).click();`
);
expect(sources.get('Python').text).toContain(
`page.frame_locator(\"[data-testid=\\\"my-testid\\\"]\").get_by_role(\"button\", name=\"Click me\").click()`
);
expect(sources.get('Python Async').text).toContain(
`await page.frame_locator("[data-testid=\\\"my-testid\\\"]").get_by_role("button", name="Click me").click()`
);
expect(sources.get('C#').text).toContain(
`await page.FrameLocator("[data-testid=\\\"my-testid\\\"]").GetByRole(AriaRole.Button, new() { Name = "Click me" }).ClickAsync();`
);
});
test('should generate role locators undef frame locators', async ({ page, openRecorder, server }) => {
const recorder = await openRecorder();
await recorder.setContentAndWait(`<iframe id=frame1 srcdoc="<button>Submit</button>">`, server.EMPTY_PAGE, 2);