import { act, type ReactNode } from "react"; import { createRoot, type Root } from "react-dom/client"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ActionBarCopy } from "./actionBar/ActionBarCopy"; import { ActionBarEdit } from "./actionBar/ActionBarEdit"; import { ActionBarFeedbackNegative } from "./actionBar/ActionBarFeedbackNegative"; import { ActionBarFeedbackPositive } from "./actionBar/ActionBarFeedbackPositive"; import { ActionBarReload } from "./actionBar/ActionBarReload"; import { AttachmentRemove } from "./attachment/AttachmentRemove"; import { BranchPickerNext } from "./branchPicker/BranchPickerNext"; import { BranchPickerPrevious } from "./branchPicker/BranchPickerPrevious"; import { ChainOfThoughtAccordionTrigger } from "./chainOfThought/ChainOfThoughtAccordionTrigger"; import { ComposerAddAttachment } from "./composer/ComposerAddAttachment"; import { ComposerCancel } from "./composer/ComposerCancel"; import { ComposerQuoteDismiss } from "./composer/ComposerQuoteDismiss"; import { ComposerSend } from "./composer/ComposerSend"; import { QueueItemRemove } from "./queueItem/QueueItemRemove"; import { QueueItemSteer } from "./queueItem/QueueItemSteer"; import { SuggestionTrigger } from "./suggestion/SuggestionTrigger"; import { ThreadSuggestion } from "./thread/ThreadSuggestion"; import { ThreadListNew } from "./threadList/ThreadListNew"; import { ThreadListItemArchive } from "./threadListItem/ThreadListItemArchive"; import { ThreadListItemDelete } from "./threadListItem/ThreadListItemDelete"; import { ThreadListItemTrigger } from "./threadListItem/ThreadListItemTrigger"; import { ThreadListItemUnarchive } from "./threadListItem/ThreadListItemUnarchive"; vi.mock("@assistant-ui/store", () => ({ useAui: () => ({ attachment: { remove: vi.fn() }, chainOfThought: { setCollapsed: vi.fn() }, composer: { setQuote: vi.fn() }, queueItem: { remove: vi.fn(), steer: vi.fn() }, }), useAuiState: ( selector: (s: { suggestion: { prompt: string }; chainOfThought: { collapsed: boolean }; threads: { newThreadId: string | null; mainThreadId: string }; threadListItem: { id: string }; }) => T, ) => selector({ suggestion: { prompt: "p" }, chainOfThought: { collapsed: false }, threads: { newThreadId: null, mainThreadId: "main" }, threadListItem: { id: "thread" }, }), })); vi.mock("@assistant-ui/core/react", () => ({ useActionBarCopy: () => ({ copy: vi.fn(), disabled: false, isCopied: false, }), useActionBarEdit: () => ({ edit: vi.fn(), disabled: false }), useActionBarFeedbackNegative: () => ({ submit: vi.fn(), isSubmitted: false, }), useActionBarFeedbackPositive: () => ({ submit: vi.fn(), isSubmitted: false, }), useActionBarReload: () => ({ reload: vi.fn(), disabled: false }), useBranchPickerNext: () => ({ next: vi.fn(), disabled: false }), useBranchPickerPrevious: () => ({ previous: vi.fn(), disabled: false }), useComposerAddAttachment: () => ({ disabled: false }), useComposerCancel: () => ({ cancel: vi.fn(), disabled: false }), useComposerSend: () => ({ send: vi.fn(), disabled: false }), useSuggestionTrigger: () => ({ trigger: vi.fn(), disabled: false }), useThreadListNew: () => ({ switchToNewThread: vi.fn() }), useThreadListItemArchive: () => ({ archive: vi.fn() }), useThreadListItemDelete: () => ({ delete: vi.fn() }), useThreadListItemTrigger: () => ({ switchTo: vi.fn() }), useThreadListItemUnarchive: () => ({ unarchive: vi.fn() }), })); (globalThis as Record).IS_REACT_ACT_ENVIRONMENT = true; declare global { interface ImportMeta { glob( pattern: string, options: { query: "?raw"; eager: true; import: "default" }, ): Record; } } type Extra = { accessibilityRole?: "link" | undefined }; const actionable: ReadonlyArray<{ name: string; render: (extra: Extra) => ReactNode; }> = [ { name: "ActionBarCopy", render: (e) => ( x ), }, { name: "ActionBarEdit", render: (e) => ( x ), }, { name: "ActionBarFeedbackNegative", render: (e) => ( x ), }, { name: "ActionBarFeedbackPositive", render: (e) => ( x ), }, { name: "ActionBarReload", render: (e) => ( x ), }, { name: "AttachmentRemove", render: (e) => ( x ), }, { name: "BranchPickerNext", render: (e) => ( x ), }, { name: "BranchPickerPrevious", render: (e) => ( x ), }, { name: "ChainOfThoughtAccordionTrigger", render: (e) => ( x ), }, { name: "ComposerAddAttachment", render: (e) => ( x ), }, { name: "ComposerCancel", render: (e) => ( x ), }, { name: "ComposerSend", render: (e) => ( x ), }, { name: "ComposerQuoteDismiss", render: (e) => ( x ), }, { name: "QueueItemRemove", render: (e) => ( x ), }, { name: "QueueItemSteer", render: (e) => ( x ), }, { name: "SuggestionTrigger", render: (e) => ( x ), }, { name: "ThreadSuggestion", render: (e) => ( x ), }, { name: "ThreadListNew", render: (e) => ( x ), }, { name: "ThreadListItemArchive", render: (e) => ( x ), }, { name: "ThreadListItemDelete", render: (e) => ( x ), }, { name: "ThreadListItemTrigger", render: (e) => ( x ), }, { name: "ThreadListItemUnarchive", render: (e) => ( x ), }, ]; describe("react-native primitive accessibility roles", () => { let container: HTMLDivElement; let root: Root; beforeEach(() => { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); }); afterEach(async () => { await act(async () => { root.unmount(); }); container.remove(); }); const mount = async (node: ReactNode) => { await act(async () => { root.render(node); }); const el = container.querySelector('[data-testid="t"]'); expect(el).not.toBeNull(); return el as HTMLElement; }; it("gives every Pressable primitive in the package a button role", () => { const wrappers = Object.entries( import.meta.glob("./**/*.tsx", { query: "?raw", eager: true, import: "default", }), ).filter( ([file, source]) => !file.endsWith(".test.tsx") && source.includes(" !source.includes('accessibilityRole="button"')) .map(([file]) => file), ).toEqual([]); expect( wrappers.map(([file]) => file.replace(/^.*\/|\.tsx$/g, "")).sort(), ).toEqual(actionable.map(({ name }) => name).sort()); }); for (const { name, render } of actionable) { it(`${name} defaults its accessibilityRole to button`, async () => { const el = await mount(render({})); expect(el.getAttribute("role")).toBe("button"); }); it(`${name} lets the caller override the role`, async () => { const el = await mount(render({ accessibilityRole: "link" })); expect(el.getAttribute("role")).toBe("link"); }); } });