mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-25 08:03:45 +00:00
* fix: stop websockets resurrecting a reclaimed dev server * docs: condense the websocket invariant comment * test: stub fetch suite-wide so waking cannot hit a real dev server * fix: let websockets join an in-flight start
126 lines
4.0 KiB
TypeScript
126 lines
4.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
// Freshly imported per case: install is idempotent by design, so a shared module instance
|
|
// would make the second case a no-op against a restored `window.setInterval`.
|
|
async function install() {
|
|
vi.resetModules()
|
|
const module = await import('./devPollingDormancy')
|
|
module.installDevPollingDormancy()
|
|
}
|
|
|
|
// The patch is invisible in production and has no UI, so the freeze/re-arm pair is the
|
|
// only thing standing between a forgotten tab and a dev server that never gets reclaimed.
|
|
describe('devPollingDormancy', () => {
|
|
const original = { setInterval: window.setInterval, clearInterval: window.clearInterval }
|
|
let fetchSpy: ReturnType<typeof vi.fn>
|
|
|
|
// Stubbed for every case, not just the one that asserts on it: waking issues a real
|
|
// request otherwise, and jsdom's default origin is localhost:3000 — the frontend port
|
|
// of the first worktree, whose dev server the suite would then resurrect.
|
|
beforeEach(() => {
|
|
fetchSpy = vi.fn(() => Promise.resolve(new Response()))
|
|
vi.stubGlobal('fetch', fetchSpy)
|
|
})
|
|
|
|
afterEach(() => {
|
|
window.setInterval = original.setInterval
|
|
window.clearInterval = original.clearInterval
|
|
// The install guard lives on `window`, so it is part of the global state to restore.
|
|
delete (window as unknown as Record<string, boolean>).__wmDevPollingDormancyInstalled
|
|
vi.useRealTimers()
|
|
vi.unstubAllEnvs()
|
|
// restoreAllMocks does not undo stubGlobal, and the config sets no unstubGlobals.
|
|
vi.unstubAllGlobals()
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
function setHidden(hidden: boolean) {
|
|
Object.defineProperty(document, 'hidden', { value: hidden, configurable: true })
|
|
document.dispatchEvent(new Event('visibilitychange'))
|
|
}
|
|
|
|
it('freezes intervals once the tab goes inactive and re-arms them on return', async () => {
|
|
vi.useFakeTimers()
|
|
vi.stubEnv('VITE_DEV_DORMANT_MS', '1000')
|
|
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
|
|
setHidden(false)
|
|
|
|
await install()
|
|
|
|
const tick = vi.fn()
|
|
const handle = window.setInterval(tick, 100)
|
|
vi.advanceTimersByTime(300)
|
|
expect(tick).toHaveBeenCalledTimes(3)
|
|
|
|
setHidden(true)
|
|
vi.advanceTimersByTime(1000)
|
|
tick.mockClear()
|
|
vi.advanceTimersByTime(500)
|
|
expect(tick).not.toHaveBeenCalled()
|
|
|
|
setHidden(false)
|
|
vi.advanceTimersByTime(300)
|
|
expect(tick).toHaveBeenCalledTimes(3)
|
|
|
|
// Clearing through the patched handle must still stop the underlying timer.
|
|
window.clearInterval(handle)
|
|
tick.mockClear()
|
|
vi.advanceTimersByTime(300)
|
|
expect(tick).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('warms the dev server on wake, since websockets cannot restart it', async () => {
|
|
vi.useFakeTimers()
|
|
vi.stubEnv('VITE_DEV_DORMANT_MS', '1000')
|
|
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
|
|
setHidden(false)
|
|
|
|
await install()
|
|
|
|
setHidden(true)
|
|
vi.advanceTimersByTime(1000)
|
|
expect(fetchSpy).not.toHaveBeenCalled()
|
|
|
|
setHidden(false)
|
|
expect(fetchSpy).toHaveBeenCalledTimes(1)
|
|
// The origin is the load-bearing half: it has to reach the supervised port.
|
|
expect(fetchSpy.mock.calls[0][0]).toBe(`${location.origin}/`)
|
|
expect(fetchSpy.mock.calls[0][1]).toMatchObject({ method: 'HEAD' })
|
|
})
|
|
|
|
it('does not re-patch when the module itself is hot-replaced', async () => {
|
|
vi.useFakeTimers()
|
|
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
|
|
setHidden(false)
|
|
|
|
await install()
|
|
const patched = window.setInterval
|
|
|
|
// A fresh module instance is what HMR hands the layout on the next edit.
|
|
await install()
|
|
|
|
expect(window.setInterval).toBe(patched)
|
|
})
|
|
|
|
it('arms intervals registered while dormant only once the tab is back', async () => {
|
|
vi.useFakeTimers()
|
|
vi.stubEnv('VITE_DEV_DORMANT_MS', '1000')
|
|
vi.spyOn(document, 'hasFocus').mockReturnValue(true)
|
|
setHidden(false)
|
|
|
|
await install()
|
|
|
|
setHidden(true)
|
|
vi.advanceTimersByTime(1000)
|
|
|
|
const tick = vi.fn()
|
|
window.setInterval(tick, 100)
|
|
vi.advanceTimersByTime(500)
|
|
expect(tick).not.toHaveBeenCalled()
|
|
|
|
setHidden(false)
|
|
vi.advanceTimersByTime(200)
|
|
expect(tick).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|