1import { describe, expect, it } from "vitest"; 2 3import { 4 BlockChangedFrameSchema, 5 ClientFrameSchema, 6 PresenceBroadcastSchema, 7 PresenceFrameSchema, 8 PresenceLeaveBroadcastSchema, 9 PresenceLeaveFrameSchema, 10 ServerFrameSchema, 11} from "./frames.js"; 12 13describe("BlockChangedFrameSchema (TASK-0092 (b) refetch-hint)", () => { 14 it("accepts a pageId (+ optional advisory version)", () => { 15 const r = BlockChangedFrameSchema.safeParse({ 16 type: "block:changed", 17 pageId: "pg_1", 18 version: 7, 19 }); 20 expect(r.success).toBe(true); 21 if (r.success) expect(r.data).toEqual({ type: "block:changed", pageId: "pg_1", version: 7 }); 22 }); 23 24 it("version is optional (a REST write may omit it)", () => { 25 const r = BlockChangedFrameSchema.safeParse({ type: "block:changed", pageId: "pg_1" }); 26 expect(r.success).toBe(true); 27 if (r.success) expect(r.data).toEqual({ type: "block:changed", pageId: "pg_1" }); 28 }); 29 30 it("rejects an empty pageId", () => { 31 expect(BlockChangedFrameSchema.safeParse({ type: "block:changed", pageId: "" }).success).toBe( 32 false, 33 ); 34 }); 35 36 it("rejects a non-integer / <1 version", () => { 37 expect( 38 BlockChangedFrameSchema.safeParse({ type: "block:changed", pageId: "pg_1", version: 0 }) 39 .success, 40 ).toBe(false); 41 expect( 42 BlockChangedFrameSchema.safeParse({ type: "block:changed", pageId: "pg_1", version: 1.5 }) 43 .success, 44 ).toBe(false); 45 }); 46 47 it("is a member of the ServerFrame discriminated union", () => { 48 const r = ServerFrameSchema.safeParse({ type: "block:changed", pageId: "pg_1", version: 3 }); 49 expect(r.success).toBe(true); 50 }); 51}); 52 53describe("PresenceFrameSchema / PresenceLeaveFrameSchema (TASK-0093 client frames)", () => { 54 it("accepts a cursor move (blockId + offset)", () => { 55 const r = PresenceFrameSchema.safeParse({ 56 type: "presence", 57 cursor: { blockId: "b3", offset: 4 }, 58 }); 59 expect(r.success).toBe(true); 60 }); 61 62 it("accepts a cursor with a (multi-block) selection range", () => { 63 const r = PresenceFrameSchema.safeParse({ 64 type: "presence", 65 cursor: { 66 blockId: "b3", 67 offset: 2, 68 selection: { fromBlockId: "b3", fromOffset: 0, toBlockId: "b4", toOffset: 5 }, 69 }, 70 viewing: true, 71 }); 72 expect(r.success).toBe(true); 73 }); 74 75 it("accepts a cursor CLEAR (cursor: null — tab hidden / blur)", () => { 76 const r = PresenceFrameSchema.safeParse({ type: "presence", cursor: null, viewing: false }); 77 expect(r.success).toBe(true); 78 if (r.success) expect(r.data.cursor).toBeNull(); 79 }); 80 81 it("accepts a viewing-only update (cursor absent = no change)", () => { 82 const r = PresenceFrameSchema.safeParse({ type: "presence", viewing: false }); 83 expect(r.success).toBe(true); 84 if (r.success) expect(r.data.cursor).toBeUndefined(); 85 }); 86 87 it("rejects a non-integer / negative offset", () => { 88 expect( 89 PresenceFrameSchema.safeParse({ type: "presence", cursor: { blockId: "b3", offset: -1 } }) 90 .success, 91 ).toBe(false); 92 expect( 93 PresenceFrameSchema.safeParse({ type: "presence", cursor: { blockId: "b3", offset: 1.5 } }) 94 .success, 95 ).toBe(false); 96 }); 97 98 it("rejects an empty blockId", () => { 99 expect( 100 PresenceFrameSchema.safeParse({ type: "presence", cursor: { blockId: "", offset: 0 } }) 101 .success, 102 ).toBe(false); 103 }); 104 105 it("is a member of the ClientFrame discriminated union (both frames)", () => { 106 expect( 107 ClientFrameSchema.safeParse({ type: "presence", cursor: { blockId: "b3", offset: 0 } }) 108 .success, 109 ).toBe(true); 110 expect(ClientFrameSchema.safeParse({ type: "presence-leave" }).success).toBe(true); 111 }); 112 113 it("presence-leave carries no payload", () => { 114 const r = PresenceLeaveFrameSchema.safeParse({ type: "presence-leave" }); 115 expect(r.success).toBe(true); 116 }); 117}); 118 119describe("PresenceBroadcastSchema (TASK-0093 server → client)", () => { 120 const user = { id: "u-1", name: "Ana", color: "sky" }; 121 122 it("accepts a full peer state (user + cursor + viewing + lastSeen)", () => { 123 const r = PresenceBroadcastSchema.safeParse({ 124 type: "presence", 125 pageId: "pg_1", 126 user, 127 cursor: { blockId: "b3", offset: 4 }, 128 viewing: true, 129 lastSeen: 1700000000000, 130 }); 131 expect(r.success).toBe(true); 132 }); 133 134 it("accepts an avatars-only state (cap strip: no cursor, no viewing)", () => { 135 const r = PresenceBroadcastSchema.safeParse({ 136 type: "presence", 137 pageId: "pg_1", 138 user, 139 lastSeen: 1700000000000, 140 }); 141 expect(r.success).toBe(true); 142 }); 143 144 it("rejects an empty pageId or a missing/empty user id", () => { 145 expect( 146 PresenceBroadcastSchema.safeParse({ type: "presence", pageId: "", user, lastSeen: 1 }) 147 .success, 148 ).toBe(false); 149 expect( 150 PresenceBroadcastSchema.safeParse({ 151 type: "presence", 152 pageId: "pg_1", 153 user: { id: "", name: "A", color: "sky" }, 154 lastSeen: 1, 155 }).success, 156 ).toBe(false); 157 }); 158 159 it("is a member of the ServerFrame discriminated union", () => { 160 expect( 161 ServerFrameSchema.safeParse({ type: "presence", pageId: "pg_1", user, lastSeen: 1 }).success, 162 ).toBe(true); 163 }); 164}); 165 166describe("PresenceLeaveBroadcastSchema (TASK-0093 server → client leave)", () => { 167 it("accepts a leave with a room id + user id", () => { 168 const r = PresenceLeaveBroadcastSchema.safeParse({ 169 type: "presence-leave", 170 pageId: "page:pg_1", 171 userId: "u-1", 172 }); 173 expect(r.success).toBe(true); 174 }); 175 176 it("rejects an empty room id or user id", () => { 177 expect( 178 PresenceLeaveBroadcastSchema.safeParse({ type: "presence-leave", pageId: "", userId: "u-1" }) 179 .success, 180 ).toBe(false); 181 expect( 182 PresenceLeaveBroadcastSchema.safeParse({ 183 type: "presence-leave", 184 pageId: "page:pg_1", 185 userId: "", 186 }).success, 187 ).toBe(false); 188 }); 189 190 it("is a member of the ServerFrame discriminated union", () => { 191 expect( 192 ServerFrameSchema.safeParse({ type: "presence-leave", pageId: "page:pg_1", userId: "u-1" }) 193 .success, 194 ).toBe(true); 195 }); 196}); 197