/** * Test: lunch-poll generated-art wiring — host-gated generation + explicit * keep-action persistence (the post-CT-1836 structure: the card reads the * GeneratedArt sub-pattern's fetch-derived outputs directly; nothing sends * from inside a computed). * * Single-identity caveat (as main.test.tsx): this runtime's one identity IS * the host after joining, so the host path runs end-to-end: join → add an * option → the host-gated GeneratedArt fetches the mocked /api/ai/img * generation (visible as the cf-image overlay) → the host keeps it — the * card's keep button sends { optionId, imageUrl } into `setOptionImage`; this * test drives that same stream directly with the imageUrl the button would * read — → the option carries `imageUrl` and the stored renders. Every * other viewer renders that same stored value by construction (sourceUrl * short-circuits generation); the gate itself (shouldGenerate) is covered at * the sub-pattern level in generated-art.test.tsx. */ import { action, assert, pattern, TESTS, UI, Writable } from "commonfabric"; import { findElement, findNodeByProp, readValue, } from "../test/vnode-helpers.ts"; import CozyPoll, { type LunchProfile } from "./main.tsx"; // 1×1 transparent PNG, the mocked generation response body. The persisted // value is its exact data URL: FetchBinary bytes → base64 re-encode is an // identity round-trip on the same bytes, so the card's keep button and this // test send the same string. (Both plain literals: SES-mode module scope // rejects computed top-level values like template joins.) const TINY_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="; const EXPECTED_DATA_URL = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="; export const fetchMocks = [ { urlIncludes: "/api/ai/img", contentType: "image/png", base64Body: TINY_PNG_BASE64, }, ]; export default pattern(() => { // Identity is a profile cell; claim the host's through the test seam. const host = Writable.of({ name: "Host" }); const poll = CozyPoll({}); const action_become_host = action(() => { poll.overrideViewer.send({ profile: host, name: "Host" }); }); const action_join_as_host = action(() => { poll.joinAs.send({}); }); const action_add_sushi = action(() => { poll.addOption.send({ title: "Sushi Palace" }); }); const assert_option_added = assert(() => poll.options.length === 1 && poll.options[0]?.title === "Sushi Palace" ); // Post-settle the host's client has generated: the cf-image overlay // (generated, not yet stored) is in the rendered tree — the fetch-derived // read chain through both sub-pattern boundaries works. (Until CT-1836's // traversal fix this file carried a canary pinning the opposite.) const assert_generated_overlay_renders = assert(() => findElement(poll[UI], "cf-image") !== undefined ); // The host keeps the art: the same payload the card's keep button sends // (the button reads `art.imageDataUrl`, which equals EXPECTED_DATA_URL for // the mocked bytes — the identity round-trip noted above). const action_keep_art = action(() => { const optionId = readValue(poll.options[0]?.id); poll.setOptionImage.send({ optionId: typeof optionId === "string" ? optionId : "", imageUrl: EXPECTED_DATA_URL, }); }); const assert_image_persisted = assert(() => readValue(poll.options[0]?.imageUrl) === EXPECTED_DATA_URL ); const assert_stored_img_renders = assert(() => findNodeByProp(poll[UI], "src", EXPECTED_DATA_URL) !== undefined ); return { [TESTS]: [ { action: action_become_host }, { action: action_join_as_host }, { action: action_add_sushi }, { assertion: assert_option_added }, // Drives the mocked generation fetch to completion. { settle: true }, { assertion: assert_generated_overlay_renders }, { action: action_keep_art }, { assertion: assert_image_persisted }, // One more settle beat: the persisted URL flows back into the card as // `sourceUrl` and the stored- branch re-renders. { settle: true }, { assertion: assert_stored_img_renders }, ], poll, }; });