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 (