import { createTestSpace, env, Page } from "@commonfabric/integration"; import { type DID, Identity } from "@commonfabric/identity"; import { ShellIntegration } from "@commonfabric/integration/shell-utils"; import { resolveLocalProgram } from "@commonfabric/runner/local-program.deno"; import { afterAll, beforeAll, describe, it } from "@std/testing/bdd"; import { join } from "@std/path"; import { initializePiecesController, PiecesController, } from "./pieces-controller.ts"; import { clickTrustedAction, fillCfInput, fillCfTextarea, waitForRuntimeIdle, waitForSettledText, waitForTextAbsent, } from "./cfc-browser-helpers.ts"; import { clickButtonWithExactText } from "./note-button-helpers.ts"; const { API_URL, FRONTEND_URL } = env; // The `#profile` wish fallback surface (rendered by the embed when no profile // resolves) is the shared trusted create surface: it forwards `inputId: // "wish-profile-name-input"` to its inner input and exposes the trusted // `CreateProfile` action (see packages/runner/src/builtins/wish.ts). Same // surface the shared-profile integration test drives. const TRUSTED_PROFILE_CREATE_ACTION = "CreateProfile"; const CREATE_INPUT = "#wish-profile-name-input"; /** * Browser integration coverage for the CT-1833 profile-embed pattern's * RESOLVED-profile path (CT-1846). * * The lane-2 unit test (packages/patterns/system/profile-embed.test.tsx) can * only cover the fallback branch + the amend contract against a raw ProfileHome, * because `#profile` cannot be satisfied in the console-error-gated pattern-unit * lane (a valid profile needs a cross-space create the lane forbids). This test * closes that gap end-to-end in a real browser: * * 1. Deploy profile-embed as a piece and visit it. The pattern wishes * `#profile` itself, so it renders the logged-in identity's own profile — * no profile input. * 2. With no profile yet, the embed renders the wish fallback (the trusted * create surface). Create a profile through it, satisfying `#profile`. * 3. Assert the RESOLVED presentation: the hero `cf-profile-badge` (name) and, * after a bio amend, the bio paragraph — and that the ProfileHome * developer chrome ("Pin a piece" form / elements grid) is NOT rendered * (the embed hides it). * 4. Enter edit mode, amend the name, save; assert the badge updates. The save * dispatches through the resolved profile's exported owner-protected * `setName` / `setBio` streams on `profileWish.result`. */ describe("profile-embed integration test", () => { const shell = new ShellIntegration(); shell.bindLifecycle(); let identity: Identity; let cc: PiecesController; let spaceDid: DID; let pieceId: string; let pieceSinkCancel: (() => void) | undefined; beforeAll(async () => { identity = await Identity.generate({ implementation: "noble" }); spaceDid = await createTestSpace(identity); cc = await initializePiecesController({ space: spaceDid, apiUrl: new URL(API_URL), identity, }); // Pre-create the space-root (default) pattern so the browser's // `pattern:getSpaceRoot` storage-RESUMEs it instead of taking the create // path and cold-compiling default-app inside its worker — see the // beforeAll comment in lunch-poll-vote.test.ts. await cc.ensureDefaultPattern(); // Resolve the profile-embed system pattern through the harness (content- // addressed program) and create from that, mirroring shared-profile.test.ts. // profile-embed.tsx imports its sibling ./profile-home.tsx, so the resolver // root must be the patterns package so that relative import resolves. const sourcePath = join( import.meta.dirname!, "..", "system", "profile-embed.tsx", ); const rootPath = join(import.meta.dirname!, ".."); const program = await resolveLocalProgram( (resolver) => cc.runtime.harness.resolve(resolver), { main: sourcePath, root: rootPath }, ); const piece = await cc.create(program, { start: true }); pieceId = piece.id; // Keep the result cell subscribed so the started piece's data stays live // and durable when the shell loads it (see shared-profile.test.ts note). const resultCell = cc.getResult(piece.getCell()); pieceSinkCancel = resultCell.sink(() => {}); }); afterAll(async () => { pieceSinkCancel?.(); await cc?.dispose(); }); it("renders the resolved profile and amends it through the exported streams", async () => { const page = shell.page(); await shell.goto({ frontendUrl: FRONTEND_URL, view: { spaceDid, pieceId }, identity, }); // No profile yet: the embed renders the wish fallback (create surface), not // the resolved presentation. await waitForSelector( page, '[data-ui-region="profile-embed-fallback"]', ); // Create a profile through the fallback surface, satisfying `#profile`. await createProfileFromFallback(page, "Ada Lovelace"); // Resolved presentation: the hero badge renders the name. await waitForSettledText(page, "#profile-embed-badge", "Ada Lovelace"); // The presentation region is present. await waitForSelector( page, '[data-ui-region="profile-embed-presentation"]', ); // The embed HIDES the ProfileHome developer chrome: the "Pin a piece" form // (a ProfileHome-only affordance) must not render anywhere in the embed. await waitForTextAbsent( page, '[data-ui-pattern="ProfileEmbed"]', "Pin a piece", ); // Enter edit mode: the "Edit profile" button flips `editing` and seeds the // draft inputs from the resolved values. await clickButtonWithExactText(page, "Edit profile"); await waitForRuntimeIdle(page); await waitForSelector(page, '[data-ui-region="profile-embed-edit"]'); // Amend the name: fill the draft and click "Save name". The save reads the // draft, suppresses empty, and dispatches { name } into the resolved // profile's exported owner-protected setName stream. await fillCfInput( page, '[data-ui-region="profile-embed-edit"] cf-input', "Grace Hopper", ); await clickButtonWithExactText(page, "Save name"); // The save dispatches into the resolved profile's owner-protected setName // stream — a cross-space write into the profile space this session created // and still holds open. `waitForRuntimeIdle` awaits the pending-commit // barrier, so the write is server-confirmed before the next edit. await waitForRuntimeIdle(page); // Amend the bio too (bio is clearable; the embed sends the trimmed draft). // cf-textarea wraps a native