Transactional emails: - Add nodemailer SMTP email module with dev-mode console logging - Magic link template and welcome template with HTML + plain text - Wire sendMagicLink into login flow, sendWelcome into registration - Update privacy page and deploy docs for SMTP configuration Planner features: - No-go areas: draw polygons on map (leaflet-geoman), synced via Yjs, passed to BRouter as nogos parameter, route recomputes on change - Session notes: collaborative Y.Text textarea in sidebar tab - Crash recovery: periodic localStorage save of Yjs state, restore on reconnect - Rate limit session creation (10/IP/hour) in /new route Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
56 lines
1.8 KiB
TypeScript
56 lines
1.8 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import * as Y from "yjs";
|
|
|
|
/**
|
|
* Tests the crash recovery encode/decode logic used in use-yjs.ts.
|
|
* The actual hook wiring is tested via E2E; these verify the data round-trip.
|
|
*/
|
|
describe("crash recovery", () => {
|
|
it("round-trips Yjs state through base64 localStorage format", () => {
|
|
const doc = new Y.Doc();
|
|
const waypoints = doc.getArray("waypoints");
|
|
const wp = new Y.Map();
|
|
wp.set("lat", 52.52);
|
|
wp.set("lon", 13.405);
|
|
waypoints.push([wp]);
|
|
|
|
// Encode (same as use-yjs.ts save)
|
|
const state = Y.encodeStateAsUpdate(doc);
|
|
const b64 = btoa(String.fromCharCode(...state));
|
|
|
|
// Decode (same as use-yjs.ts restore)
|
|
const restored = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
|
const doc2 = new Y.Doc();
|
|
Y.applyUpdate(doc2, restored);
|
|
|
|
const restoredWaypoints = doc2.getArray("waypoints");
|
|
expect(restoredWaypoints.length).toBe(1);
|
|
const restoredWp = restoredWaypoints.get(0) as Y.Map<unknown>;
|
|
expect(restoredWp.get("lat")).toBe(52.52);
|
|
expect(restoredWp.get("lon")).toBe(13.405);
|
|
});
|
|
|
|
it("merges local and remote state without conflict", () => {
|
|
// Simulate: local has waypoint A, server has waypoint B
|
|
const local = new Y.Doc();
|
|
const localWps = local.getArray("waypoints");
|
|
const wpA = new Y.Map();
|
|
wpA.set("lat", 52.0);
|
|
wpA.set("lon", 13.0);
|
|
localWps.push([wpA]);
|
|
|
|
const remote = new Y.Doc();
|
|
const remoteWps = remote.getArray("waypoints");
|
|
const wpB = new Y.Map();
|
|
wpB.set("lat", 48.0);
|
|
wpB.set("lon", 11.0);
|
|
remoteWps.push([wpB]);
|
|
|
|
// Merge remote into local (simulates reconnect)
|
|
const remoteState = Y.encodeStateAsUpdate(remote);
|
|
Y.applyUpdate(local, remoteState);
|
|
|
|
// Both waypoints should exist
|
|
expect(localWps.length).toBe(2);
|
|
});
|
|
});
|