test(ai-toolbar): isolate split suites and action coverage
This commit is contained in:
@@ -1,11 +1,11 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
getMatchingClassTokens,
|
getMatchingClassTokens,
|
||||||
registerAIToolbarTestHooks,
|
registerAIToolbarMarkupHooks,
|
||||||
renderToolbarMarkup,
|
renderToolbarMarkup,
|
||||||
} from "./helpers/ai-toolbar-test-helpers";
|
} from "./helpers/ai-toolbar-test-helpers";
|
||||||
|
|
||||||
registerAIToolbarTestHooks();
|
registerAIToolbarMarkupHooks();
|
||||||
|
|
||||||
describe("AI toolbar layout contracts", () => {
|
describe("AI toolbar layout contracts", () => {
|
||||||
test("desktop composer uses a dedicated multi-column branch while mobile stays single-column", async () => {
|
test("desktop composer uses a dedicated multi-column branch while mobile stays single-column", async () => {
|
||||||
|
|||||||
@@ -6,13 +6,13 @@ import {
|
|||||||
createDocumentKeydownEvent,
|
createDocumentKeydownEvent,
|
||||||
getDocumentListener,
|
getDocumentListener,
|
||||||
getDocumentListenerMocks,
|
getDocumentListenerMocks,
|
||||||
registerAIToolbarTestHooks,
|
registerAIToolbarEffectHooks,
|
||||||
resetRegisteredDocumentListeners,
|
resetRegisteredDocumentListeners,
|
||||||
renderToolbar,
|
renderToolbar,
|
||||||
renderToolbarBoundary,
|
renderToolbarBoundary,
|
||||||
} from "./helpers/ai-toolbar-test-helpers";
|
} from "./helpers/ai-toolbar-test-helpers";
|
||||||
|
|
||||||
registerAIToolbarTestHooks();
|
registerAIToolbarEffectHooks();
|
||||||
|
|
||||||
describe("AI toolbar paste capture", () => {
|
describe("AI toolbar paste capture", () => {
|
||||||
test("textarea paste path forwards clipboard images to onImagesSelect through the component boundary", async () => {
|
test("textarea paste path forwards clipboard images to onImagesSelect through the component boundary", async () => {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { describe, expect, mock, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
createTextareaKeydownEvent,
|
createTextareaKeydownEvent,
|
||||||
registerAIToolbarTestHooks,
|
registerAIToolbarMarkupHooks,
|
||||||
renderToolbarBoundary,
|
renderToolbarBoundary,
|
||||||
} from "./helpers/ai-toolbar-test-helpers";
|
} from "./helpers/ai-toolbar-test-helpers";
|
||||||
|
|
||||||
registerAIToolbarTestHooks();
|
registerAIToolbarMarkupHooks();
|
||||||
|
|
||||||
describe("AI toolbar keyboard shortcuts", () => {
|
describe("AI toolbar keyboard shortcuts", () => {
|
||||||
test("Ctrl+Enter triggers AI generation when the prompt has content", async () => {
|
test("Ctrl+Enter triggers AI generation when the prompt has content", async () => {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { describe, expect, test } from "bun:test";
|
import { describe, expect, mock, test } from "bun:test";
|
||||||
import {
|
import {
|
||||||
getButtonOpeningTag,
|
getButtonOpeningTag,
|
||||||
registerAIToolbarTestHooks,
|
renderToolbarActionBoundary,
|
||||||
|
registerAIToolbarMarkupHooks,
|
||||||
renderToolbarMarkup,
|
renderToolbarMarkup,
|
||||||
} from "./helpers/ai-toolbar-test-helpers";
|
} from "./helpers/ai-toolbar-test-helpers";
|
||||||
|
|
||||||
registerAIToolbarTestHooks();
|
registerAIToolbarMarkupHooks();
|
||||||
|
|
||||||
describe("AI toolbar state rendering", () => {
|
describe("AI toolbar state rendering", () => {
|
||||||
test("unauthenticated users see the sign-in-required callout", async () => {
|
test("unauthenticated users see the sign-in-required callout", async () => {
|
||||||
@@ -54,6 +55,14 @@ describe("AI toolbar state rendering", () => {
|
|||||||
expect(markup).toContain("Dismiss AI summary");
|
expect(markup).toContain("Dismiss AI summary");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("pending state renders loading skeletons instead of the live composer", async () => {
|
||||||
|
const markup = await renderToolbarMarkup({ isPending: true });
|
||||||
|
|
||||||
|
expect(markup).toContain("h-[160px]");
|
||||||
|
expect(markup).toContain("h-12");
|
||||||
|
expect(markup).not.toContain("Type or paste event details...");
|
||||||
|
});
|
||||||
|
|
||||||
test("Summarize only appears when there are events to summarize", async () => {
|
test("Summarize only appears when there are events to summarize", async () => {
|
||||||
const emptyMarkup = await renderToolbarMarkup({ events: [] });
|
const emptyMarkup = await renderToolbarMarkup({ events: [] });
|
||||||
const populatedMarkup = await renderToolbarMarkup({
|
const populatedMarkup = await renderToolbarMarkup({
|
||||||
@@ -98,4 +107,62 @@ describe("AI toolbar state rendering", () => {
|
|||||||
expect(markup).not.toContain("Type or paste event details...");
|
expect(markup).not.toContain("Type or paste event details...");
|
||||||
expect(markup).not.toContain("Generate event");
|
expect(markup).not.toContain("Generate event");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("example prompt buttons call onAiTemplateSelect with the selected prompt", async () => {
|
||||||
|
const onAiTemplateSelectMock = mock();
|
||||||
|
const actions = await renderToolbarActionBoundary({
|
||||||
|
onAiTemplateSelect: onAiTemplateSelectMock,
|
||||||
|
});
|
||||||
|
const promptButton = actions.getButtonByLabel("Lunch with Maya next Thursday");
|
||||||
|
|
||||||
|
promptButton.onClick?.({} as never);
|
||||||
|
|
||||||
|
expect(onAiTemplateSelectMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onAiTemplateSelectMock).toHaveBeenCalledWith(
|
||||||
|
"Lunch with Maya next Thursday at 12:30pm at Toma, remind me 30 minutes before.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Summarize button calls onAiSummarize when events are present", async () => {
|
||||||
|
const onAiSummarizeMock = mock();
|
||||||
|
const actions = await renderToolbarActionBoundary({
|
||||||
|
events: [{ id: "event-1" }],
|
||||||
|
onAiSummarize: onAiSummarizeMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
actions
|
||||||
|
.getButtonByLabel("Summarize")
|
||||||
|
.onClick?.({} as never);
|
||||||
|
|
||||||
|
expect(onAiSummarizeMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("summary dismiss button calls onSummaryDismiss", async () => {
|
||||||
|
const onSummaryDismissMock = mock();
|
||||||
|
const actions = await renderToolbarActionBoundary({
|
||||||
|
summary: "Three events need attention.",
|
||||||
|
onSummaryDismiss: onSummaryDismissMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
actions
|
||||||
|
.getButtonByAriaLabel("Dismiss AI summary")
|
||||||
|
.onClick?.({} as never);
|
||||||
|
|
||||||
|
expect(onSummaryDismissMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("remove image button calls onImageRemove with the preview index", async () => {
|
||||||
|
const onImageRemoveMock = mock();
|
||||||
|
const actions = await renderToolbarActionBoundary({
|
||||||
|
imagePreviews: ["blob:first", "blob:second"],
|
||||||
|
onImageRemove: onImageRemoveMock,
|
||||||
|
});
|
||||||
|
|
||||||
|
actions
|
||||||
|
.getButtonByAriaLabel("Remove image 2")
|
||||||
|
.onClick?.({} as never);
|
||||||
|
|
||||||
|
expect(onImageRemoveMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onImageRemoveMock).toHaveBeenCalledWith(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const createToolbarProps = (
|
|||||||
...overrides,
|
...overrides,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const registerAIToolbarTestHooks = () => {
|
export const registerAIToolbarEffectHooks = () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
documentAddEventListener.mockClear();
|
documentAddEventListener.mockClear();
|
||||||
documentRemoveEventListener.mockClear();
|
documentRemoveEventListener.mockClear();
|
||||||
@@ -97,6 +97,26 @@ export const registerAIToolbarTestHooks = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const registerAIToolbarMarkupHooks = () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
globalThis.document = {
|
||||||
|
addEventListener: () => {},
|
||||||
|
removeEventListener: () => {},
|
||||||
|
} as Document;
|
||||||
|
mock.module("@/hooks/use-mobile", () => ({
|
||||||
|
useIsMobile: () => false,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete (globalThis as { document?: Document }).document;
|
||||||
|
delete (globalThis as { navigator?: Navigator }).navigator;
|
||||||
|
mock.restore();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerAIToolbarTestHooks = registerAIToolbarEffectHooks;
|
||||||
|
|
||||||
export const renderToolbar = async (
|
export const renderToolbar = async (
|
||||||
overrides: Partial<AIToolbarProps> = {},
|
overrides: Partial<AIToolbarProps> = {},
|
||||||
) => {
|
) => {
|
||||||
@@ -158,6 +178,52 @@ export const renderToolbarBoundary = async (
|
|||||||
return { textareaProps: capturedTextareaProps!, imageTriggerOpen };
|
return { textareaProps: capturedTextareaProps!, imageTriggerOpen };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const renderToolbarActionBoundary = async (
|
||||||
|
overrides: Partial<AIToolbarProps> = {},
|
||||||
|
) => {
|
||||||
|
type CapturedButton = React.ComponentProps<"button"> & {
|
||||||
|
label: string;
|
||||||
|
ariaLabel?: string;
|
||||||
|
};
|
||||||
|
const buttons: CapturedButton[] = [];
|
||||||
|
|
||||||
|
mock.module("@/components/ui/button", () => ({
|
||||||
|
Button: ({ children, ...props }: React.ComponentProps<"button">) => {
|
||||||
|
const label = renderToStaticMarkup(
|
||||||
|
actualReact.createElement(actualReact.Fragment, null, children),
|
||||||
|
)
|
||||||
|
.replace(/<[^>]+>/g, "")
|
||||||
|
.trim();
|
||||||
|
buttons.push({
|
||||||
|
...props,
|
||||||
|
label,
|
||||||
|
ariaLabel: props["aria-label"] as string | undefined,
|
||||||
|
});
|
||||||
|
return actualReact.createElement("button", props, children);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const { AIToolbar } = await import("@/components/ai-toolbar");
|
||||||
|
|
||||||
|
renderToStaticMarkup(
|
||||||
|
actualReact.createElement(AIToolbar, createToolbarProps(overrides)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
buttons,
|
||||||
|
getButtonByLabel: (label: string) => {
|
||||||
|
const button = buttons.find((entry) => entry.label.includes(label));
|
||||||
|
expect(button).toBeDefined();
|
||||||
|
return button!;
|
||||||
|
},
|
||||||
|
getButtonByAriaLabel: (ariaLabel: string) => {
|
||||||
|
const button = buttons.find((entry) => entry.ariaLabel === ariaLabel);
|
||||||
|
expect(button).toBeDefined();
|
||||||
|
return button!;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const getDocumentListener = (
|
export const getDocumentListener = (
|
||||||
type: string,
|
type: string,
|
||||||
predicate: (entry: {
|
predicate: (entry: {
|
||||||
|
|||||||
Reference in New Issue
Block a user