27a63b3b2b
Each vite:beforeUpdate scheduled an unconditional setTimeout to reset the dev status from "update" back to "ok" after 2s, but never stored or cleared the handle. Rapid successive updates stacked independent timers; an earlier update's timer firing while a later update was still showing would reset the newer status line early (its guard only checked type === "update", which is still true for the newer update). Track the pending timer in a module-level variable, clear it at the top of vite:beforeUpdate before scheduling a new one (so only the latest update's timer is ever live), and clear it on vite:error (an error supersedes a pending update->ok reset). Also unref() the timer so it doesn't hold the event loop open; .unref is optional since the DOM number handle lacks it. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
99 lines
3.8 KiB
TypeScript
99 lines
3.8 KiB
TypeScript
// SEQUENTIAL: this test uses vi.useFakeTimers(), which mutates the process-global
|
|
// setTimeout/clearTimeout. File-level parallelism could perturb other tests'
|
|
// timer assertions, so it must run in a *.sequential.test.* file. It also
|
|
// re-imports hmr.ts via vi.resetModules() to reset the module-level
|
|
// idempotency guard + pending-timer state between cases (the module cache is
|
|
// process-global), matching unit/hmr-bridge-idempotent.sequential.test.ts.
|
|
import { afterEach, expect, test, vi } from "vite-plus/test";
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
vi.resetModules();
|
|
});
|
|
|
|
// `on`/`send` are typed with their real signatures so FakeHot structurally
|
|
// satisfies the HotContext param of initHmrBridge. `handlers` is test-only
|
|
// state for firing the registered callbacks back.
|
|
type FakeHot = {
|
|
on: ReturnType<typeof vi.fn<(event: string, cb: (payload: unknown) => void) => void>>;
|
|
send: ReturnType<typeof vi.fn<(event: string, data?: unknown) => void>>;
|
|
handlers: Map<string, (payload: unknown) => void>;
|
|
};
|
|
|
|
function makeFakeHot(): FakeHot {
|
|
const handlers = new Map<string, (payload: unknown) => void>();
|
|
const on = vi.fn((event: string, cb: (payload: unknown) => void) => {
|
|
handlers.set(event, cb);
|
|
});
|
|
const send = vi.fn<(event: string, data?: unknown) => void>();
|
|
return { on, send, handlers };
|
|
}
|
|
|
|
function updatePayload(paths: string[]): { updates: Array<{ path: string }> } {
|
|
return { updates: paths.map((path) => ({ path })) };
|
|
}
|
|
|
|
test("a stale update timer does not reset a newer update's status early", async () => {
|
|
vi.resetModules();
|
|
const { initHmrBridge, devState } = await import("../../runtime/src/hmr.ts");
|
|
const hot = makeFakeHot();
|
|
|
|
vi.useFakeTimers();
|
|
try {
|
|
initHmrBridge(hot);
|
|
const beforeUpdate = hot.handlers.get("vite:beforeUpdate");
|
|
expect(beforeUpdate).toBeTypeOf("function");
|
|
|
|
// t=0: update #1 schedules a reset for t=2000.
|
|
beforeUpdate!(updatePayload(["a"]));
|
|
expect(devState.value).toEqual({ type: "update", paths: ["a"] });
|
|
|
|
// t=1500: update #2 schedules a reset for t=3500. The bug: update #1's
|
|
// timer is still live and will fire at t=2000.
|
|
vi.advanceTimersByTime(1500);
|
|
beforeUpdate!(updatePayload(["b"]));
|
|
expect(devState.value).toEqual({ type: "update", paths: ["b"] });
|
|
|
|
// t=2000: update #1's stale timer fires. With the bug it sees type==="update"
|
|
// (now update #2) and resets to "ok", clearing update #2's status 1500ms early.
|
|
// After the fix update #1's timer was cleared, so the status must persist.
|
|
vi.advanceTimersByTime(500);
|
|
expect(devState.value).toEqual({ type: "update", paths: ["b"] });
|
|
|
|
// t=3500: update #2's own timer fires and resets to "ok".
|
|
vi.advanceTimersByTime(1500);
|
|
expect(devState.value).toEqual({ type: "ok" });
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|
|
|
|
test("vite:error clears a pending update→ok reset so the error status persists", async () => {
|
|
vi.resetModules();
|
|
const { initHmrBridge, devState } = await import("../../runtime/src/hmr.ts");
|
|
const hot = makeFakeHot();
|
|
|
|
vi.useFakeTimers();
|
|
try {
|
|
initHmrBridge(hot);
|
|
const beforeUpdate = hot.handlers.get("vite:beforeUpdate");
|
|
const onError = hot.handlers.get("vite:error");
|
|
expect(beforeUpdate).toBeTypeOf("function");
|
|
expect(onError).toBeTypeOf("function");
|
|
|
|
// An update schedules a reset for t=2000...
|
|
beforeUpdate!(updatePayload(["a"]));
|
|
expect(devState.value).toEqual({ type: "update", paths: ["a"] });
|
|
|
|
// ...but an error supersedes it. The pending reset must be cleared so it can't
|
|
// later overwrite the error status with "ok".
|
|
onError!({ err: { message: "boom" } });
|
|
expect(devState.value).toEqual({ type: "error", error: { message: "boom" } });
|
|
|
|
vi.advanceTimersByTime(2000);
|
|
expect(devState.value).toEqual({ type: "error", error: { message: "boom" } });
|
|
} finally {
|
|
vi.useRealTimers();
|
|
}
|
|
});
|