Decide which of the two elements you actually want, then say so in the locator. Strict mode fires when getByText('Submit') matches two elements - locally your seed data renders one, but CI's dataset renders a second (a duplicate row, a second form, a modal plus the page behind it). Do not just slap .first() on it and move on: first check whether the duplicate is legitimate test data or a rendering bug, because clicking the wrong Submit can make the test pass while testing nothing. The durable fix is a more specific locator - scope it to the form, dialog, or row under test - so the test keeps working when the data changes again.

```text
playwright "strict mode violation: getByText('Submit') resolved to 2 elements" appearing only in CI data
```

## Steps
1. Find the duplicate: run against the CI dataset (or a local copy of the seed) and inspect what the second matching element is - screenshot or DOM dump around the locator.
   Expected: you can describe both matched elements: which one is the intended target and what the extra one is.

2. Decide if the duplicate is a data bug or a test bug. If CI seed data creates two identical forms, fix the seed; if the app legitimately renders two Submits, fix the locator.
   Expected: the duplicate is classified: bad data (fix the seed) or real UI (scope the locator).

3. Scope the locator: chain it under the container under test (the dialog, the form, the table row) instead of searching the whole page. Prefer role-based locators with accessible names over bare getByText.
   Expected: the locator resolves to exactly one element against both local and CI data.

4. Re-run against both datasets. A locator fixed only against CI data can break against local data if the scoping was too narrow.
   Expected: green against local seed data and green against CI seed data.

5. If the CI dataset is shared, document the duplicate-trap (or fix the seed) so the next test hitting that page does not rediscover it.
   Expected: the seed data or the shared docs note the duplicate, not just your test.

## Use this when

- "strict mode violation" on getByText that appears only with CI seed data
- a locator matching one element locally and two in CI
- duplicate UI elements caused by dataset differences

## Not for this skill when

- strict-mode violations that reproduce locally too (same locator-scoping fix, but no data investigation needed)
- "element is not visible" timeouts (that is a timing problem, not a duplicate problem)

## Variant phrasings

### playwright strict mode violation

The general form. CI-only adds the dataset-diff investigation on top of locator scoping.

### getByText resolved to multiple elements

Same error, any text. Scope the locator to the container under test.

## Why it happens

Playwright's strict mode refuses to act when a locator matches more than one element, because acting on an ambiguous match would make tests pass or fail for the wrong reasons. getByText searches the whole page, so any second copy of the text - a duplicate row from seed data, a hidden modal, a mobile plus desktop layout - triggers the violation. Local and CI datasets often differ (CI seeds more rows, different tenants, extra fixtures), which is why the violation appears only in CI even though the locator code is identical.

## Edge cases

- two genuinely identical buttons where either is fine: scope by container anyway - "either is fine" breaks the day the layout changes
- the duplicate is inside a hidden element: strict mode counts hidden matches too - check visibility, but still scope the locator rather than filtering by visible
- localizing the app later: getByText on literal English strings breaks under i18n - role-based locators with names survive translation

## Provenance

Resolved from the public thread: https://vectle.com/posts/pst_299gOZVB2wXumfkxCqSvRA
