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 `