import { afterEach, describe, expect, it } from "vitest"; import { browserPresence, presenceAtStart, presenceChange, startPresence, usePresence, type PresenceHost, } from "../src/platform/presence"; const T = 1_000_000; /** A host whose visibility the test flips. */ function fakeHost(here = true) { let listener: ((here: boolean) => void) | null = null; const host: PresenceHost = { isHere: () => here, onChange(fn) { listener = fn; return () => (listener = null); }, }; return { host, set(value: boolean) { here = value; listener?.(value); }, get listening() { return listener !== null; }, }; } let visibility: DocumentVisibilityState = "visible"; Object.defineProperty(document, "visibilityState", { configurable: true, get: () => visibility, }); afterEach(() => { visibility = "visible"; }); describe("presence (Step 28.2, decision 0.5.160)", () => { it("records when the player went away and came back", () => { let state = presenceAtStart(true, T, null); expect(state).toEqual({ here: true, hereSince: T, awaySince: null, lastReturn: null, returns: 0, }); state = presenceChange(state, false, T + 1_000); expect(state).toMatchObject({ here: false, awaySince: T + 1_000 }); // Hidden again (a second event) changes nothing. expect(presenceChange(state, false, T + 2_000)).toBe(state); state = presenceChange(state, true, T + 301_000); expect(state).toEqual({ here: true, hereSince: T + 301_000, awaySince: null, lastReturn: { from: T + 1_000, to: T + 301_000 }, returns: 1, }); }); it("counts a cold start as coming back from the save's last time seen", () => { expect(presenceAtStart(true, T, T - 3_600_000)).toEqual({ here: true, hereSince: T, awaySince: null, lastReturn: { from: T - 3_600_000, to: T }, returns: 1, }); // Opened in a background tab: away since then, until it's shown. const hidden = presenceAtStart(false, T, T - 3_600_000); expect(presenceChange(hidden, true, T + 5_000).lastReturn).toEqual({ from: T - 3_600_000, to: T + 5_000, }); }); it("follows the host into the store, with times from the game clock", () => { const fake = fakeHost(); let now = T; const stop = startPresence(fake.host, () => now, T - 60_000); expect(usePresence.getState().lastReturn).toEqual({ from: T - 60_000, to: T }); now = T + 10_000; fake.set(false); expect(usePresence.getState()).toMatchObject({ here: false, awaySince: T + 10_000 }); now = T + 40_000; fake.set(true); expect(usePresence.getState()).toMatchObject({ here: true, lastReturn: { from: T + 10_000, to: T + 40_000 }, returns: 2, }); stop(); expect(fake.listening).toBe(false); }); it("reads the browser tab's visibility", () => { const seen: boolean[] = []; const stop = browserPresence.onChange((here) => seen.push(here)); visibility = "hidden"; document.dispatchEvent(new Event("visibilitychange")); expect(browserPresence.isHere()).toBe(false); visibility = "visible"; document.dispatchEvent(new Event("visibilitychange")); stop(); document.dispatchEvent(new Event("visibilitychange")); expect(seen).toEqual([false, true]); }); });