Skip to content

Commit 00d187e

Browse files
committed
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
1 parent afa01fe commit 00d187e

7 files changed

Lines changed: 119 additions & 40 deletions

File tree

‎.changeset/hide-in-automation.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@tanstack/devtools': minor
3+
---
4+
5+
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`.

‎docs/configuration.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ type KeyboardKey = ModifierKey | (string & {});
7171
{ requireUrlFlag: boolean }
7272
```
7373

74+
- `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`.
75+
76+
```ts
77+
{ hideInAutomation: boolean }
78+
```
79+
7480
- `triggerImage` - The image used for the dev tools trigger
7581

7682
```ts

‎e2e/apps/react-vite/src/main.tsx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ function App() {
1616
config={{
1717
theme: 'dark',
1818
requireUrlFlag: new URLSearchParams(location.search).has('gated'),
19+
hideInAutomation: new URLSearchParams(location.search).has(
20+
'automation-hidden',
21+
),
1922
}}
2023
plugins={[
2124
{
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { test, expect } from '@playwright/test'
2+
import { DevtoolsPage } from '@tanstack/devtools-e2e'
3+
4+
// Playwright sets navigator.webdriver to true.
5+
test.describe('hideInAutomation', () => {
6+
test('set → devtools not rendered', async ({ page }) => {
7+
const dt = new DevtoolsPage(page)
8+
await dt.goto('/?automation-hidden')
9+
// Devtools set the theme on <html> when they mount, also when hidden, so
10+
// the check below runs after the mount and not before it.
11+
await expect(page.locator('html')).toHaveAttribute(
12+
'data-tanstack-devtools-theme',
13+
'dark',
14+
)
15+
await expect(page.getByTestId('tanstack_devtools')).toHaveCount(0)
16+
await expect(dt.trigger()).toHaveCount(0)
17+
})
18+
19+
test('not set → trigger visible', async ({ page }) => {
20+
const dt = new DevtoolsPage(page)
21+
await dt.goto('/')
22+
await expect(dt.trigger()).toBeVisible()
23+
})
24+
})

‎packages/devtools/src/context/devtools-store.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,13 @@ export type DevtoolsStore = {
110110
* @default "tanstack-devtools"
111111
*/
112112
urlFlag: string
113+
/**
114+
* Whether to not render the dev tools in a browser driven by automation
115+
* (Playwright, Cypress, Selenium), detected by `navigator.webdriver`.
116+
* Keeps the dev tools out of end-to-end test selectors.
117+
* @default false
118+
*/
119+
hideInAutomation: boolean
113120
/**
114121
* The theme of the dev tools
115122
* @default "dark"
@@ -196,6 +203,7 @@ export const initialState: DevtoolsStore = {
196203
inspectHotkey: ['Shift', 'Alt', 'CtrlOrMeta'],
197204
requireUrlFlag: false,
198205
urlFlag: 'tanstack-devtools',
206+
hideInAutomation: false,
199207
theme:
200208
typeof window !== 'undefined' &&
201209
typeof window.matchMedia !== 'undefined' &&

‎packages/devtools/src/devtools.tsx‎

Lines changed: 43 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -182,48 +182,51 @@ export default function DevTools() {
182182

183183
return (
184184
<ThemeContextProvider theme={theme()}>
185-
<Portal mount={(pip().pipWindow ?? window).document.body}>
186-
<div ref={setRootEl} data-testid={TANSTACK_DEVTOOLS}>
187-
<Show
188-
when={
189-
pip().pipWindow !== null
190-
? true
191-
: settings().requireUrlFlag
192-
? window.location.search.includes(settings().urlFlag)
193-
: true
194-
}
195-
>
196-
<Trigger isOpen={isOpen} setIsOpen={toggleOpen} />
197-
<MainPanel
198-
isResizing={isResizing}
199-
isOpen={isOpen}
200-
isCollapsed={isCollapsed}
201-
hasSubheader={hasSubheader}
185+
{/* Playwright, Cypress, and Selenium set navigator.webdriver. */}
186+
<Show when={!(settings().hideInAutomation && navigator.webdriver)}>
187+
<Portal mount={(pip().pipWindow ?? window).document.body}>
188+
<div ref={setRootEl} data-testid={TANSTACK_DEVTOOLS}>
189+
<Show
190+
when={
191+
pip().pipWindow !== null
192+
? true
193+
: settings().requireUrlFlag
194+
? window.location.search.includes(settings().urlFlag)
195+
: true
196+
}
202197
>
203-
<ContentPanel
204-
ref={(ref) => (panelRef = ref)}
205-
handleDragStart={(event) => handleDragStart(panelRef, event)}
206-
handleHeightChange={updateHeight}
198+
<Trigger isOpen={isOpen} setIsOpen={toggleOpen} />
199+
<MainPanel
200+
isResizing={isResizing}
201+
isOpen={isOpen}
202+
isCollapsed={isCollapsed}
203+
hasSubheader={hasSubheader}
207204
>
208-
<WorkbenchHeader
209-
showMarketplace={showMarketplace}
210-
setShowMarketplace={setShowMarketplace}
211-
toggleOpen={toggleOpen}
212-
/>
213-
{/* Stays mounted while folded so it can slide shut. */}
214-
<Show when={showsPluginsStrip()}>
215-
<PluginsStrip isOpen={isOpen} />
216-
</Show>
217-
<TabContent
218-
isOpen={isOpen()}
219-
showMarketplace={showMarketplace()}
220-
/>
221-
</ContentPanel>
222-
</MainPanel>
223-
</Show>
224-
<SourceInspector />
225-
</div>
226-
</Portal>
205+
<ContentPanel
206+
ref={(ref) => (panelRef = ref)}
207+
handleDragStart={(event) => handleDragStart(panelRef, event)}
208+
handleHeightChange={updateHeight}
209+
>
210+
<WorkbenchHeader
211+
showMarketplace={showMarketplace}
212+
setShowMarketplace={setShowMarketplace}
213+
toggleOpen={toggleOpen}
214+
/>
215+
{/* Stays mounted while folded so it can slide shut. */}
216+
<Show when={showsPluginsStrip()}>
217+
<PluginsStrip isOpen={isOpen} />
218+
</Show>
219+
<TabContent
220+
isOpen={isOpen()}
221+
showMarketplace={showMarketplace()}
222+
/>
223+
</ContentPanel>
224+
</MainPanel>
225+
</Show>
226+
<SourceInspector />
227+
</div>
228+
</Portal>
229+
</Show>
227230
</ThemeContextProvider>
228231
)
229232
}

‎packages/devtools/tests/workbench.test.tsx‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,36 @@ afterEach(() => {
203203
})
204204

205205
describe('workbench', { timeout: 30_000 }, () => {
206+
describe('hideInAutomation', () => {
207+
const devtoolsRoot = () =>
208+
document.querySelector('[data-testid="tanstack_devtools"]')
209+
210+
beforeEach(() => {
211+
// Playwright, Cypress, and Selenium set navigator.webdriver to true.
212+
Object.defineProperty(navigator, 'webdriver', {
213+
configurable: true,
214+
value: true,
215+
})
216+
})
217+
218+
afterEach(() => {
219+
delete (navigator as { webdriver?: boolean }).webdriver
220+
})
221+
222+
it('does not render in an automated browser when hideInAutomation is set', () => {
223+
mountWorkbench([plugin('one')], { hideInAutomation: true })
224+
225+
expect(devtoolsRoot()).toBeNull()
226+
expect(events).toEqual([])
227+
})
228+
229+
it('still renders in an automated browser by default', () => {
230+
mountWorkbench([plugin('one')])
231+
232+
expect(devtoolsRoot()?.childElementCount).toBeGreaterThan(0)
233+
})
234+
})
235+
206236
it('keeps the secondary Plugins bar fixed-height without hover timers', () => {
207237
vi.useFakeTimers()
208238
expect({

0 commit comments

Comments
 (0)