diff --git a/src/reactivity/track.ts b/src/reactivity/track.ts index 8199ca7..af49749 100644 --- a/src/reactivity/track.ts +++ b/src/reactivity/track.ts @@ -39,6 +39,9 @@ const _isDev = isDev(); // raw ESM it is undefined, so we fall back to "dev". Only used to enrich the // multi-instance dev warning. declare const __SIBU_VERSION__: string | undefined; +// The `__SIBU_VERSION__` branch only runs when a bundler inlined the define; the +// source test runner always takes the "dev" fallback, so that side is excluded. +/* v8 ignore next */ const _runtimeVersion = typeof __SIBU_VERSION__ !== "undefined" ? __SIBU_VERSION__ : "dev"; interface ReactiveApi { diff --git a/tests/duplicate-instance-source.test.ts b/tests/duplicate-instance-source.test.ts new file mode 100644 index 0000000..db395b1 --- /dev/null +++ b/tests/duplicate-instance-source.test.ts @@ -0,0 +1,68 @@ +import { afterEach, beforeEach, describe, expect, test, vi } from "vitest"; + +// --------------------------------------------------------------------------- +// Covers the duplicate-instance DETECTION path on the real source modules. +// +// `tests/duplicate-instance.test.ts` proves cross-instance behavior by bundling +// the core and evaluating it twice — but that runs a bundled copy, so coverage +// instrumentation never credits the source module's "a duplicate loaded" branch +// (the in-process module evaluates exactly once, always taking the first-copy +// path). Here we exercise that branch directly: pre-seed the `globalThis` +// registry as if a first copy had already published its API, then freshly +// import the source module so its resolver takes the duplicate path. +// --------------------------------------------------------------------------- + +const REACTIVE_KEY = Symbol.for("sibujs.reactive.v1"); +const BATCH_KEY = Symbol.for("sibujs.reactive.batch.v1"); + +type Registry = Record; + +describe("duplicate-instance detection on the source modules", () => { + beforeEach(() => { + vi.resetModules(); + }); + + afterEach(() => { + delete (globalThis as Registry)[REACTIVE_KEY]; + delete (globalThis as Registry)[BATCH_KEY]; + vi.restoreAllMocks(); + }); + + test("track.ts delegates to the first copy and dev-warns once on a duplicate load", async () => { + // Simulate a first copy of the reactive runtime having published its API. + (globalThis as Registry)[REACTIVE_KEY] = { version: "0.0.0-first", __dupWarned: false }; + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await import("../src/reactivity/track"); + + expect(warn).toHaveBeenCalledTimes(1); + expect(String(warn.mock.calls[0][0])).toContain("Multiple instances of the reactive runtime"); + // The registry object is stamped so a third copy stays quiet. + expect((globalThis as Record)[REACTIVE_KEY].__dupWarned).toBe(true); + }); + + test("track.ts does not warn again when the first copy already warned", async () => { + (globalThis as Registry)[REACTIVE_KEY] = { version: "0.0.0-first", __dupWarned: true }; + const warn = vi.spyOn(console, "warn").mockImplementation(() => {}); + + await import("../src/reactivity/track"); + + expect(warn).not.toHaveBeenCalled(); + }); + + test("batch.ts re-exports the first copy's functions on a duplicate load", async () => { + const firstBatch = { + batch: (fn: () => T): T => fn(), + enqueueBatchedSignal: () => false, + isBatching: () => false, + }; + (globalThis as Registry)[BATCH_KEY] = firstBatch; + + const mod = await import("../src/reactivity/batch"); + + // The duplicate copy delegates to the first copy's functions verbatim. + expect(mod.batch).toBe(firstBatch.batch); + expect(mod.isBatching).toBe(firstBatch.isBatching); + expect(mod.enqueueBatchedSignal).toBe(firstBatch.enqueueBatchedSignal); + }); +});