An issue card in a Playwright suite reads '3 comments' with a count that changes between runs — how do you locate it by text?
answer
- A string bakes today's data into the locator
- Switch the argument type, not the string
- One option stops meaning anything here
- Flags do not come along for free
- Finding and checking are separate jobs
basics
~20 sPass a RegExp instead of a string: page.getByText with a pattern such as one matching digits followed by the word comments. The exact option is ignored for a RegExp, and the pattern is case-sensitive unless you add the i flag.
solid answer
~40 sGive `page.getByText` a `RegExp` rather than a string — a pattern anchored as `/^\d+ comments$/` matches the card whatever the count is. Two rules come with that switch. First, `{ exact: true }` is **ignored** when the argument is a RegExp, so anchoring is your job: without `^` and `$` the pattern behaves like a substring search. Second, a RegExp does **not** inherit the case-insensitivity of the string form; add the `i` flag yourself if casing may vary. The same applies to `getByPlaceholder`, `getByAltText`, `getByTitle` and `getByTestId`, which all accept a string or a RegExp. If the varying part is not what identifies the element, the better move is often to locate by the stable part and assert the count separately.
code
typescript · 11 linesimport { expect, test } from '@playwright/test';
test('a changing comment count needs a pattern, not a string', async ({ page }) => {
await page.setContent('<article><span class="meta">3 comments</span></article>');
// Anchored: matches whatever the count is, and nothing longer.
await expect(page.getByText(/^\d+ comments$/)).toBeVisible();
// Case-insensitivity is not inherited from the string form; add the flag.
await expect(page.getByText(/COMMENTS/i)).toBeVisible();
});go deeper
Remember that these queries accept a regular expression as well as a string, and that a pattern is the way to cope with text containing a number or date that changes between runs.
Explain the two rules that change with a pattern: the exact option no longer applies, so you anchor with caret and dollar, and case-insensitivity is not inherited, so you add the i flag when you want it.
Show the debugging judgment: decide whether the varying text identifies the element or is data to assert. Locating by the stable part and asserting the count separately turns a silent timeout into a readable diff.
Set the boundary for the suite. Patterns spreading through locators is a signal that components lack stable identity, and the durable fix is upstream in the markup rather than more regular expressions in tests.
## Why the string form breaks `page.getByText('3 comments')` encodes today's data in the locator. On an issue tracker where anyone can comment, the count moves the moment a fixture changes, another test posts, or the seed data is regenerated — and the failure reads as *element not found*, which sends people hunting for a rendering bug that is not there. Any locator that embeds a value the application owns is a scheduled flake. Loosening to `getByText('comments')` trades one problem for another: it is a case-insensitive substring match, so it also matches the `Comments` section heading, an empty-state line reading `No comments yet`, and every other card on the board. ## The RegExp form Every text-shaped query — `getByText`, `getByLabel`, `getByPlaceholder`, `getByAltText`, `getByTitle` — takes a `string` **or** a `RegExp`, as does `getByTestId`. Passing a pattern is the idiomatic answer to text with a variable part: ```ts await expect(page.getByText(/^\d+ comments$/)).toBeVisible(); ``` Two rules change the moment the argument stops being a string. 1. **`exact` is ignored.** The option only means anything for string arguments. With a RegExp, whole-string matching is expressed by anchoring the pattern with `^` and `$`; leave the anchors out and the pattern matches anywhere in the text, which is the RegExp equivalent of the loose default. 2. **Case sensitivity is yours to set.** A string argument is compared case-insensitively unless you pass `exact`. A RegExp carries only the flags you gave it, so `/comments/` is case-sensitive and `/comments/i` is not. Candidates who assume the pattern inherits the loose default write locators that pass locally and fail against a locale or a design tweak that recases the label. | Argument | Whole string? | Case-insensitive? | |---|---|---| | `'3 comments'` | no, substring | yes | | `'3 comments', { exact: true }` | yes | no | | `/\d+ comments/` | no, matches anywhere | no | | `/^\d+ comments$/` | yes, by anchors | no | | `/^\d+ COMMENTS$/i` | yes, by anchors | yes | ## Write the pattern narrowly A pattern is a locator, so treat it with the same suspicion as a selector. - **Anchor it** unless you deliberately want a substring; `/\d+ comments/` also matches `13 comments pending review`. - **Keep the variable part narrow**: `\d+` says the count is digits, where `.*` would match half the page's copy. - **Escape regex metacharacters** in the literal part — a `(`, `?`, `+` or `.` in the copy changes the pattern's meaning silently. - **Do not encode punctuation you have not verified**, especially a non-breaking space or a typographic apostrophe that a design system inserted. ## Often the better answer Reaching for a pattern is right when the varying text *is* the identity of the element. It frequently is not. On an issue card, the count is data being displayed, not the way a user identifies the card: 1. Locate the card by something stable — its title, or the test id the component owns. 2. Scope into it for the comment counter. 3. Assert the number with a text assertion rather than baking it into the locator. That split gives a far better failure message: instead of *element not found*, the run says the counter read `4` where `3` was expected, which is the actual diagnosis. As a rule of thumb, use a RegExp to *find* an element despite noise, and an assertion to *check* the value — a locator that both finds and verifies collapses those two signals into one unhelpful timeout. ## Interview framing State the mechanism first — a RegExp argument, anchored, with the `i` flag added deliberately — and name the two rules that trip people up: `exact` is ignored and case-insensitivity is not inherited. Then show judgment by separating identification from verification, and say when you would locate by the stable part instead. Answering only the first half reads as knowing the API; adding the second reads as having debugged a flaky suite.
- Does passing { exact: true } alongside a RegExp change anything?No, the option is ignored whenever the argument is a RegExp. Whole-string behaviour comes from anchoring the pattern with `^` and `$` instead. Leaving `exact` in the call is misleading to the next reader, so drop it once you switch to a pattern.
- Which other built-in queries accept a RegExp?`getByText`, `getByLabel`, `getByPlaceholder`, `getByAltText` and `getByTitle` all take a string or a RegExp, and so does `getByTestId` — which is notable because it has no `exact` option, making a pattern the only way to match a family of generated ids.
saying these in an interview costs you the question
- Says a RegExp argument is case-insensitive by default
- Thinks exact: true still applies to a pattern
- Uses an unanchored pattern and expects a whole-string match
- Bakes the current count into the locator string
- Puts the assertion inside the locator instead of an expectation