test(ai-toolbar): assert runtime layout and action states
This commit is contained in:
@@ -1,10 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||
import { readFileSync } from "node:fs";
|
||||
import * as React from "react";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
|
||||
const readToolbarSource = () => readFileSync("src/components/ai-toolbar.tsx", "utf8");
|
||||
|
||||
type AIToolbarProps = {
|
||||
adminAiEnabled: boolean;
|
||||
aiEnabled: boolean;
|
||||
@@ -128,7 +125,12 @@ const renderToolbar = async (overrides: Partial<AIToolbarProps> = {}) => {
|
||||
|
||||
const renderToolbarMarkup = async (
|
||||
overrides: Partial<AIToolbarProps> = {},
|
||||
{ isMobile = false }: { isMobile?: boolean } = {},
|
||||
) => {
|
||||
mock.module("@/hooks/use-mobile", () => ({
|
||||
useIsMobile: () => isMobile,
|
||||
}));
|
||||
|
||||
const { AIToolbar } = await import("@/components/ai-toolbar");
|
||||
|
||||
return renderToStaticMarkup(
|
||||
@@ -136,6 +138,22 @@ const renderToolbarMarkup = async (
|
||||
);
|
||||
};
|
||||
|
||||
const escapeForRegex = (value: string) =>
|
||||
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
const getButtonOpeningTag = (markup: string, label: string) => {
|
||||
const buttonMatch = markup.match(
|
||||
new RegExp(
|
||||
`<button\\b[^>]*>(?:(?!<\\/button>)[\\s\\S])*?${escapeForRegex(label)}(?:(?!<\\/button>)[\\s\\S])*?<\\/button>`,
|
||||
),
|
||||
);
|
||||
const openingTagMatch = buttonMatch?.[0].match(/^<button\b[^>]*>/);
|
||||
|
||||
expect(openingTagMatch).toBeDefined();
|
||||
|
||||
return openingTagMatch![0];
|
||||
};
|
||||
|
||||
const renderToolbarBoundary = async (
|
||||
overrides: Partial<AIToolbarProps> = {},
|
||||
) => {
|
||||
@@ -160,27 +178,29 @@ const renderToolbarBoundary = async (
|
||||
};
|
||||
|
||||
describe("AI toolbar layout contracts", () => {
|
||||
test("desktop composer uses a 70/30 split driven by the isMobile branch", () => {
|
||||
const source = readToolbarSource();
|
||||
test("desktop composer renders a 70/30 split while mobile stays single-column", async () => {
|
||||
const desktopMarkup = await renderToolbarMarkup();
|
||||
const mobileMarkup = await renderToolbarMarkup({}, { isMobile: true });
|
||||
|
||||
expect(source).toContain("isMobile");
|
||||
expect(source).toContain('? "grid gap-3"');
|
||||
expect(source).toContain(
|
||||
': "grid gap-3 grid-cols-[minmax(0,0.7fr)_minmax(0,0.3fr)]"',
|
||||
expect(desktopMarkup).toContain(
|
||||
"grid gap-3 grid-cols-[minmax(0,0.7fr)_minmax(0,0.3fr)]",
|
||||
);
|
||||
expect(source).not.toContain(
|
||||
': "grid gap-3 grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"',
|
||||
expect(desktopMarkup).not.toContain(
|
||||
"grid gap-3 grid-cols-[minmax(0,1fr)_minmax(0,1fr)]",
|
||||
);
|
||||
expect(mobileMarkup).not.toContain(
|
||||
"grid-cols-[minmax(0,0.7fr)_minmax(0,0.3fr)]",
|
||||
);
|
||||
});
|
||||
|
||||
test("example prompts render as a two-column masonry-style cluster below the textarea", () => {
|
||||
const source = readToolbarSource();
|
||||
test("example prompts render as a masonry-style cluster below the textarea", async () => {
|
||||
const markup = await renderToolbarMarkup();
|
||||
|
||||
expect(source).toContain("Try:");
|
||||
expect(source).toContain('className="columns-2 gap-2"');
|
||||
expect(source).toContain('className="mb-2 break-inside-avoid"');
|
||||
expect(source).toContain(
|
||||
'className="h-auto w-full justify-start text-left whitespace-normal rounded-2xl px-3 py-2 text-[11px] leading-relaxed"',
|
||||
expect(markup).toContain("Try:");
|
||||
expect(markup).toContain('class="columns-2 gap-2"');
|
||||
expect(markup).toContain('class="mb-2 break-inside-avoid"');
|
||||
expect(markup).toContain(
|
||||
"h-auto w-full justify-start text-left whitespace-normal rounded-2xl px-3 py-2 text-[11px] leading-relaxed",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -195,15 +215,19 @@ describe("AI toolbar layout contracts", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("attachment previews stack one per row instead of rendering a horizontal strip or multi-column gallery", () => {
|
||||
const source = readToolbarSource();
|
||||
const previewGridClassNames = [...source.matchAll(
|
||||
/className="([^"]*\bmt-3\b[^"]*\bgrid\b[^"]*\bgap-2\b[^"]*)"/g,
|
||||
test("attachment previews render in a stacked grid instead of a multi-column strip", async () => {
|
||||
const markup = await renderToolbarMarkup({
|
||||
imagePreviews: ["blob:first", "blob:second"],
|
||||
});
|
||||
const previewGridClassNames = [...markup.matchAll(
|
||||
/class="([^"]*\bmt-3\b[^"]*\bgrid\b[^"]*\bgap-2\b[^"]*)"/g,
|
||||
)].map(([, className]) => className);
|
||||
const multiColumnPreviewPattern =
|
||||
/(?:^|\s)(?:[a-z]+:)*(?:grid-cols-(?:\[[^\]]+\]|\S+)|grid-flow-col|auto-cols-(?:\[[^\]]+\]|\S+)|columns-(?:\[[^\]]+\]|\S+)|overflow-x-auto|flex-row)/;
|
||||
|
||||
expect(source).toContain('className="mt-3 grid gap-2"');
|
||||
expect(markup).toContain('class="mt-3 grid gap-2"');
|
||||
expect(markup).toContain("Attached image 1");
|
||||
expect(markup).toContain("Attached image 2");
|
||||
expect(previewGridClassNames).toHaveLength(1);
|
||||
expect(previewGridClassNames[0]).not.toMatch(multiColumnPreviewPattern);
|
||||
});
|
||||
@@ -255,6 +279,43 @@ describe("AI toolbar state rendering", () => {
|
||||
expect(markup).toContain("Updated just now");
|
||||
expect(markup).toContain("Dismiss AI summary");
|
||||
});
|
||||
|
||||
test("Summarize only appears when there are events to summarize", async () => {
|
||||
const emptyMarkup = await renderToolbarMarkup({ events: [] });
|
||||
const populatedMarkup = await renderToolbarMarkup({
|
||||
events: [{ id: "event-1" }],
|
||||
});
|
||||
|
||||
expect(emptyMarkup).not.toContain("Summarize");
|
||||
expect(populatedMarkup).toContain("Summarize");
|
||||
});
|
||||
|
||||
test("Generate event stays disabled while loading or when both prompt and images are absent", async () => {
|
||||
const emptyMarkup = await renderToolbarMarkup();
|
||||
const loadingMarkup = await renderToolbarMarkup({
|
||||
aiLoading: true,
|
||||
aiPrompt: "Draft a kickoff",
|
||||
});
|
||||
const promptMarkup = await renderToolbarMarkup({
|
||||
aiPrompt: "Draft a kickoff",
|
||||
});
|
||||
const imageMarkup = await renderToolbarMarkup({
|
||||
imagePreviews: ["blob:first"],
|
||||
});
|
||||
|
||||
expect(getButtonOpeningTag(emptyMarkup, "Generate event")).toContain(
|
||||
" disabled=\"\"",
|
||||
);
|
||||
expect(getButtonOpeningTag(loadingMarkup, "Generating...")).toContain(
|
||||
" disabled=\"\"",
|
||||
);
|
||||
expect(getButtonOpeningTag(promptMarkup, "Generate event")).not.toContain(
|
||||
" disabled=",
|
||||
);
|
||||
expect(getButtonOpeningTag(imageMarkup, "Generate event")).not.toContain(
|
||||
" disabled=",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("AI toolbar paste capture", () => {
|
||||
|
||||
Reference in New Issue
Block a user