deadhead-space-freight-tycoon/apps/web/test/presence.test.ts

143 lines
4.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { afterEach, describe, expect, it } from "vitest";
import {
browserPresence,
presenceAtStart,
presenceChange,
startPresence,
usePresence,
type PresenceHost,
type PresenceMemory,
} from "../src/platform/presence";
const T = 1_000_000;
/** Device memory for the time the player went away. */
function fakeMemory(value: number | null = null): PresenceMemory & { value: number | null } {
return {
value,
read() {
return this.value;
},
write(awaySince) {
this.value = awaySince;
},
};
}
/** 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 memory = fakeMemory();
const stop = startPresence(fake.host, () => now, T - 60_000, memory);
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 });
// Remembered on the device, in case the game is closed while hidden.
expect(memory.value).toBe(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,
});
expect(memory.value).toBeNull();
stop();
expect(fake.listening).toBe(false);
});
it("comes back from when the player went away, not the later save, after closing a hidden tab", () => {
// Hidden at T − 2 h; the game kept running (and saving) until T − 1 h, then was closed.
const memory = fakeMemory(T - 2 * 3_600_000);
startPresence(fakeHost().host, () => T, T - 3_600_000, memory);
expect(usePresence.getState().lastReturn).toEqual({ from: T - 2 * 3_600_000, to: T });
expect(memory.value).toBeNull();
});
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]);
});
});