diff --git a/__mocks__/lucide-react.tsx b/__mocks__/lucide-react.tsx new file mode 100644 index 0000000..24f491c --- /dev/null +++ b/__mocks__/lucide-react.tsx @@ -0,0 +1,15 @@ +/** + * @file Jest mock for lucide-react. Provides stub icon components used by extension components. + */ + +import type { ReactElement } from 'react'; + +/** @param props - SVG props forwarded from the component. */ +export function Trash2(props: Readonly<{ className?: string; size?: number }>): ReactElement { + return ; +} + +/** @param props - SVG props forwarded from the component. */ +export function Info(props: Readonly<{ className?: string; size?: number }>): ReactElement { + return ; +} diff --git a/__mocks__/papi-backend.ts b/__mocks__/papi-backend.ts index c8d3a9f..819078b 100644 --- a/__mocks__/papi-backend.ts +++ b/__mocks__/papi-backend.ts @@ -10,6 +10,10 @@ const mockSelectProject = jest.fn(); const mockGetOpenWebViewDefinition = jest.fn(); const mockOnDidOpenWebView = jest.fn(); const mockOnDidCloseWebView = jest.fn(); +const mockReadUserData = jest.fn(); +const mockWriteUserData = jest.fn(); +const mockDeleteUserData = jest.fn(); +const mockNotificationsSend = jest.fn(); const mockLogger = { debug: jest.fn(), error: jest.fn(), @@ -24,6 +28,14 @@ const papi = { dialogs: { selectProject: mockSelectProject, }, + notifications: { + send: mockNotificationsSend, + }, + storage: { + readUserData: mockReadUserData, + writeUserData: mockWriteUserData, + deleteUserData: mockDeleteUserData, + }, webViewProviders: { registerWebViewProvider: mockRegisterWebViewProvider, }, @@ -44,6 +56,10 @@ const defaultExport = { __mockGetOpenWebViewDefinition: mockGetOpenWebViewDefinition, __mockOnDidOpenWebView: mockOnDidOpenWebView, __mockOnDidCloseWebView: mockOnDidCloseWebView, + __mockReadUserData: mockReadUserData, + __mockWriteUserData: mockWriteUserData, + __mockDeleteUserData: mockDeleteUserData, + __mockNotificationsSend: mockNotificationsSend, __mockLogger: mockLogger, }; diff --git a/__mocks__/papi-frontend-react.ts b/__mocks__/papi-frontend-react.ts index 52c6e6c..23dc855 100644 --- a/__mocks__/papi-frontend-react.ts +++ b/__mocks__/papi-frontend-react.ts @@ -58,12 +58,27 @@ const useRecentScriptureRefs = jest .fn() .mockImplementation(() => ({ recentScriptureRefs: [], addRecentScriptureRef: jest.fn() })); +/** + * Mock for `useData`. Returns a `Proxy` whose property accesses each yield a function returning + * `[undefined, jest.fn(), false]`, matching the real hook's `[data, setter, isLoading]` tuple + * without requiring a live data provider. + */ +const useData = jest.fn(() => + new Proxy( + {}, + { + get: () => jest.fn().mockReturnValue([undefined, jest.fn(), false]), + }, + ), +); + module.exports = { __esModule: true, useProjectData, useProjectSetting, useLocalizedStrings, useRecentScriptureRefs, + useData, }; /** Marks this file as a module so top-level const/let are module-scoped. */ diff --git a/__mocks__/papi-frontend.ts b/__mocks__/papi-frontend.ts index 009859d..7c5c746 100644 --- a/__mocks__/papi-frontend.ts +++ b/__mocks__/papi-frontend.ts @@ -1,6 +1,6 @@ /** - * @file Jest mock for @papi/frontend. Provides a logger stub so WebView/frontend code can be - * unit-tested without loading the real Platform API. + * @file Jest mock for @papi/frontend. Provides a logger stub and a minimal papi object so + * WebView/frontend components can be unit-tested without loading the real Platform API. */ const mockLogger = { @@ -10,8 +10,24 @@ const mockLogger = { warn: jest.fn(), }; +const mockSendCommand = jest.fn(); +const mockNotificationsSend = jest.fn(); + +const papi = { + commands: { + sendCommand: mockSendCommand, + }, + notifications: { + send: mockNotificationsSend, + }, + menuData: { + dataProviderName: 'platform.menuDataServiceDataProvider', + }, +}; + module.exports = { __esModule: true, + default: papi, logger: mockLogger, }; diff --git a/__mocks__/platform-bible-react.tsx b/__mocks__/platform-bible-react.tsx index 0266fdd..c5655bc 100644 --- a/__mocks__/platform-bible-react.tsx +++ b/__mocks__/platform-bible-react.tsx @@ -7,6 +7,20 @@ 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,22 +29,108 @@ interface SerializedVerseRef { versificationStr?: string; } -export const BOOK_CHAPTER_CONTROL_STRING_KEYS: string[] = []; +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 passed by the mock toolbar when the select-project menu button is clicked. */ +export const MOCK_CREATE_PROJECT_MENU_ITEM: MenuItemContainingCommand = { + label: '%interlinearizer_menu_select_project%', + command: 'interlinearizer.createProject', + group: 'interlinearizer.project.actions', + order: 1, + localizeNotes: '', +}; + +/** Sentinel menu item passed by the mock toolbar when the new-project button is clicked. */ +export const MOCK_NEW_PROJECT_MENU_ITEM: MenuItemContainingCommand = { + label: '%interlinearizer_menu_new_project%', + command: 'interlinearizer.newProject', + group: 'interlinearizer.project.actions', + order: 2, + localizeNotes: '', +}; + +/** Sentinel menu item passed by the mock toolbar when the view-project-info button is clicked. */ +export const MOCK_VIEW_PROJECT_INFO_MENU_ITEM: MenuItemContainingCommand = { + label: '%interlinearizer_menu_view_project_info%', + command: 'interlinearizer.viewProjectInfo', + group: 'interlinearizer.project.actions', + order: 3, + localizeNotes: '', +}; + 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 && ( + + )} + {onSelectProjectMenuItem && ( + + )} + {onSelectProjectMenuItem && ( + + )} + {onSelectViewInfoMenuItem && ( + + )}
); } @@ -40,15 +140,19 @@ export function ScrollGroupSelector({ 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' | 'lg' | 'icon'; + className?: string; + id?: string; }>): ReactElement { return ( setName(e.target.value)} + placeholder={localizedStrings['%interlinearizer_modal_create_name_placeholder%']} + /> + +