You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Bug]: getByText/getByLabel with a global or sticky regex (/x/g, /x/y) match only some of the elements #42813
import{test,expect}from'@playwright/test';test('regex with g flag',async({ page })=>{awaitpage.setContent(` <div>foo</div><div>foo</div><div>foo</div> <label>foo<input></label><label>foo<input></label><label>foo<input></label> `);console.log(awaitpage.getByText(/foo/).count());// 6console.log(awaitpage.getByText(/foo/g).count());// 4console.log(awaitpage.getByLabel(/foo/g).count());// 2console.log(awaitpage.getByText(/foo/y).count());// 4});
Expected behavior
The g and y flags have no meaning for a locator match, so /foo/g should match the same elements as /foo/. getByRole('button', { name: /foo/g }) and filter({ hasText: /foo/g }) already behave that way.
Actual behavior
Every other matching element is skipped, depending on traversal order:
6
4
2
4
Additional context
createTextMatcher in packages/injected/src/injectedScript.ts builds one RegExp and reuses re.test(elementText.full) for every element. RegExp.prototype.test is stateful when the g or y flag is set: lastIndex advances after a hit and only resets after a miss, so the next element is tested from a non-zero offset. escapeRegexForSelector passes user flags through verbatim and the selector parser accepts [dgimsuvy], so the flag reaches the engine.
Role-name matching uses String.prototype.match and has-text builds a fresh RegExp per element, which is why those are unaffected and the inconsistency is confusing. Copying a /pattern/g regex from String.replace code into getByText is an easy habit; the result is a silently wrong count or a hidden strict-mode violation.
Activity