import { afterEach, beforeEach, expect, mock } from "bun:test"; import * as React from "react"; import { renderToStaticMarkup } from "react-dom/server"; import type { CalendarEvent } from "@/lib/types"; export type AIToolbarProps = { adminAiEnabled: boolean; aiEnabled: boolean; isAuthenticated: boolean; isPending: boolean; aiPrompt: string; setAiPrompt: (prompt: string) => void; aiLoading: boolean; imagePreviews: string[]; onImagesSelect: (files: File[]) => void; onImageRemove: (index: number) => void; onAiCreate: () => void; onAiTemplateSelect: (prompt: string) => void; onAiSummarize: () => void; onSummaryDismiss: () => void; summary: string | null; summaryUpdated: string | null; events: CalendarEvent[]; }; const actualReact = React; const documentAddEventListener = mock(); const documentRemoveEventListener = mock(); let lastEffectCleanup: (() => void) | undefined; const registeredDocumentListeners = new Map< string, Array<{ listener: EventListener; options?: AddEventListenerOptions }> >(); export const createToolbarProps = ( overrides: Partial = {}, ): AIToolbarProps => ({ adminAiEnabled: true, aiEnabled: true, isAuthenticated: true, isPending: false, aiPrompt: "", setAiPrompt: () => {}, aiLoading: false, imagePreviews: [], onImagesSelect: () => {}, onImageRemove: () => {}, onAiCreate: () => {}, onAiTemplateSelect: () => {}, onAiSummarize: () => {}, onSummaryDismiss: () => {}, summary: null, summaryUpdated: null, events: [], ...overrides, }); export const registerAIToolbarEffectHooks = () => { beforeEach(() => { documentAddEventListener.mockClear(); documentRemoveEventListener.mockClear(); lastEffectCleanup = undefined; registeredDocumentListeners.clear(); globalThis.document = { addEventListener: documentAddEventListener, removeEventListener: documentRemoveEventListener, } as unknown as Document; documentAddEventListener.mockImplementation( ( type: string, listener: EventListener, options?: AddEventListenerOptions, ) => { const listeners = registeredDocumentListeners.get(type) ?? []; listeners.push({ listener, options }); registeredDocumentListeners.set(type, listeners); }, ); mock.module("react", () => ({ ...actualReact, useEffect: (effect: () => void | (() => void)) => { const cleanup = effect(); lastEffectCleanup = typeof cleanup === "function" ? cleanup : undefined; }, useRef: (initialValue: T) => ({ current: initialValue }), useState: (initialValue: T) => [initialValue, mock()], })); mock.module("@/hooks/use-mobile", () => ({ useIsMobile: () => false, })); }); afterEach(() => { delete (globalThis as { document?: Document }).document; delete (globalThis as { navigator?: Navigator }).navigator; lastEffectCleanup = undefined; mock.restore(); }); }; export const registerAIToolbarMarkupHooks = () => { beforeEach(() => { globalThis.document = { addEventListener: () => {}, removeEventListener: () => {}, } as unknown 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 ( overrides: Partial = {}, ) => { const { AIToolbar } = await import("@/components/ai-toolbar"); AIToolbar(createToolbarProps(overrides)); }; export const renderToolbarMarkup = async ( overrides: Partial = {}, { isMobile = false }: { isMobile?: boolean } = {}, ) => { mock.module("@/hooks/use-mobile", () => ({ useIsMobile: () => isMobile, })); const { AIToolbar } = await import("@/components/ai-toolbar"); return renderToStaticMarkup( actualReact.createElement(AIToolbar, createToolbarProps(overrides)), ); }; export const renderToolbarBoundary = async ( overrides: Partial = {}, ) => { let capturedTextareaProps: React.ComponentProps<"textarea"> | undefined; const imageTriggerOpen = mock(); mock.module("@/components/ui/textarea", () => ({ Textarea: (props: React.ComponentProps<"textarea">) => { capturedTextareaProps = props; return actualReact.createElement("textarea", props); }, })); mock.module("@/components/image-picker", () => ({ ImagePicker: ({ triggerRef, children, ...props }: React.ComponentProps<"button"> & { triggerRef?: { current: { open: () => void } | null }; }) => { if (triggerRef) { triggerRef.current = { open: imageTriggerOpen }; } return actualReact.createElement("button", props, children); }, })); const { AIToolbar } = await import("@/components/ai-toolbar"); renderToStaticMarkup( actualReact.createElement(AIToolbar, createToolbarProps(overrides)), ); expect(capturedTextareaProps).toBeDefined(); return { textareaProps: capturedTextareaProps!, imageTriggerOpen }; }; export const renderToolbarActionBoundary = async ( overrides: Partial = {}, ) => { 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 = ( type: string, predicate: (entry: { listener: EventListener; options?: AddEventListenerOptions; }) => boolean = () => true, ) => { const entry = registeredDocumentListeners.get(type)?.find(predicate); expect(entry).toBeDefined(); return entry!.listener; }; export const getDocumentListenerMocks = () => ({ addEventListener: documentAddEventListener, removeEventListener: documentRemoveEventListener, lastEffectCleanup, }); export const resetRegisteredDocumentListeners = () => { registeredDocumentListeners.clear(); }; export const createClipboardData = (files: File[]): DataTransfer => ({ files, items: files.map((file) => ({ kind: "file", type: file.type, getAsFile: () => file, })), } as unknown as DataTransfer); export const createTextareaKeydownEvent = ( overrides: Partial> = {}, ) => ({ key: "Enter", ctrlKey: false, metaKey: false, shiftKey: false, altKey: false, preventDefault: mock(), ...overrides, }) as unknown as React.KeyboardEvent; export const createDocumentKeydownEvent = ( overrides: Partial = {}, ) => ({ key: "v", ctrlKey: false, metaKey: false, shiftKey: false, altKey: false, target: { tagName: "DIV", isContentEditable: false } as unknown as EventTarget, ...overrides, }) as unknown as KeyboardEvent; export const setMockNavigatorClipboardRead = (read: () => Promise) => { (globalThis as { navigator?: Navigator }).navigator = { clipboard: { read }, } as unknown as Navigator; }; const escapeForRegex = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); export const getButtonOpeningTag = (markup: string, label: string) => { const buttonMatch = markup.match( new RegExp( `]*>(?:(?!<\\/button>)[\\s\\S])*?${escapeForRegex(label)}(?:(?!<\\/button>)[\\s\\S])*?<\\/button>`, ), ); const openingTagMatch = buttonMatch?.[0].match(/^]*>/); expect(openingTagMatch).toBeDefined(); return openingTagMatch![0]; }; const getClassAttributes = (markup: string) => [...markup.matchAll(/class="([^"]+)"/g)].map(([, className]) => className); export const getMatchingClassTokens = ( markup: string, predicate: (tokens: string[]) => boolean, ) => getClassAttributes(markup) .map((className) => className.split(/\s+/).filter(Boolean)) .filter(predicate);