Waypoint drag: - Use observeDeep instead of observe to detect Y.Map property changes Host election: - Extracted to host-election.ts with 8 unit tests - Simple rule: lowest real client ID is always host, deterministic - Always sets role (host or participant), no missing roles - Filters server docs (empty awareness state) from election BRouter routing: - Route segment-by-segment like bikerouter.de (one request per waypoint pair), guarantees route passes through every waypoint - Merge GeoJSON segments with accumulated stats - 8 unit tests for segment merging and waypoint splitting Awareness cleanup: - Track client IDs per WebSocket connection - Immediately remove awareness on WebSocket close Debug panel: - Visibility + expanded state persisted in localStorage - Hotkey (Shift+Cmd+Option+T) toggles visibility - Labels server docs as (server) - All logic self-contained in YjsDebugPanel.tsx User identity: - Color and name persisted in localStorage across reloads Tests: 32 total (16 existing + 8 host election + 8 routing) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.2 KiB
TypeScript
94 lines
3.2 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { electHost } from "./host-election";
|
|
|
|
function makeStates(
|
|
entries: Array<{ id: number; user?: boolean; role?: string }>,
|
|
): Map<number, Record<string, unknown>> {
|
|
const map = new Map<number, Record<string, unknown>>();
|
|
for (const e of entries) {
|
|
const state: Record<string, unknown> = {};
|
|
if (e.user) state.user = { color: "#000", name: "Test" };
|
|
if (e.role) state.role = e.role;
|
|
map.set(e.id, state);
|
|
}
|
|
return map;
|
|
}
|
|
|
|
describe("electHost", () => {
|
|
it("elects the only real client as host", () => {
|
|
const states = makeStates([
|
|
{ id: 100, user: true },
|
|
]);
|
|
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
|
|
});
|
|
|
|
it("elects lowest ID among two real clients", () => {
|
|
const states = makeStates([
|
|
{ id: 200, user: true },
|
|
{ id: 100, user: true },
|
|
]);
|
|
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
|
|
expect(electHost(states, 200)).toEqual({ isHost: false, role: "participant" });
|
|
});
|
|
|
|
it("ignores server docs (no user state)", () => {
|
|
const states = makeStates([
|
|
{ id: 50 }, // server doc — lowest ID but no user
|
|
{ id: 200, user: true },
|
|
{ id: 100, user: true },
|
|
]);
|
|
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
|
|
expect(electHost(states, 200)).toEqual({ isHost: false, role: "participant" });
|
|
});
|
|
|
|
it("always assigns a role (never undefined)", () => {
|
|
const states = makeStates([
|
|
{ id: 300, user: true },
|
|
{ id: 100, user: true },
|
|
{ id: 200, user: true },
|
|
]);
|
|
const result = electHost(states, 200);
|
|
expect(result.role).toBeDefined();
|
|
expect(result.role).toBe("participant");
|
|
});
|
|
|
|
it("handles single client with server doc", () => {
|
|
const states = makeStates([
|
|
{ id: 999 }, // server
|
|
{ id: 500, user: true },
|
|
]);
|
|
expect(electHost(states, 500)).toEqual({ isHost: true, role: "host" });
|
|
});
|
|
|
|
it("handles no real clients (defensive)", () => {
|
|
const states = makeStates([
|
|
{ id: 10 }, // server only
|
|
]);
|
|
// localId not in states — still returns host as fallback
|
|
expect(electHost(states, 42)).toEqual({ isHost: true, role: "host" });
|
|
});
|
|
|
|
it("ignores existing role claims — always uses lowest ID", () => {
|
|
const states = makeStates([
|
|
{ id: 200, user: true, role: "host" },
|
|
{ id: 100, user: true, role: "participant" },
|
|
]);
|
|
// ID 100 should be host regardless of current role claims
|
|
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
|
|
expect(electHost(states, 200)).toEqual({ isHost: false, role: "participant" });
|
|
});
|
|
|
|
it("works with many clients", () => {
|
|
const states = makeStates([
|
|
{ id: 999, user: true },
|
|
{ id: 500, user: true },
|
|
{ id: 42 }, // server
|
|
{ id: 200, user: true },
|
|
{ id: 100, user: true },
|
|
{ id: 800, user: true },
|
|
]);
|
|
expect(electHost(states, 100)).toEqual({ isHost: true, role: "host" });
|
|
expect(electHost(states, 500)).toEqual({ isHost: false, role: "participant" });
|
|
expect(electHost(states, 999)).toEqual({ isHost: false, role: "participant" });
|
|
});
|
|
});
|