social components inlay.at
atproto components sdui
100
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 355 lines 10 kB view raw
1import { describe, it } from "node:test"; 2import assert from "node:assert/strict"; 3import type { l } from "@atproto/lex"; 4 5import { 6 $, 7 serializeTree, 8 deserializeTree, 9 resolveBindings, 10 type Element, 11} from "../src/index.ts"; 12 13// Props from deserialized data are untyped — cast when passing back through $. 14type Props = l.LexMap & { key?: string }; 15 16function xrpc( 17 tree: Element, 18 component: (input: Record<string, unknown>) => Element 19): Element { 20 const props = (tree.props ?? {}) as Record<string, unknown>; 21 22 const refs = new Map<string, Element>(); 23 const refSlots = new Set<object>(); 24 const requestBody = JSON.stringify( 25 serializeTree(props, (el) => { 26 if (el.type === "at.inlay.Slot") { 27 if (refSlots.has(el as Element)) return el; 28 throw new Error("Unexpected Slot in props"); 29 } 30 const id = String(refs.size); 31 refs.set(id, el as Element); 32 const slot = $("at.inlay.Slot", { id }); 33 refSlots.add(slot); 34 return slot; 35 }) 36 ); 37 38 const slots = new Set<object>(); 39 const input = deserializeTree(JSON.parse(requestBody), (el) => { 40 if (el.type === "at.inlay.Slot" && el.props) { 41 slots.add(el.props); 42 return el; 43 } 44 throw new Error(`Unexpected element in component input: ${el.type}`); 45 }) as Record<string, unknown>; 46 47 const response = component(input); 48 49 const responseBody = JSON.stringify( 50 serializeTree(response, (el) => { 51 if (el.type === "at.inlay.Slot") { 52 if (!el.props || !slots.has(el.props)) { 53 throw new Error(`Forged slot in component output`); 54 } 55 } 56 return el; 57 }) 58 ); 59 60 return deserializeTree(JSON.parse(responseBody), (el) => { 61 if (el.type === "at.inlay.Slot" && el.props) { 62 const id = (el.props as Record<string, unknown>).id as string; 63 const stashed = refs.get(id); 64 if (stashed) return stashed; 65 throw new Error(`Unknown slot id: ${id}`); 66 } 67 return el; 68 }) as Element; 69} 70 71describe("XRPC flow", () => { 72 it("children round-trip through full flow", () => { 73 const tree = $( 74 "com.example.Card", 75 {}, 76 $("com.example.Text", { value: "hi" }) 77 ); 78 assert.deepEqual( 79 xrpc(tree, (input) => 80 $("com.example.Box", { children: input.children } as Props) 81 ), 82 $("com.example.Box", {}, $("com.example.Text", { key: "0", value: "hi" })) 83 ); 84 }); 85 86 it("component can reorder children", () => { 87 const tree = $( 88 "com.example.Layout", 89 {}, 90 $("com.example.Header", { title: "Hi" }), 91 $("com.example.Footer", { year: 2025 }) 92 ); 93 assert.deepEqual( 94 xrpc(tree, (input) => { 95 const [a, b] = input.children as unknown[]; 96 return $("com.example.Box", { children: [b, a] } as Props); 97 }), 98 $( 99 "com.example.Box", 100 {}, 101 $("com.example.Footer", { key: "1", year: 2025 }), 102 $("com.example.Header", { key: "0", title: "Hi" }) 103 ) 104 ); 105 }); 106 107 it("component can duplicate a child", () => { 108 const child = $("com.example.Text", { key: "0", value: "hi" }); 109 const tree = $( 110 "com.example.Card", 111 {}, 112 $("com.example.Text", { value: "hi" }) 113 ); 114 assert.deepEqual( 115 xrpc(tree, (input) => { 116 const c = (input.children as unknown[])[0]; 117 return $("com.example.Box", { children: [c, c] } as Props); 118 }), 119 $("com.example.Box", {}, child, child) 120 ); 121 }); 122 123 it("component can drop children", () => { 124 const tree = $( 125 "com.example.Layout", 126 {}, 127 $("com.example.Header"), 128 $("com.example.Footer") 129 ); 130 assert.deepEqual( 131 xrpc(tree, () => $("com.example.Box", { label: "empty" })), 132 $("com.example.Box", { label: "empty" }) 133 ); 134 }); 135 136 it("elements nested in plain object props round-trip", () => { 137 const tree = $("com.example.Card", { 138 header: { icon: $("com.example.Icon", { name: "star" }) }, 139 }); 140 assert.deepEqual( 141 xrpc(tree, (input) => { 142 const icon = (input.header as Record<string, unknown>).icon; 143 return $("com.example.Box", { content: icon } as Props); 144 }), 145 $("com.example.Box", { 146 content: $("com.example.Icon", { name: "star" }), 147 }) 148 ); 149 }); 150 151 it("primitive-only props pass through unchanged", () => { 152 const tree = $("com.example.Spacer", { 153 gap: "medium", 154 count: 42, 155 flag: true, 156 }); 157 assert.deepEqual( 158 xrpc(tree, (input) => $("com.example.Box", input as Props)), 159 $("com.example.Box", { gap: "medium", count: 42, flag: true }) 160 ); 161 }); 162 163 it("data with $ keys survives full pipeline", () => { 164 const tree = $("com.example.Card", { 165 a: { $: "$", type: "app.bsky.feed.post" }, 166 b: { $: "$weird$value" }, 167 c: { $: "$$double" }, 168 d: { $: "hello" }, 169 }); 170 assert.deepEqual( 171 xrpc(tree, (input) => $("com.example.Box", input as Props)), 172 $("com.example.Box", { 173 a: { $: "$", type: "app.bsky.feed.post" }, 174 b: { $: "$weird$value" }, 175 c: { $: "$$double" }, 176 d: { $: "hello" }, 177 }) 178 ); 179 }); 180 181 it("rejects forged slots even with valid-looking id", () => { 182 const tree = $( 183 "com.example.Card", 184 {}, 185 $("com.example.Text", { value: "hi" }) 186 ); 187 assert.throws( 188 () => 189 xrpc(tree, () => 190 $("com.example.Box", { 191 stolen: $("at.inlay.Slot", { id: "0" }), 192 }) 193 ), 194 /slot/i 195 ); 196 }); 197 198 it("named element props round-trip through XRPC", () => { 199 const left = $("com.example.Text", { value: "hello" }); 200 const right = $("com.example.Text", { value: "world" }); 201 const tree = $("com.example.LeftRight", { left, right }); 202 203 assert.deepEqual( 204 xrpc(tree, (input) => 205 $( 206 "com.example.Row", 207 { gap: "medium" }, 208 $("com.example.Stack", {} as Props, input.left as unknown as Element), 209 $("com.example.Stack", {} as Props, input.right as unknown as Element) 210 ) 211 ), 212 $( 213 "com.example.Row", 214 { gap: "medium" }, 215 $("com.example.Stack", { children: [left] } as Props), 216 $("com.example.Stack", { children: [right] } as Props) 217 ) 218 ); 219 }); 220 221 it("same element passed to multiple props gets separate slots", () => { 222 const foo = $("com.example.Text", { value: "shared" }); 223 const tree = $("com.example.LeftRight", { left: foo, right: foo }); 224 225 assert.deepEqual( 226 xrpc(tree, (input) => 227 $("com.example.Box", { 228 first: input.left, 229 second: input.right, 230 } as Props) 231 ), 232 $("com.example.Box", { first: foo, second: foo }) 233 ); 234 }); 235 236 it("component can return same slot multiple times", () => { 237 const child = $("com.example.Text", { key: "0", value: "once" }); 238 const tree = $( 239 "com.example.Wrapper", 240 {}, 241 $("com.example.Text", { value: "once" }) 242 ); 243 244 assert.deepEqual( 245 xrpc(tree, (input) => { 246 const c = (input.children as unknown[])[0]; 247 return $("com.example.Stack", { children: [c, c] } as Props); 248 }), 249 $("com.example.Stack", {}, child, child) 250 ); 251 }); 252 253 it("nested XRPC calls produce independent refs", () => { 254 const tree = $( 255 "com.example.Outer", 256 {}, 257 $("com.example.Inner", {}, $("com.example.Text", { value: "leaf" })) 258 ); 259 260 const afterOuter = xrpc(tree, (input) => 261 $("com.example.Box", { children: input.children } as Props) 262 ); 263 assert.deepEqual( 264 afterOuter, 265 $( 266 "com.example.Box", 267 {}, 268 $( 269 "com.example.Inner", 270 { key: "0" }, 271 $("com.example.Text", { key: "0", value: "leaf" }) 272 ) 273 ) 274 ); 275 276 const inner = ( 277 (afterOuter.props as Record<string, unknown>).children as Element[] 278 )[0]; 279 assert.deepEqual( 280 xrpc(inner, (input) => 281 $("com.example.Wrapper", { children: input.children } as Props) 282 ), 283 $( 284 "com.example.Wrapper", 285 {}, 286 $("com.example.Text", { key: "0", value: "leaf" }) 287 ) 288 ); 289 }); 290}); 291 292// --- Full pipeline: resolve bindings → XRPC --- 293 294function resolvePath(scope: Record<string, unknown>, path: string[]): unknown { 295 let current: unknown = scope; 296 for (const seg of path) { 297 if (current == null || typeof current !== "object") return undefined; 298 current = (current as Record<string, unknown>)[seg]; 299 } 300 return current; 301} 302 303describe("resolve → XRPC pipeline", () => { 304 it("bindings in props resolve before XRPC", () => { 305 const scope = { user: { name: "Dan" } }; 306 const props = { 307 title: $("at.inlay.Binding", { path: ["user", "name"] }), 308 }; 309 310 // Step 1: resolve bindings in props 311 const resolvedProps = resolveBindings(props, (path) => { 312 const v = resolvePath(scope, path); 313 if (v === undefined) throw new Error(`MISSING:${path.join(".")}`); 314 return v; 315 }) as Record<string, unknown>; 316 317 // Step 2: XRPC with resolved props 318 const tree = $("com.example.Card", resolvedProps as Props); 319 const result = xrpc(tree, (input) => 320 $("com.example.Box", { label: input.title } as Props) 321 ); 322 323 assert.deepEqual(result, $("com.example.Box", { label: "Dan" })); 324 }); 325 326 it("bindings in children resolve, child elements stash as slots", () => { 327 const scope = { greeting: "Hello" }; 328 const tree = $( 329 "com.example.Card", 330 {}, 331 $("com.example.Text", { 332 value: $("at.inlay.Binding", { path: ["greeting"] }), 333 }) 334 ); 335 336 // resolveBindings is opaque to non-Binding elements — 337 // the Binding inside Text's props is NOT resolved here. 338 // In the real pipeline, host components resolve their own props. 339 // For XRPC, the caller resolves ALL bindings before sending. 340 const props = (tree.props ?? {}) as Record<string, unknown>; 341 const resolved = resolveBindings(props, (path) => { 342 const v = resolvePath(scope, path); 343 if (v === undefined) throw new Error(`MISSING:${path.join(".")}`); 344 return v; 345 }) as Record<string, unknown>; 346 347 // The Text element (with its Binding) passes through opaque 348 const children = resolved.children as Element[]; 349 const textValue = (children[0].props as Record<string, unknown>).value; 350 assert.ok( 351 typeof textValue === "object" && textValue !== null, 352 "Binding inside Text is still an element (opaque)" 353 ); 354 }); 355});