Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ concurrency:

env:
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
# Same cap as pr.yml. nx.json sets parallel: 5 for local machines.
# GitHub-hosted runners have far fewer cores/RAM, and each test:lib
# (vitest) / test:types (tsc) task spawns its own worker pool.
# Release runs `nx run-many` across every package, so unbounded
# parallelism oversubscribes the runner and flakes test:lib
# (5s timeouts, vitest worker RPC stalls).
NX_PARALLEL: 3

permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion examples/react/bundling-repro/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"jsdom": "^27.0.0",
"typescript": "~5.9.2",
"vite": "^8.0.0",
"vitest": "^3.2.4",
"vitest": "^4.1.10",
"web-vitals": "^5.1.0",
"wrangler": "^4.40.3"
}
Expand Down
2 changes: 1 addition & 1 deletion examples/react/start/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"jsdom": "^27.0.0",
"typescript": "~5.9.2",
"vite": "^8.0.0",
"vitest": "^3.2.4",
"vitest": "^4.1.10",
"web-vitals": "^5.1.0"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@
"tinyglobby": "^0.2.15",
"typescript": "~5.9.2",
"vite": "^8.0.0",
"vitest": "^3.2.4"
"vitest": "^4.1.10"
},
"overrides": {
"@tanstack/devtools": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion packages/angular-devtools/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@
"@angular/core": "^21.2.0",
"ng-packagr": "^21.2.0",
"tslib": "^2.3.0",
"vitest": "^3.2.4"
"vitest": "^4.1.10"
},
"peerDependencies": {
"@angular/core": ">=21.0.0"
Expand Down
8 changes: 8 additions & 0 deletions packages/devtools-a11y/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const config = defineConfig({
plugins: [
solid({
ssr: process.env.VITEST !== 'true',
// Vitest 4's module runner treats `/@solid-refresh` as `file:///@solid-refresh`
// and throws. HMR is not used in tests.
hot: process.env.VITEST !== 'true',
}) as any satisfies Plugin,
],
test: {
Expand All @@ -18,6 +21,11 @@ const config = defineConfig({
environment: 'jsdom',
setupFiles: ['./tests/test-setup.ts'],
globals: true,
// Solid component mounts in tests/theme.test.ts share the same CI-load
// profile as @tanstack/devtools-ui. Raise the default 5s timeout so a
// loaded GitHub runner does not flake test:lib.
testTimeout: 30_000,
hookTimeout: 30_000,
alias: {
'@tanstack/devtools-utils/react': fileURLToPath(
new URL('../devtools-utils/src/react/index.ts', import.meta.url),
Expand Down
15 changes: 14 additions & 1 deletion packages/devtools-ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ import type { Plugin } from 'vite'

const config = defineConfig({
base: './',
plugins: [solid() as any satisfies Plugin],
plugins: [
solid({
// Vitest 4's module runner treats `/@solid-refresh` as `file:///@solid-refresh`
// and throws. HMR is not used in tests.
hot: process.env.VITEST !== 'true',
}) as any satisfies Plugin,
],
build: {
rollupOptions: {
output: {
Expand All @@ -23,6 +29,13 @@ const config = defineConfig({
environment: 'jsdom',
setupFiles: ['./tests/test-setup.ts'],
globals: true,
// Component tests render the full Solid + goober tree (see tests/*.ts(x)).
// These take 1-5s locally but run on shared CI runners that are ~4x slower,
// pushing individual tests past vitest's default 5s timeout and causing
// flaky `test:lib` failures on Release. Give them headroom; fast tests
// are unaffected.
testTimeout: 30_000,
hookTimeout: 30_000,
},
})

Expand Down
8 changes: 7 additions & 1 deletion packages/devtools-utils/vite.config.solid-class.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import solid from 'vite-plugin-solid'
import packageJson from './package.json'

const config = defineConfig({
plugins: [solid()],
plugins: [
solid({
// Vitest 4's module runner treats `/@solid-refresh` as `file:///@solid-refresh`
// and throws. HMR is not used in tests.
hot: process.env.VITEST !== 'true',
}),
],
test: {
name: packageJson.name,
dir: './',
Expand Down
3 changes: 3 additions & 0 deletions packages/devtools-utils/vite.config.solid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const config = defineConfig({
plugins: [
solid({
ssr: true,
// Vitest 4's module runner treats `/@solid-refresh` as `file:///@solid-refresh`
// and throws. HMR is not used in tests.
hot: process.env.VITEST !== 'true',
}) as any,
],
test: {
Expand Down
12 changes: 8 additions & 4 deletions packages/devtools-vite/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,14 +332,18 @@ describe('devtools plugin', () => {

describe('configureServer - onConsolePipe uses pre-wrap console methods', () => {
let originalLog: typeof console.log
let beforeWrapSpy: ReturnType<typeof vi.fn>
let afterWrapSpy: ReturnType<typeof vi.fn>
// Vitest 4 types a bare `vi.fn()` as a constructor too. Give the spy
// the console.log signature so it can replace the live method.
const createLogSpy = () =>
vi.fn((..._data: Parameters<typeof console.log>) => {})
let beforeWrapSpy: ReturnType<typeof createLogSpy>
let afterWrapSpy: ReturnType<typeof createLogSpy>

beforeEach(async () => {
capturedOnConsolePipe = undefined
originalLog = console.log
beforeWrapSpy = vi.fn()
afterWrapSpy = vi.fn()
beforeWrapSpy = createLogSpy()
afterWrapSpy = createLogSpy()

console.log = beforeWrapSpy

Expand Down
16 changes: 8 additions & 8 deletions packages/devtools/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,9 @@ import DevTools from '../src/devtools'
import { TanStackDevtoolsCore } from '../src/core'
import { DevtoolsProvider } from '../src/context/devtools-context'
import { PiPProvider } from '../src/context/pip-context'
import { mountDevtools } from '../src/mount-impl'
import * as mountImpl from '../src/mount-impl'
import type { TanStackDevtoolsConfig } from '../src/context/devtools-context'

vi.mock('../src/mount-impl', () => ({ mountDevtools: vi.fn() }))
const mockedMount = vi.mocked(mountDevtools)

const shellDisposers = new Map<HTMLElement, () => void>()
const mountShell = (config: Partial<TanStackDevtoolsConfig>) => {
const host = document.body.appendChild(document.createElement('div'))
Expand Down Expand Up @@ -41,7 +38,6 @@ const disposeShell = (host: HTMLElement) => {
beforeEach(() => {
localStorage.clear()
history.replaceState({}, '', '/')
mockedMount.mockReset()
vi.stubGlobal(
'ResizeObserver',
class ResizeObserver {
Expand Down Expand Up @@ -134,7 +130,10 @@ describe('devtools core boundaries', () => {
})

it('aborts a pending dynamic mount and resets after repeated mount errors', async () => {
mockedMount.mockReturnValue({
// Spy on the live ESM export. Vitest 4's module runner does not apply
// `vi.mock` to `import('./mount-impl')` inside core.ts, so a file-level
// mock never sees those calls. A spy on the same module namespace does.
const mockedMount = vi.spyOn(mountImpl, 'mountDevtools').mockReturnValue({
dispose: vi.fn(),
eventBus: { stop: vi.fn() },
})
Expand All @@ -152,9 +151,10 @@ describe('devtools core boundaries', () => {
})
const failing = new TanStackDevtoolsCore({})
failing.mount(host)
await vi.waitFor(() => expect(error).toHaveBeenCalledTimes(1))
await vi.waitFor(() => expect(mockedMount).toHaveBeenCalledTimes(1))
failing.mount(host)
await vi.waitFor(() => expect(error).toHaveBeenCalledTimes(2))
await vi.waitFor(() => expect(mockedMount).toHaveBeenCalledTimes(2))
expect(error).toHaveBeenCalledTimes(2)
expect(() => failing.unmount()).toThrow('Devtools is not mounted')
})
})
8 changes: 7 additions & 1 deletion packages/devtools/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import packageJson from './package.json'
import type { Plugin } from 'vite'

const config = defineConfig({
plugins: [solid() as any satisfies Plugin],
plugins: [
solid({
// Vitest 4's module runner treats `/@solid-refresh` as `file:///@solid-refresh`
// and throws. HMR is not used in tests.
hot: process.env.VITEST !== 'true',
}) as any satisfies Plugin,
],
test: {
name: packageJson.name,
dir: './',
Expand Down
48 changes: 26 additions & 22 deletions packages/event-bus-client/tests/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,19 @@ describe('EventClient', () => {
let bus: ClientEventBus

beforeEach(() => {
// Create a fresh bus for each test to ensure isolation
// EventClient starts reconnect intervals and has no public stop.
// Fake timers so each test owns those intervals and leftover
// `tanstack-connect` events do not leak into the next test.
vi.useFakeTimers()
bus = new ClientEventBus()
bus.start()
})

afterEach(() => {
// Clean up after each test
bus.stop()
vi.clearAllTimers()
vi.useRealTimers()
vi.restoreAllMocks()
Comment on lines +12 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

file='packages/event-bus-client/tests/index.test.ts'

rg -n -C 3 \
  'new EventClient|\.on(All|AllPluginEvents)?\(|cleanup\(\)|afterEach|bus\.stop|clearAllTimers' \
  "$file"

Repository: TanStack/devtools

Length of output: 9723


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- test structure and teardown ---'
cat -n packages/event-bus-client/tests/index.test.ts | sed -n '1,540p'

printf '%s\n' '--- EventClient subscription implementation ---'
fd -i 'plugin.ts' packages/event-bus-client
rg -n -C 6 'class EventClient|onAllPluginEvents|onAll\(|on\(' packages/event-bus-client/src packages/event-bus-client/tests

Repository: TanStack/devtools

Length of output: 43631


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

printf '%s\n' '--- EventClient listener registration and disposal ---'
cat -n packages/event-bus-client/src/plugin.ts | sed -n '40,315p'

printf '%s\n' '--- ClientEventBus event lifecycle ---'
rg -n -C 8 'tanstack-connect|tanstack-connect-success|tanstack-devtools-global|tanstack-dispatch-event|addEventListener|removeEventListener' packages/event-bus-client packages/event-bus/src packages --glob '*.ts' --glob '*.tsx' | sed -n '1,260p'

printf '%s\n' '--- Subscription calls and disposer usage by test line ---'
python3 - <<'PY'
import re
from pathlib import Path

path = Path('packages/event-bus-client/tests/index.test.ts')
lines = path.read_text().splitlines()

for i, line in enumerate(lines, 1):
    if re.search(r'\.on(?:AllPluginEvents|All)?\s*\(', line):
        print(f'{i}: {line.strip()}')
        window = '\n'.join(lines[i-1:min(i+8, len(lines))])
        if re.search(r'\b(?:cleanup|off|dispose)\s*\(\)', window):
            print('  disposer call found nearby')
        else:
            print('  no disposer call found nearby')
PY

Repository: TanStack/devtools

Length of output: 31325


Clean up all EventClient subscriptions between tests.

Several calls to client.on(...), onAll(...), and onAllPluginEvents(...) discard their disposer. These listeners remain on window after afterEach, so later tests can invoke earlier handlers. Store and call each disposer before timer cleanup.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/event-bus-client/tests/index.test.ts` around lines 12 - 24, Update
the test setup around bus.start and afterEach to track every disposer returned
by client.on, onAll, and onAllPluginEvents, then invoke all stored disposers
before clearing timers. Ensure the disposer collection is reset per test so
EventClient subscriptions do not leak between tests.

})

describe('debug config', () => {
Expand Down Expand Up @@ -209,8 +214,7 @@ describe('EventClient', () => {
describe('queued events', () => {
it('emits queued events when connected to the event bus', async () => {
bus.stop()
// Wait for bus to fully stop
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

const client = new EventClient({
debug: false,
Expand All @@ -222,13 +226,13 @@ describe('EventClient', () => {

// Start bus first, then emit (to ensure bus is ready)
bus.start()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

// Now emit - this will queue and trigger connection
client.emit('event', { foo: 'bar' })

// wait for connection to establish and queued events to be emitted
await new Promise((resolve) => setTimeout(resolve, 300))
await vi.advanceTimersByTimeAsync(300)
expect(eventHandler).toHaveBeenCalledWith({
type: 'test-queued:event',
payload: { foo: 'bar' },
Expand All @@ -241,7 +245,7 @@ describe('EventClient', () => {
// The ClientEventBus dispatches to global window events which persist across tests
// This needs a more robust cleanup mechanism
bus.stop()
await new Promise((resolve) => setTimeout(resolve, 100))
await vi.advanceTimersByTimeAsync(100)

const client = new EventClient({
debug: false,
Expand All @@ -253,15 +257,15 @@ describe('EventClient', () => {

// Start bus FIRST, wait for it to be ready
bus.start()
await new Promise((resolve) => setTimeout(resolve, 100))
await vi.advanceTimersByTimeAsync(100)

// NOW emit multiple events (they'll queue and then connect)
client.emit('event', { count: 1 })
client.emit('event', { count: 2 })
client.emit('event', { count: 3 })

// Wait for connection and all queued events to be emitted
await new Promise((resolve) => setTimeout(resolve, 300))
await vi.advanceTimersByTimeAsync(300)

// All 3 events should have been received
expect(eventHandler).nthCalledWith(1, {
Expand Down Expand Up @@ -308,7 +312,7 @@ describe('EventClient', () => {
describe('connecting behavior', () => {
it('should only attempt connection once when #connecting flag is set', async () => {
bus.stop()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

const client = new EventClient({
debug: false,
Expand Down Expand Up @@ -338,7 +342,7 @@ describe('EventClient', () => {

it('should stop connect loop after successful connection', async () => {
bus.stop()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

const client = new EventClient({
debug: false,
Expand All @@ -351,15 +355,15 @@ describe('EventClient', () => {

// Start bus to allow connection
bus.start()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

// Wait for connection to establish
await new Promise((resolve) => setTimeout(resolve, 200))
await vi.advanceTimersByTimeAsync(200)

const dispatchSpy = vi.spyOn(window, 'dispatchEvent')

// Wait for what would be several retry intervals
await new Promise((resolve) => setTimeout(resolve, 400))
await vi.advanceTimersByTimeAsync(400)

const connectCalls = dispatchSpy.mock.calls.filter(
(call) =>
Expand All @@ -375,7 +379,7 @@ describe('EventClient', () => {
it('should respect max retries limit', async () => {
// Don't start the bus so connection always fails
bus.stop()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

const client = new EventClient({
debug: false,
Expand All @@ -389,7 +393,7 @@ describe('EventClient', () => {
client.emit('event', { foo: 'bar' })

// Wait long enough for max retries (5 attempts at 50ms intervals = 250ms + buffer)
await new Promise((resolve) => setTimeout(resolve, 400))
await vi.advanceTimersByTimeAsync(400)

const connectCalls = dispatchSpy.mock.calls.filter(
(call) =>
Expand All @@ -401,7 +405,7 @@ describe('EventClient', () => {

// Wait longer to ensure no more attempts
dispatchSpy.mockClear()
await new Promise((resolve) => setTimeout(resolve, 200))
await vi.advanceTimersByTimeAsync(200)

const additionalCalls = dispatchSpy.mock.calls.filter(
(call) =>
Expand All @@ -416,7 +420,7 @@ describe('EventClient', () => {

it('should reset connecting flag when connection succeeds and allow new connections', async () => {
bus.stop()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

// Create first client and connect it
const client1 = new EventClient({
Expand All @@ -427,18 +431,18 @@ describe('EventClient', () => {

// Start bus before emitting so connection succeeds
bus.start()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

// First connection attempt
client1.emit('event1', { id: 1 })

// Wait for connection
await new Promise((resolve) => setTimeout(resolve, 150))
await vi.advanceTimersByTimeAsync(150)

// Now create a SECOND client (which will need to connect)
// Stop/start bus to simulate a scenario where new client needs to connect
bus.stop()
await new Promise((resolve) => setTimeout(resolve, 50))
await vi.advanceTimersByTimeAsync(50)

const dispatchSpy = vi.spyOn(window, 'dispatchEvent')

Expand All @@ -452,7 +456,7 @@ describe('EventClient', () => {
client2.emit('event2', { id: 2 })

// Wait a bit for the connection attempt
await new Promise((resolve) => setTimeout(resolve, 100))
await vi.advanceTimersByTimeAsync(100)

const connectCalls = dispatchSpy.mock.calls.filter(
(call) =>
Expand Down
Loading
Loading