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:
parent
b149d132a6
commit
bde2e90973
|
|
@ -43,6 +43,7 @@ const kRoleWithNameScore = 140;
|
||||||
const kAltTextScore = 160;
|
const kAltTextScore = 160;
|
||||||
const kTextScore = 180;
|
const kTextScore = 180;
|
||||||
const kTitleScore = 200;
|
const kTitleScore = 200;
|
||||||
|
const kTextScoreRegex = 250;
|
||||||
const kPlaceholderScoreExact = kPlaceholderScore + kExactPenalty;
|
const kPlaceholderScoreExact = kPlaceholderScore + kExactPenalty;
|
||||||
const kLabelScoreExact = kLabelScore + kExactPenalty;
|
const kLabelScoreExact = kLabelScore + kExactPenalty;
|
||||||
const kRoleWithNameScoreExact = kRoleWithNameScore + kExactPenalty;
|
const kRoleWithNameScoreExact = kRoleWithNameScore + kExactPenalty;
|
||||||
|
|
@ -268,11 +269,10 @@ function buildTextCandidates(injectedScript: InjectedScript, element: Element, i
|
||||||
const candidates: SelectorToken[][] = [];
|
const candidates: SelectorToken[][] = [];
|
||||||
|
|
||||||
const escaped = escapeForTextSelector(text, false);
|
const escaped = escapeForTextSelector(text, false);
|
||||||
const exactEscaped = escapeForTextSelector(text, true);
|
|
||||||
|
|
||||||
if (isTargetNode) {
|
if (isTargetNode) {
|
||||||
candidates.push([{ engine: 'internal:text', selector: escaped, score: kTextScore }]);
|
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);
|
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 });
|
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: 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);
|
penalizeScoreForLength(candidates);
|
||||||
return candidates;
|
return candidates;
|
||||||
}
|
}
|
||||||
|
|
@ -467,3 +468,8 @@ function isGuidLike(id: string): boolean {
|
||||||
}
|
}
|
||||||
return transitionCount >= id.length / 4;
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -75,8 +75,11 @@ function innerAsLocator(factory: LocatorFactory, parsed: ParsedSelector, isFrame
|
||||||
}
|
}
|
||||||
if (part.name === 'internal:has-text') {
|
if (part.name === 'internal:has-text') {
|
||||||
const { exact, text } = detectExact(part.body as string);
|
const { exact, text } = detectExact(part.body as string);
|
||||||
tokens.push(factory.generateLocator(base, 'has-text', text, { exact }));
|
// There is no locator equivalent for strict has-text, leave it as is.
|
||||||
continue;
|
if (!exact) {
|
||||||
|
tokens.push(factory.generateLocator(base, 'has-text', text, { exact }));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (part.name === 'internal:has') {
|
if (part.name === 'internal:has') {
|
||||||
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
const inner = innerAsLocator(factory, (part.body as NestedSelectorBody).parsed);
|
||||||
|
|
|
||||||
|
|
@ -347,6 +347,8 @@ it.describe(() => {
|
||||||
javascript: `locator('div').filter({ hasText: 'Goodbye world' }).locator('span')`,
|
javascript: `locator('div').filter({ hasText: 'Goodbye world' }).locator('span')`,
|
||||||
python: 'locator("div").filter(has_text="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')`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,15 @@ it.describe('selector generator', () => {
|
||||||
expect(await generate(page, 'a:has-text("Hello")')).toBe(`a >> internal:has-text="Hello world"i`);
|
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 }) => {
|
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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue