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 { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
||||||
import { readFileSync } from "node:fs";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
|
||||||
const readToolbarSource = () => readFileSync("src/components/ai-toolbar.tsx", "utf8");
|
|
||||||
|
|
||||||
type AIToolbarProps = {
|
type AIToolbarProps = {
|
||||||
adminAiEnabled: boolean;
|
adminAiEnabled: boolean;
|
||||||
aiEnabled: boolean;
|
aiEnabled: boolean;
|
||||||
@@ -128,7 +125,12 @@ const renderToolbar = async (overrides: Partial<AIToolbarProps> = {}) => {
|
|||||||
|
|
||||||
const renderToolbarMarkup = async (
|
const renderToolbarMarkup = async (
|
||||||
overrides: Partial<AIToolbarProps> = {},
|
overrides: Partial<AIToolbarProps> = {},
|
||||||
|
{ isMobile = false }: { isMobile?: boolean } = {},
|
||||||
) => {
|
) => {
|
||||||
|
mock.module("@/hooks/use-mobile", () => ({
|
||||||
|
useIsMobile: () => isMobile,
|
||||||
|
}));
|
||||||
|
|
||||||
const { AIToolbar } = await import("@/components/ai-toolbar");
|
const { AIToolbar } = await import("@/components/ai-toolbar");
|
||||||
|
|
||||||
return renderToStaticMarkup(
|
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 (
|
const renderToolbarBoundary = async (
|
||||||
overrides: Partial<AIToolbarProps> = {},
|
overrides: Partial<AIToolbarProps> = {},
|
||||||
) => {
|
) => {
|
||||||
@@ -160,27 +178,29 @@ const renderToolbarBoundary = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("AI toolbar layout contracts", () => {
|
describe("AI toolbar layout contracts", () => {
|
||||||
test("desktop composer uses a 70/30 split driven by the isMobile branch", () => {
|
test("desktop composer renders a 70/30 split while mobile stays single-column", async () => {
|
||||||
const source = readToolbarSource();
|
const desktopMarkup = await renderToolbarMarkup();
|
||||||
|
const mobileMarkup = await renderToolbarMarkup({}, { isMobile: true });
|
||||||
|
|
||||||
expect(source).toContain("isMobile");
|
expect(desktopMarkup).toContain(
|
||||||
expect(source).toContain('? "grid gap-3"');
|
"grid gap-3 grid-cols-[minmax(0,0.7fr)_minmax(0,0.3fr)]",
|
||||||
expect(source).toContain(
|
|
||||||
': "grid gap-3 grid-cols-[minmax(0,0.7fr)_minmax(0,0.3fr)]"',
|
|
||||||
);
|
);
|
||||||
expect(source).not.toContain(
|
expect(desktopMarkup).not.toContain(
|
||||||
': "grid gap-3 grid-cols-[minmax(0,1fr)_minmax(0,1fr)]"',
|
"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", () => {
|
test("example prompts render as a masonry-style cluster below the textarea", async () => {
|
||||||
const source = readToolbarSource();
|
const markup = await renderToolbarMarkup();
|
||||||
|
|
||||||
expect(source).toContain("Try:");
|
expect(markup).toContain("Try:");
|
||||||
expect(source).toContain('className="columns-2 gap-2"');
|
expect(markup).toContain('class="columns-2 gap-2"');
|
||||||
expect(source).toContain('className="mb-2 break-inside-avoid"');
|
expect(markup).toContain('class="mb-2 break-inside-avoid"');
|
||||||
expect(source).toContain(
|
expect(markup).toContain(
|
||||||
'className="h-auto w-full justify-start text-left whitespace-normal rounded-2xl px-3 py-2 text-[11px] leading-relaxed"',
|
"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", () => {
|
test("attachment previews render in a stacked grid instead of a multi-column strip", async () => {
|
||||||
const source = readToolbarSource();
|
const markup = await renderToolbarMarkup({
|
||||||
const previewGridClassNames = [...source.matchAll(
|
imagePreviews: ["blob:first", "blob:second"],
|
||||||
/className="([^"]*\bmt-3\b[^"]*\bgrid\b[^"]*\bgap-2\b[^"]*)"/g,
|
});
|
||||||
|
const previewGridClassNames = [...markup.matchAll(
|
||||||
|
/class="([^"]*\bmt-3\b[^"]*\bgrid\b[^"]*\bgap-2\b[^"]*)"/g,
|
||||||
)].map(([, className]) => className);
|
)].map(([, className]) => className);
|
||||||
const multiColumnPreviewPattern =
|
const multiColumnPreviewPattern =
|
||||||
/(?:^|\s)(?:[a-z]+:)*(?:grid-cols-(?:\[[^\]]+\]|\S+)|grid-flow-col|auto-cols-(?:\[[^\]]+\]|\S+)|columns-(?:\[[^\]]+\]|\S+)|overflow-x-auto|flex-row)/;
|
/(?:^|\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).toHaveLength(1);
|
||||||
expect(previewGridClassNames[0]).not.toMatch(multiColumnPreviewPattern);
|
expect(previewGridClassNames[0]).not.toMatch(multiColumnPreviewPattern);
|
||||||
});
|
});
|
||||||
@@ -255,6 +279,43 @@ describe("AI toolbar state rendering", () => {
|
|||||||
expect(markup).toContain("Updated just now");
|
expect(markup).toContain("Updated just now");
|
||||||
expect(markup).toContain("Dismiss AI summary");
|
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", () => {
|
describe("AI toolbar paste capture", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user