|
| 1 | +import { createElement, type ReactElement } from "react"; |
| 2 | +import { renderToStaticMarkup } from "react-dom/server"; |
| 3 | +import { describe, expect, it } from "vitest"; |
| 4 | +import { OperatingSystemContextProvider } from "~/components/primitives/OperatingSystemProvider"; |
| 5 | +import { ShortcutsProvider } from "~/components/primitives/ShortcutsProvider"; |
| 6 | +import { AGENT_AI_DISCLOSURE } from "./AgentAiDisclosure"; |
| 7 | +import { |
| 8 | + DashboardAgentComposer, |
| 9 | + type DashboardAgentComposerLayout, |
| 10 | +} from "./DashboardAgentComposer"; |
| 11 | +import { AgentPanelColumn } from "./panel-layout"; |
| 12 | + |
| 13 | +const CONTEXT = "context-slot"; |
| 14 | +const TRAILING_ACTION = "trailing-action-slot"; |
| 15 | +const NOTICE = "notice-slot"; |
| 16 | + |
| 17 | +function render(element: ReactElement) { |
| 18 | + return renderToStaticMarkup( |
| 19 | + createElement( |
| 20 | + OperatingSystemContextProvider, |
| 21 | + { platform: "mac" }, |
| 22 | + createElement(ShortcutsProvider, null, element) |
| 23 | + ) |
| 24 | + ); |
| 25 | +} |
| 26 | + |
| 27 | +function composer(layout: DashboardAgentComposerLayout, isStreaming = false) { |
| 28 | + return createElement(DashboardAgentComposer, { |
| 29 | + layout, |
| 30 | + value: "Why did this run fail?", |
| 31 | + onChange: () => {}, |
| 32 | + onSubmit: () => {}, |
| 33 | + onStop: () => {}, |
| 34 | + isStreaming, |
| 35 | + context: createElement("span", null, CONTEXT), |
| 36 | + trailingAction: createElement("span", null, TRAILING_ACTION), |
| 37 | + notice: createElement("span", null, NOTICE), |
| 38 | + }); |
| 39 | +} |
| 40 | + |
| 41 | +function occurrences(html: string, needle: string) { |
| 42 | + return html.split(needle).length - 1; |
| 43 | +} |
| 44 | + |
| 45 | +/** |
| 46 | + * Markup order stands in for the layout: the panel is everything from the text box up to the |
| 47 | + * notice, which sits under it. The webapp renders on the server, so this needs no DOM. |
| 48 | + */ |
| 49 | +describe.each(["docked", "hero"] as const)("the %s composer", (layout) => { |
| 50 | + it("leaves the AI disclosure to the panel column around it", () => { |
| 51 | + expect(render(composer(layout))).not.toContain(AGENT_AI_DISCLOSURE); |
| 52 | + }); |
| 53 | + |
| 54 | + it("keeps the context, the trailing action and Send inside the panel, once each", () => { |
| 55 | + const html = render(composer(layout)); |
| 56 | + const textBox = html.indexOf("<textarea"); |
| 57 | + const notice = html.indexOf(NOTICE); |
| 58 | + for (const needle of [CONTEXT, TRAILING_ACTION, 'aria-label="Send"']) { |
| 59 | + expect(occurrences(html, needle), needle).toBe(1); |
| 60 | + expect(html.indexOf(needle), needle).toBeGreaterThan(textBox); |
| 61 | + expect(html.indexOf(needle), needle).toBeLessThan(notice); |
| 62 | + } |
| 63 | + }); |
| 64 | + |
| 65 | + it("puts Stop where Send was while a turn is in flight", () => { |
| 66 | + const html = render(composer(layout, true)); |
| 67 | + expect(html).not.toContain('aria-label="Send"'); |
| 68 | + const stop = html.indexOf('aria-label="Stop generating"'); |
| 69 | + expect(stop).toBeGreaterThan(html.indexOf(TRAILING_ACTION)); |
| 70 | + expect(stop).toBeLessThan(html.indexOf(NOTICE)); |
| 71 | + }); |
| 72 | +}); |
| 73 | + |
| 74 | +describe("the panel column", () => { |
| 75 | + it("ends with the AI disclosure, once, under the composer and its notice", () => { |
| 76 | + const html = render(createElement(AgentPanelColumn, { fullscreen: false }, composer("docked"))); |
| 77 | + expect(occurrences(html, AGENT_AI_DISCLOSURE)).toBe(1); |
| 78 | + expect(html.indexOf(AGENT_AI_DISCLOSURE)).toBeGreaterThan(html.indexOf(NOTICE)); |
| 79 | + }); |
| 80 | + |
| 81 | + it("ends with it whatever fills the panel, so it stays put from new chat to chat", () => { |
| 82 | + const html = render( |
| 83 | + createElement(AgentPanelColumn, { fullscreen: true }, createElement("div", null, "hero-slot")) |
| 84 | + ); |
| 85 | + expect(occurrences(html, AGENT_AI_DISCLOSURE)).toBe(1); |
| 86 | + expect(html.indexOf(AGENT_AI_DISCLOSURE)).toBeGreaterThan(html.indexOf("hero-slot")); |
| 87 | + }); |
| 88 | +}); |
0 commit comments