143 lines
4.3 KiB
TypeScript
143 lines
4.3 KiB
TypeScript
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]);
|
||
});
|
||
});
|