[Feature]: Option to make all locators ignore hidden elements by default

クローズ
#43,855 コメント 6 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

関連するプルリクエストがすでにマージされています。

  • #43859 @microsoft-playwright-automation による — マージ済み
  • #43903 @microsoft-playwright-automation による — マージ済み

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
18/100
issue の種類
機能追加
明瞭さ
明確に書かれている
活発さ
停滞
技術スタック
typescript
領域
testing

調査の方向性

Start with the merged pull request #43859 linked from this issue, since it may already cover part of the request. Look at the existing strictSelectors and testIdAttribute context options, and at how locator.visible() and getByRole's includeHidden filter hidden elements. Done means a context option that applies the visibility filter to getByTestId, getByText and locator() by default, with a per-locator includeHidden: true opt-out, and visibility assertions behaving as the issue describes.

索引モデルが issue の本文から書いたものです。

説明

🚀 Feature Request

Add a context/config option that makes every locator skip hidden elements by default, as if .visible() (added in v1.63) were applied to them. The option name is only a suggestion.

// playwright.config.ts
export default defineConfig({
  use: {
    locatorIncludeHidden: false, // default: true = current behaviour, nothing breaks
  },
});

Per-locator opt-out, reusing the name getByRole already has:

page.getByTestId('save', { includeHidden: true });
page.locator('.toast', { includeHidden: true });

Follow-up to #35294, which was closed in the P3 cleanup (#43813) with ~32 👍. Related: #31840, #36045.

Example

Playwright v1.64.1:

test('hidden duplicate from a cached route', async ({ page }) => {
  await page.setContent(`
    <div hidden><button data-testid="save">Save</button></div>  <!-- previous route, kept mounted -->
    <div><button data-testid="save">Save</button></div>          <!-- active route -->
  `);

  await page.getByRole('button', { name: 'Save' }).click(); // ✅ getByRole already skips hidden elements
  await page.getByTestId('save').visible().click();         // ✅ works only if you put explicit .visible()
  await page.getByTestId('save').click();                   // ❌ strict mode violation
});

Actual output of the example above on v1.64.1 (Chromium):

Error: locator.click: Error: strict mode violation: getByTestId('save') resolved to 2 elements:
    1) <button data-testid="save">Save</button> aka getByTestId('save').first()
    2) <button data-testid="save">Save</button> aka getByRole('button', { name: 'Save' })

Call log:
  - waiting for getByTestId('save')

   9 |   await page.getByRole('button', { name: 'Save' }).click(); // ✅ getByRole already skips hidden elements
  10 |   await page.getByTestId('save').visible().click();         // ✅ works only if you put explicit .visible()
> 11 |   await page.getByTestId('save').click();                   // ❌ strict mode violation
     |                                  ^

Lines 9 and 10 pass and line 11 fails. Playwright itself suggests getByRole() for element 2: it identifies the visible button uniquely, because getByRole already ignores hidden elements. That is exactly the behavior this issue asks to make available to all locators.

Motivation

UI that stays mounted while hidden is now the default in mainstream stacks:

  • React 19.2 <Activity mode="hidden"> keeps previously rendered UI in the DOM.
  • Next.js cacheComponents keeps visited routes mounted and hidden.
  • Streaming SSR and hydration briefly leave hidden duplicates in the DOM, which causes flaky strict-mode violations (see comments in #35294).
  • react-navigation / react-native-web keep stacked screens mounted.

In these apps, almost no test ever needs to target a hidden element. Matching hidden elements is the exception, but today it is the default.

Why locator.visible() (v1.63) is not enough

.visible() made opting in shorter, but it is still opt-in per locator:

  1. It has to go on nearly every locator, including those in page objects and shared fixtures. If you forget it once, the test does not fail clearly. It fails intermittently, only when a cached route or hydration duplicate happens to be in the DOM.
  2. Locator methods behave inconsistently. getByRole excludes hidden elements by default, but getByText, getByTestId, getByLabel, getByPlaceholder and locator() do not.
  3. Codegen and the recorder do not add .visible(), so every recorded locator has to be edited by hand (or AI 🙄).

"Config hides behaviour from the reader" (concern raised in #19235)

Config already changes how locators resolve: testIdAttribute and the strictSelectors context option both do. This option works the same way, is off by default, and lives in one visible place in the config. A team that enables it has decided its app keeps hidden UI mounted, which is a property of the whole project, not of a single locator.

Proposed semantics

To keep the scope small, reuse what already exists:

  • What counts as hidden: the same definition locator.visible() uses.
  • Where visibility is checked: only on the final matched element, exactly as if .visible() were appended.
  • What it affects: element resolution, so actions, strict mode, count(), all(), toHaveCount(), etc.
  • Visibility assertions keep their meaning:
    • toBeVisible() passes when a visible element matches.
    • toBeHidden() / waitFor({ state: 'hidden' }) pass when no visible element matches. That is already their contract, since a hidden element and a missing element both satisfy them.
  • Only attachment checks need the opt-out: toBeAttached() / waitFor({ state: 'attached' }) on a hidden element need { includeHidden: true }.
  • Implementation: a context option next to strictSelectors, as @dgozman suggested in #35294.
Alternatives considered
  • .visible() / .filter({ visible: true }) on every locator: the current workaround. See the problems above.
  • Using only getByRole: not always possible, for example with data-testid conventions, non-semantic markup, or migrations from Testing Library or Cypress.
主要言語
TypeScript
スター
99.1k
フォーク
6.8k
平均マージ
17時間 6分
マージ済み PR(30日)
306

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

microsoft/playwright のほかの issue

microsoft/playwright の issue をすべて見る

似ている issue

TypeScript の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。