diff --git a/ui/src/pages/BoardChat.css.test.ts b/ui/src/pages/BoardChat.css.test.ts new file mode 100644 index 00000000000..7f6b313e44b --- /dev/null +++ b/ui/src/pages/BoardChat.css.test.ts @@ -0,0 +1,42 @@ +import { existsSync, readFileSync } from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; + +/** + * PAP-54: the .typing-dots CSS block was silently dropped from index.css + * during a theme migration, leaving static markup with no animation. Guard + * the source so the block can't vanish again without failing a test. The + * browser-computed `animationName !== "none"` assertion lives in + * tests/e2e/conference-room-typing-intro.spec.ts. + */ +function readIndexCss(): string { + let dir = process.cwd(); + for (let depth = 0; depth < 6; depth++) { + for (const candidate of [ + path.join(dir, "src/index.css"), + path.join(dir, "ui/src/index.css"), + ]) { + if (existsSync(candidate)) return readFileSync(candidate, "utf8"); + } + dir = path.dirname(dir); + } + throw new Error("ui/src/index.css not found from " + process.cwd()); +} + +describe("typing-dots CSS animation guard (PAP-54 failure mode)", () => { + const css = readIndexCss(); + + it("keeps the bounce animation wired to .typing-dots span", () => { + const spanRules = [...css.matchAll(/\.typing-dots span\s*\{[^}]*\}/g)].map( + (match) => match[0], + ); + expect(spanRules.length).toBeGreaterThan(0); + expect( + spanRules.some((rule) => /animation:\s*typing-bounce/.test(rule)), + ).toBe(true); + }); + + it("keeps the typing-bounce keyframes", () => { + expect(css).toMatch(/@keyframes typing-bounce\s*\{/); + }); +}); diff --git a/ui/src/pages/BoardChat.test.tsx b/ui/src/pages/BoardChat.test.tsx index 77faf5f11aa..d8a5b181bd5 100644 --- a/ui/src/pages/BoardChat.test.tsx +++ b/ui/src/pages/BoardChat.test.tsx @@ -1,7 +1,5 @@ // @vitest-environment jsdom -import { existsSync, readFileSync } from "node:fs"; -import path from "node:path"; import { act, forwardRef, useImperativeHandle } from "react"; import type { ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; @@ -267,43 +265,34 @@ describe("BoardChat staged typing intro", () => { expect(hasWelcome(container)).toBe(true); expect(hasTypingDots(container)).toBe(false); }); -}); -describe("typing-dots CSS animation guard (PAP-54 failure mode)", () => { - // PAP-54: the .typing-dots CSS block was silently dropped from index.css - // during a theme migration, leaving static markup with no animation. Guard - // the source so the block can't vanish again without failing a test. The - // browser-computed `animationName !== "none"` assertion lives in - // tests/e2e/conference-room-typing-intro.spec.ts. - // Locate ui/src/index.css regardless of whether vitest runs from ui/ or - // the workspace root (import.meta.url is an http URL under jsdom, and the - // css pipeline swallows `?raw` imports — plain fs is the reliable path). - function readIndexCss(): string { - let dir = process.cwd(); - for (let depth = 0; depth < 6; depth++) { - for (const candidate of [ - path.join(dir, "src/index.css"), - path.join(dir, "ui/src/index.css"), - ]) { - if (existsSync(candidate)) return readFileSync(candidate, "utf8"); - } - dir = path.dirname(dir); - } - throw new Error("ui/src/index.css not found from " + process.cwd()); - } - const css = readIndexCss(); + it("reserves mobile viewport height and bottom-nav space for the composer", async () => { + await render(); - it("keeps the bounce animation wired to .typing-dots span", () => { - const spanRules = [...css.matchAll(/\.typing-dots span\s*\{[^}]*\}/g)].map( - (m) => m[0], + const shell = container.querySelector( + '[data-testid="board-chat-shell"]', + ) as HTMLDivElement | null; + expect(shell).not.toBeNull(); + expect(shell?.className).toContain("h-[calc(100dvh_-_3rem_-_4rem"); + expect(shell?.className).toContain("env(safe-area-inset-top)"); + expect(shell?.className).toContain("env(safe-area-inset-bottom)"); + expect(shell?.className).toContain("-m-4"); + expect(shell?.className).toContain("md:h-[calc(100%_+_3rem)]"); + + const dock = container.querySelector( + '[data-testid="board-chat-composer-dock"]', + ) as HTMLDivElement | null; + expect(dock).not.toBeNull(); + expect(dock?.className).toContain("bottom-0"); + expect(dock?.className).toContain("px-4"); + expect(dock?.className).toContain("md:px-6"); + + const feedButton = container.querySelector( + 'button[aria-label="Open agent feed"]', + ) as HTMLButtonElement | null; + expect(feedButton).not.toBeNull(); + expect(feedButton?.className).toContain( + "bottom-[calc(5rem_+_env(safe-area-inset-bottom))]", ); - expect(spanRules.length).toBeGreaterThan(0); - expect( - spanRules.some((rule) => /animation:\s*typing-bounce/.test(rule)), - ).toBe(true); - }); - - it("keeps the typing-bounce keyframes", () => { - expect(css).toMatch(/@keyframes typing-bounce\s*\{/); }); }); diff --git a/ui/src/pages/BoardChat.tsx b/ui/src/pages/BoardChat.tsx index 423fbd8cf55..1c504a3cbec 100644 --- a/ui/src/pages/BoardChat.tsx +++ b/ui/src/pages/BoardChat.tsx @@ -668,7 +668,10 @@ export function BoardChat() { } return ( -