import { renderToStaticMarkup } from "react-dom/server";import { describe, expect, it } from "vite-plus/test"; import { ComposerBannerStack, type ComposerBannerStackItem } from "./ComposerBannerStack"; const banner = ( id: string, variant: ComposerBannerStackItem["variant"] = "warning",): ComposerBannerStackItem => ({ id, variant, icon: <span aria-hidden="true">!</span>, title: `${id} warning`,}); describe("ComposerBannerStack", () => { it("keeps expanded banners in layout flow so surrounding content moves out of their way", () => { const markup = renderToStaticMarkup( <ComposerBannerStack items={[banner("front"), banner("stacked")]} />, ); const expandedItems = markup.match( /<div data-composer-banner-stack-expanded-items="true" class="([^"]+)">/, ); expect(expandedItems?.[1]).toContain("grid-rows-[0fr]"); expect(expandedItems?.[1]).toContain("group-hover/banner-stack:grid-rows-[1fr]"); expect(expandedItems?.[1]).toContain("z-20"); expect(expandedItems?.[1]).not.toContain("absolute"); expect(markup.indexOf("front warning")).toBeLessThan(markup.indexOf("stacked warning")); expect(markup).toContain("invisible pointer-events-none"); expect(markup).toContain("group-focus-within/banner-stack:visible"); }); it("colors the collapsed stack cap by the hidden banner's variant, not a fixed warning", () => { const neutralBehind = renderToStaticMarkup( <ComposerBannerStack items={[banner("front", "default"), banner("stacked", "default")]} />, ); expect(neutralBehind).toContain("chat-composer-banner-stack-cap"); expect(neutralBehind).toContain("border-[var(--chat-composer-attached-outline)]"); expect(neutralBehind).not.toContain("border-border"); expect(neutralBehind).not.toContain("border-warning/24"); const warningBehind = renderToStaticMarkup( <ComposerBannerStack items={[banner("front", "default"), banner("stacked", "warning")]} />, ); expect(warningBehind).toContain("border-warning/24"); }); it("does not render an expandable region for a single banner", () => { const markup = renderToStaticMarkup(<ComposerBannerStack items={[banner("front")]} />); expect(markup).not.toContain("data-composer-banner-stack-expanded-items"); expect(markup).toContain("chat-composer-drawer-surface"); expect(markup).toContain("chat-composer-drawer-attached"); expect(markup).not.toContain("before:mask-none"); expect(markup).toContain("text-xs"); expect(markup).toContain('data-composer-banner-drawer="true"'); expect(markup).toContain('data-variant="warning"'); expect(markup).toContain("transform:none"); expect(markup).not.toContain("will-change:transform"); }); it("applies item-specific surface and action layout classes", () => { const markup = renderToStaticMarkup( <ComposerBannerStack items={[ { ...banner("branch"), className: "branch-surface", actionClassName: "branch-actions", actions: <button type="button">Repair</button>, }, ]} />, ); expect(markup).toContain("branch-surface"); expect(markup).toContain("branch-actions"); }); it("renders a disabled compaction action on the shared accessible banner surface", () => { const markup = renderToStaticMarkup( <ComposerBannerStack items={[ { id: "resume-compaction", variant: "info", icon: <span aria-hidden="true">!</span>, title: "Resume with less context", description: "250k tokens from an older session", actions: ( <button type="button" disabled> Compact </button> ), dismissLabel: "Keep full history", onDismiss: () => {}, }, ]} />, ); expect(markup).toContain('role="alert"'); expect(markup).toContain("chat-composer-drawer-attached"); expect(markup).toContain('disabled=""'); expect(markup).toContain('aria-label="Keep full history"'); });});
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";
import { ComposerBannerStack, type ComposerBannerStackItem } from "./ComposerBannerStack";
const banner = (
id: string,
variant: ComposerBannerStackItem["variant"] = "warning",
): ComposerBannerStackItem => ({
id,
variant,
icon: <span aria-hidden="true">!</span>,
title: `${id} warning`,
});
describe("ComposerBannerStack", () => {
it("keeps expanded banners in layout flow so surrounding content moves out of their way", () => {
const markup = renderToStaticMarkup(
<ComposerBannerStack items={[banner("front"), banner("stacked")]} />,
);
const expandedItems = markup.match(
/<div data-composer-banner-stack-expanded-items="true" class="([^"]+)">/,
);
expect(expandedItems?.[1]).toContain("grid-rows-[0fr]");
expect(expandedItems?.[1]).toContain("group-hover/banner-stack:grid-rows-[1fr]");
expect(expandedItems?.[1]).toContain("z-20");
expect(expandedItems?.[1]).not.toContain("absolute");
expect(markup.indexOf("front warning")).toBeLessThan(markup.indexOf("stacked warning"));
expect(markup).toContain("invisible pointer-events-none");
expect(markup).toContain("group-focus-within/banner-stack:visible");
});
it("colors the collapsed stack cap by the hidden banner's variant, not a fixed warning", () => {
const neutralBehind = renderToStaticMarkup(
<ComposerBannerStack items={[banner("front", "default"), banner("stacked", "default")]} />,
);
expect(neutralBehind).toContain("chat-composer-banner-stack-cap");
expect(neutralBehind).toContain("border-[var(--chat-composer-attached-outline)]");
expect(neutralBehind).not.toContain("border-border");
expect(neutralBehind).not.toContain("border-warning/24");
const warningBehind = renderToStaticMarkup(
<ComposerBannerStack items={[banner("front", "default"), banner("stacked", "warning")]} />,
);
expect(warningBehind).toContain("border-warning/24");
});
it("does not render an expandable region for a single banner", () => {
const markup = renderToStaticMarkup(<ComposerBannerStack items={[banner("front")]} />);
expect(markup).not.toContain("data-composer-banner-stack-expanded-items");
expect(markup).toContain("chat-composer-drawer-surface");
expect(markup).toContain("chat-composer-drawer-attached");
expect(markup).not.toContain("before:mask-none");
expect(markup).toContain("text-xs");
expect(markup).toContain('data-composer-banner-drawer="true"');
expect(markup).toContain('data-variant="warning"');
expect(markup).toContain("transform:none");
expect(markup).not.toContain("will-change:transform");
});
it("applies item-specific surface and action layout classes", () => {
const markup = renderToStaticMarkup(
<ComposerBannerStack
items={[
{
...banner("branch"),
className: "branch-surface",
actionClassName: "branch-actions",
actions: <button type="button">Repair</button>,
},
]}
/>,
);
expect(markup).toContain("branch-surface");
expect(markup).toContain("branch-actions");
});
it("renders a disabled compaction action on the shared accessible banner surface", () => {
const markup = renderToStaticMarkup(
<ComposerBannerStack
items={[
{
id: "resume-compaction",
variant: "info",
icon: <span aria-hidden="true">!</span>,
title: "Resume with less context",
description: "250k tokens from an older session",
actions: (
<button type="button" disabled>
Compact
</button>
),
dismissLabel: "Keep full history",
onDismiss: () => {},
},
]}
/>,
);
expect(markup).toContain('role="alert"');
expect(markup).toContain("chat-composer-drawer-attached");
expect(markup).toContain('disabled=""');
expect(markup).toContain('aria-label="Keep full history"');
});
});