fix(selector generator): do not produce has-text="foo"s (#21679)

There is no locator counterpart for it. Instead, produce a regex.

Also fix locator generator to not produce incorrect locator in this
case.

Fixes #21649.
This commit is contained in:
Dmitry Gozman 2023-03-15 13:43:42 -07:00 committed by GitHub
parent b149d132a6
commit bde2e90973
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 25 additions and 5 deletions

View File

@ -43,6 +43,7 @@ const kRoleWithNameScore = 140;
const kAltTextScore = 160;
const kTextScore = 180;
const kTitleScore = 200;
const kTextScoreRegex = 250;
const kPlaceholderScoreExact = kPlaceholderScore + kExactPenalty;
const kLabelScoreExact = kLabelScore + kExactPenalty;
const kRoleWithNameScoreExact = kRoleWithNameScore + kExactPenalty;
@ -268,11 +269,10 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
const candidates: SelectorToken[][] = [];
const escaped = escapeForTextSelector(text, false);
const exactEscaped = escapeForTextSelector(text, true);
if (isTargetNode) {
candidates.push([{ engine: 'internal:text', selector: escaped, score: kTextScore }]);
candidates.push([{ engine: 'internal:text', selector: exactEscaped, score: kTextScoreExact }]);
candidates.push([{ engine: 'internal:text', selector: escapeForTextSelector(text, true), score: kTextScoreExact }]);
}
const ariaRole = getAriaRole(element);
@ -289,7 +289,8 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
candidate.push({ engine: 'css', selector: element.nodeName.toLowerCase(), score: kCSSTagNameScore });
}
candidates.push([...candidate, { engine: 'internal:has-text', selector: escaped, score: kTextScore }]);
candidates.push([...candidate, { engine: 'internal:has-text', selector: exactEscaped, score: kTextScoreExact }]);
if (text.length <= 80)
candidates.push([...candidate, { engine: 'internal:has-text', selector: '/^' + escapeRegExp(text) + '$/', score: kTextScoreRegex }]);
penalizeScoreForLength(candidates);
return candidates;
}
@ -467,3 +468,8 @@ function isGuidLike(id: string): boolean {
}
return transitionCount >= id.length / 4;
}
function escapeRegExp(s: string) {
// From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}

View File

@ -75,8 +75,11 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
}
if (part.name === 'internal:has-text') {
const { exact, text } = detectExact(part.body as string);
tokens.push(factory.generateLocator(base, 'has-text', text, { exact }));
continue;
// There is no locator equivalent for strict has-text, leave it as is.
if (!exact) {
tokens.push(factory.generateLocator(base, 'has-text', text, { exact }));
continue;
}
}
if (part.name === 'internal:has') {
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);

View File

@ -347,6 +347,8 @@ it.describe(() => {
javascript: `locator('div').filter({ hasText: 'Goodbye world' }).locator('span')`,
python: 'locator("div").filter(has_text="Goodbye world").locator("span")',
});
expect.soft(asLocator('javascript', 'div >> internal:has-text="foo"s', false)).toBe(`locator('div').locator('internal:has-text="foo"s')`);
});
});

View File

@ -162,6 +162,15 @@ it.describe('selector generator', () => {
expect(await generate(page, 'a:has-text("Hello")')).toBe(`a >> internal:has-text="Hello world"i`);
});
it('should use internal:has-text with regexp', async ({ page }) => {
await page.setContent(`
<span>Hello world</span>
<div><div>Hello <span>world</span></div>extra</div>
<a>Goodbye <span>world</span></a>
`);
expect(await generate(page, 'div div')).toBe(`div >> internal:has-text=/^Hello world$/`);
});
it('should chain text after parent', async ({ page }) => {
await page.setContent(`
<div>Hello <span>world</span></div>