diff --git a/__mocks__/papi-frontend-react.ts b/__mocks__/papi-frontend-react.ts index e7f99bb6..50e002da 100644 --- a/__mocks__/papi-frontend-react.ts +++ b/__mocks__/papi-frontend-react.ts @@ -10,6 +10,33 @@ */ const KNOWN_PROJECT_DATA_METHODS = new Set(['BookUSJ']); +/** Known method names on data providers accessed via {@link useData}. */ +const KNOWN_DATA_METHODS = new Set(['WebViewMenu']); + +/** + * Mock for `useData`. Returns an object whose known methods each return + * `[undefined, jest.fn(), false]`, matching the real hook's `[data, setter, isLoading]` tuple. + * Accessing an unknown method throws to catch misspelled provider keys in tests. + * + * @param _providerName - Ignored data provider name. + * @returns Proxy object exposing known data methods. + */ +const useData = jest.fn((_providerName: unknown) => + new Proxy( + {}, + { + get(_target, prop: string | symbol) { + if (typeof prop === 'string' && KNOWN_DATA_METHODS.has(prop)) { + return () => [undefined, jest.fn(), false]; + } + throw new Error( + `useData mock: unknown method "${String(prop)}". Add it to KNOWN_DATA_METHODS if intentional.`, + ); + }, + }, + ), +); + /** * Mock for `useProjectData`. Returns an object whose known methods each return * `[undefined, jest.fn(), false]`, matching the real hook's `[data, setter, isLoading]` tuple. @@ -74,6 +101,7 @@ const useRecentScriptureRefs = jest module.exports = { __esModule: true, + useData, useProjectData, useProjectSetting, useLocalizedStrings, diff --git a/__mocks__/papi-frontend.ts b/__mocks__/papi-frontend.ts index 009859da..f5070e57 100644 --- a/__mocks__/papi-frontend.ts +++ b/__mocks__/papi-frontend.ts @@ -10,8 +10,15 @@ const mockLogger = { warn: jest.fn(), }; +const mockPapi = { + menuData: { + dataProviderName: 'platform.menuDataServiceDataProvider', + }, +}; + module.exports = { __esModule: true, + default: mockPapi, logger: mockLogger, }; diff --git a/__mocks__/platform-bible-react.tsx b/__mocks__/platform-bible-react.tsx index ba354760..decbf0ca 100644 --- a/__mocks__/platform-bible-react.tsx +++ b/__mocks__/platform-bible-react.tsx @@ -1,12 +1,26 @@ /** * @file Jest mock for platform-bible-react. The real package ships ESM which Jest cannot parse * without extra transform configuration. This stub provides the subset used by extension - * components: `BookChapterControl`, `BOOK_CHAPTER_CONTROL_STRING_KEYS`, `TabToolbar`, and - * `ScrollGroupSelector`. + * components: `BookChapterControl`, `BOOK_CHAPTER_CONTROL_STRING_KEYS`, `TabToolbar`, + * `ScrollGroupSelector`, `Switch`, and `Label`. */ import type { ReactElement, ReactNode } from 'react'; +export interface MenuItemContainingCommand { + label: `%${string}%`; + command: `${string}.${string}`; + group: `${string}.${string}`; + order: number; + localizeNotes: string; + tooltip?: `%${string}%`; + searchTerms?: `%${string}%`; + iconPathBefore?: string; + iconPathAfter?: string; +} + +export type SelectMenuItemHandler = (selectedMenuItem: MenuItemContainingCommand) => void; + interface SerializedVerseRef { book: string; chapterNum: number; @@ -15,40 +29,120 @@ interface SerializedVerseRef { versificationStr?: string; } -export const BOOK_CHAPTER_CONTROL_STRING_KEYS: string[] = []; +/** Localization keys required by {@link BookChapterControl}. */ +export const BOOK_CHAPTER_CONTROL_STRING_KEYS = [ + '%scripture_section_ot_long%', + '%scripture_section_nt_long%', + '%scripture_section_dc_long%', + '%scripture_section_extra_long%', + '%history_recent%', + '%history_recentSearches_ariaLabel%', +] as const; +/** Sentinel menu item fired when the retokenize toolbar button is clicked in tests. */ +export const MOCK_RETOKENIZE_MENU_ITEM: MenuItemContainingCommand = { + label: '%interlinearizer_retokenize%', + command: 'interlinearizer.retokenize', + group: 'interlinearizer.projectData', + order: 1, + localizeNotes: '', +}; + +/** + * Stub toolbar that renders a fixed button per known project menu action, each firing a sentinel + * {@link MenuItemContainingCommand} so tests can trigger commands by clicking a stable + * `data-testid` without coupling to `projectMenuData` shape. + * + * @param props - Component props. + * @param props.startAreaChildren - Content rendered in the start slot. + * @param props.endAreaChildren - Content rendered in the end slot. + * @param props.onSelectProjectMenuItem - Called with a sentinel item when a project-menu button is + * clicked. + * @param props.onSelectViewInfoMenuItem - Called with a sentinel item when the view-info button is + * clicked. + * @returns A div with `data-testid="tab-toolbar"` containing the rendered buttons. + */ export function TabToolbar({ startAreaChildren, endAreaChildren, + onSelectProjectMenuItem, + onSelectViewInfoMenuItem, }: Readonly<{ className?: string; startAreaChildren?: ReactNode; + centerAreaChildren?: ReactNode; endAreaChildren?: ReactNode; - onSelectProjectMenuItem?: () => void; - onSelectViewInfoMenuItem?: () => void; + onSelectProjectMenuItem: SelectMenuItemHandler; + onSelectViewInfoMenuItem: SelectMenuItemHandler; + projectMenuData?: unknown; + tabViewMenuData?: unknown; + id?: string; + menuButtonIcon?: ReactNode; }>): ReactElement { return (
{startAreaChildren}
{endAreaChildren}
+ {onSelectProjectMenuItem && ( + + )} + {onSelectViewInfoMenuItem && ( + + )}
); } +/** + * Stub scroll-group selector rendered as a native `` element. + */ export function ScrollGroupSelector({ availableScrollGroupIds, scrollGroupId, onChangeScrollGroupId, }: Readonly<{ - availableScrollGroupIds?: (number | undefined)[]; - scrollGroupId?: number; - onChangeScrollGroupId?: (id: number | undefined) => void; + availableScrollGroupIds: (number | undefined)[]; + scrollGroupId: number | undefined; + onChangeScrollGroupId: (id: number | undefined) => void; + localizedStrings?: Record; + size?: 'default' | 'sm'; + className?: string; + id?: string; }>): ReactElement { return ( ` element. + */ export function Switch({ checked, disabled, @@ -105,6 +228,15 @@ export function Switch({ ); } +/** + * Stub label rendered as a native `