From 00d187ea748eb115d60b19dd7a2d9cd1800d40cc Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 2 Oct 2026 16:31:44 +0200 Subject: [PATCH 1/2] feat(devtools): add hideInAutomation to keep devtools out of e2e tests With plugins open by default, end-to-end tests that select elements by id or text also matched the devtools panels. The workarounds were turning off defaultOpen for every plugin or hiding #tanstack_devtools with CSS. `hideInAutomation: true` skips the devtools DOM when navigator.webdriver is true, which Playwright, Cypress, and Selenium set. Local development does not change. The default is false, so suites that test the devtools themselves keep working. Fixes #318 --- .changeset/hide-in-automation.md | 5 ++ docs/configuration.md | 6 ++ e2e/apps/react-vite/src/main.tsx | 3 + .../tests/hide-in-automation.spec.ts | 24 ++++++ .../devtools/src/context/devtools-store.ts | 8 ++ packages/devtools/src/devtools.tsx | 83 ++++++++++--------- packages/devtools/tests/workbench.test.tsx | 30 +++++++ 7 files changed, 119 insertions(+), 40 deletions(-) create mode 100644 .changeset/hide-in-automation.md create mode 100644 e2e/apps/react-vite/tests/hide-in-automation.spec.ts diff --git a/.changeset/hide-in-automation.md b/.changeset/hide-in-automation.md new file mode 100644 index 000000000..1baa8a3bd --- /dev/null +++ b/.changeset/hide-in-automation.md @@ -0,0 +1,5 @@ +--- +'@tanstack/devtools': minor +--- + +Add the `hideInAutomation` config option. When it is `true`, the devtools do not render in a browser that automation drives (Playwright, Cypress, Selenium), detected by `navigator.webdriver`. This keeps the devtools out of end-to-end test selectors. The default is `false`. diff --git a/docs/configuration.md b/docs/configuration.md index a8fe98fff..6240d6162 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -71,6 +71,12 @@ type KeyboardKey = ModifierKey | (string & {}); { requireUrlFlag: boolean } ``` +- `hideInAutomation` - Does not render the devtools when a browser is driven by automation (Playwright, Cypress, Selenium). This keeps the devtools out of your end-to-end test selectors. The check uses `navigator.webdriver`. The default is `false`. + +```ts +{ hideInAutomation: boolean } +``` + - `triggerImage` - The image used for the dev tools trigger ```ts diff --git a/e2e/apps/react-vite/src/main.tsx b/e2e/apps/react-vite/src/main.tsx index e30abcda6..d7a3c0a84 100644 --- a/e2e/apps/react-vite/src/main.tsx +++ b/e2e/apps/react-vite/src/main.tsx @@ -16,6 +16,9 @@ function App() { config={{ theme: 'dark', requireUrlFlag: new URLSearchParams(location.search).has('gated'), + hideInAutomation: new URLSearchParams(location.search).has( + 'automation-hidden', + ), }} plugins={[ { diff --git a/e2e/apps/react-vite/tests/hide-in-automation.spec.ts b/e2e/apps/react-vite/tests/hide-in-automation.spec.ts new file mode 100644 index 000000000..b8091d587 --- /dev/null +++ b/e2e/apps/react-vite/tests/hide-in-automation.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test' +import { DevtoolsPage } from '@tanstack/devtools-e2e' + +// Playwright sets navigator.webdriver to true. +test.describe('hideInAutomation', () => { + test('set → devtools not rendered', async ({ page }) => { + const dt = new DevtoolsPage(page) + await dt.goto('/?automation-hidden') + // Devtools set the theme on when they mount, also when hidden, so + // the check below runs after the mount and not before it. + await expect(page.locator('html')).toHaveAttribute( + 'data-tanstack-devtools-theme', + 'dark', + ) + await expect(page.getByTestId('tanstack_devtools')).toHaveCount(0) + await expect(dt.trigger()).toHaveCount(0) + }) + + test('not set → trigger visible', async ({ page }) => { + const dt = new DevtoolsPage(page) + await dt.goto('/') + await expect(dt.trigger()).toBeVisible() + }) +}) diff --git a/packages/devtools/src/context/devtools-store.ts b/packages/devtools/src/context/devtools-store.ts index 3aefe9966..abe16070a 100644 --- a/packages/devtools/src/context/devtools-store.ts +++ b/packages/devtools/src/context/devtools-store.ts @@ -110,6 +110,13 @@ export type DevtoolsStore = { * @default "tanstack-devtools" */ urlFlag: string + /** + * Whether to not render the dev tools in a browser driven by automation + * (Playwright, Cypress, Selenium), detected by `navigator.webdriver`. + * Keeps the dev tools out of end-to-end test selectors. + * @default false + */ + hideInAutomation: boolean /** * The theme of the dev tools * @default "dark" @@ -196,6 +203,7 @@ export const initialState: DevtoolsStore = { inspectHotkey: ['Shift', 'Alt', 'CtrlOrMeta'], requireUrlFlag: false, urlFlag: 'tanstack-devtools', + hideInAutomation: false, theme: typeof window !== 'undefined' && typeof window.matchMedia !== 'undefined' && diff --git a/packages/devtools/src/devtools.tsx b/packages/devtools/src/devtools.tsx index 54f01c6f7..21b215fed 100644 --- a/packages/devtools/src/devtools.tsx +++ b/packages/devtools/src/devtools.tsx @@ -182,48 +182,51 @@ export default function DevTools() { return ( - -
- - - + +
+ - (panelRef = ref)} - handleDragStart={(event) => handleDragStart(panelRef, event)} - handleHeightChange={updateHeight} + + - - {/* Stays mounted while folded so it can slide shut. */} - - - - - - - - -
-
+ (panelRef = ref)} + handleDragStart={(event) => handleDragStart(panelRef, event)} + handleHeightChange={updateHeight} + > + + {/* Stays mounted while folded so it can slide shut. */} + + + + + +
+
+ +
+
+
) } diff --git a/packages/devtools/tests/workbench.test.tsx b/packages/devtools/tests/workbench.test.tsx index 915cf1b79..a92eaa399 100644 --- a/packages/devtools/tests/workbench.test.tsx +++ b/packages/devtools/tests/workbench.test.tsx @@ -203,6 +203,36 @@ afterEach(() => { }) describe('workbench', { timeout: 30_000 }, () => { + describe('hideInAutomation', () => { + const devtoolsRoot = () => + document.querySelector('[data-testid="tanstack_devtools"]') + + beforeEach(() => { + // Playwright, Cypress, and Selenium set navigator.webdriver to true. + Object.defineProperty(navigator, 'webdriver', { + configurable: true, + value: true, + }) + }) + + afterEach(() => { + delete (navigator as { webdriver?: boolean }).webdriver + }) + + it('does not render in an automated browser when hideInAutomation is set', () => { + mountWorkbench([plugin('one')], { hideInAutomation: true }) + + expect(devtoolsRoot()).toBeNull() + expect(events).toEqual([]) + }) + + it('still renders in an automated browser by default', () => { + mountWorkbench([plugin('one')]) + + expect(devtoolsRoot()?.childElementCount).toBeGreaterThan(0) + }) + }) + it('keeps the secondary Plugins bar fixed-height without hover timers', () => { vi.useFakeTimers() expect({ From e36e7314cf29ea0ca7ad498c0b7da2567e0f1fc0 Mon Sep 17 00:00:00 2001 From: Alem Tuzlak Date: Fri, 2 Oct 2026 17:39:20 +0200 Subject: [PATCH 2/2] fix(devtools): ignore the open hotkey while hidden in automation With hideInAutomation the devtools render nothing, but the open hotkey still called toggleOpen(). That saved persistOpen and emitted trigger-toggled for devtools that do not exist. The hotkey now uses the same check as the render. --- packages/devtools/src/devtools.tsx | 9 ++++++--- packages/devtools/tests/workbench.test.tsx | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/packages/devtools/src/devtools.tsx b/packages/devtools/src/devtools.tsx index 21b215fed..1d0da2f11 100644 --- a/packages/devtools/src/devtools.tsx +++ b/packages/devtools/src/devtools.tsx @@ -77,6 +77,9 @@ export default function DevTools() { const { isCollapsed } = createCollapsed() const [showMarketplace, setShowMarketplace] = createSignal(false) const themeOwner = Symbol('tanstack-devtools-theme') + // Playwright, Cypress, and Selenium set navigator.webdriver. + const hiddenInAutomation = () => + settings().hideInAutomation && navigator.webdriver const updateHeight = (nextHeight: number) => { setHeight(nextHeight) @@ -168,7 +171,8 @@ export default function DevTools() { } for (const permutation of getHotkeyPermutations(settings().openHotkey)) { createShortcut(permutation, () => { - if (!isEditableTarget(document.activeElement)) toggleOpen() + if (!hiddenInAutomation() && !isEditableTarget(document.activeElement)) + toggleOpen() }) } }) @@ -182,8 +186,7 @@ export default function DevTools() { return ( - {/* Playwright, Cypress, and Selenium set navigator.webdriver. */} - +
{ expect(events).toEqual([]) }) + it('ignores the open hotkey while hidden in an automated browser', () => { + mountWorkbench([plugin('one')], { + hideInAutomation: true, + defaultOpen: false, + }) + + document.dispatchEvent( + new KeyboardEvent('keydown', { + key: '~', + ctrlKey: true, + bubbles: true, + }), + ) + + const stored = JSON.parse( + localStorage.getItem(TANSTACK_DEVTOOLS_STATE) ?? '{}', + ) + expect(stored.persistOpen).not.toBe(true) + }) + it('still renders in an automated browser by default', () => { mountWorkbench([plugin('one')])