Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions __mocks__/lucide-react.tsx
Original file line number Diff line number Diff line change
@@ -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 <svg data-testid="trash-icon" {...props} />;
}

/** @param props - SVG props forwarded from the component. */
export function Info(props: Readonly<{ className?: string; size?: number }>): ReactElement {
return <svg data-testid="info-icon" {...props} />;
}
16 changes: 16 additions & 0 deletions __mocks__/papi-backend.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -24,6 +28,14 @@ const papi = {
dialogs: {
selectProject: mockSelectProject,
},
notifications: {
send: mockNotificationsSend,
},
storage: {
readUserData: mockReadUserData,
writeUserData: mockWriteUserData,
deleteUserData: mockDeleteUserData,
},
webViewProviders: {
registerWebViewProvider: mockRegisterWebViewProvider,
},
Expand All @@ -44,6 +56,10 @@ const defaultExport = {
__mockGetOpenWebViewDefinition: mockGetOpenWebViewDefinition,
__mockOnDidOpenWebView: mockOnDidOpenWebView,
__mockOnDidCloseWebView: mockOnDidCloseWebView,
__mockReadUserData: mockReadUserData,
__mockWriteUserData: mockWriteUserData,
__mockDeleteUserData: mockDeleteUserData,
__mockNotificationsSend: mockNotificationsSend,
__mockLogger: mockLogger,
};

Expand Down
15 changes: 15 additions & 0 deletions __mocks__/papi-frontend-react.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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. */
Expand Down
20 changes: 18 additions & 2 deletions __mocks__/papi-frontend.ts
Original file line number Diff line number Diff line change
@@ -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 = {
Expand All @@ -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,
};

Expand Down
154 changes: 146 additions & 8 deletions __mocks__/platform-bible-react.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 (
<div data-testid="tab-toolbar">
<div data-testid="tab-toolbar-start">{startAreaChildren}</div>
<div data-testid="tab-toolbar-end">{endAreaChildren}</div>
{onSelectProjectMenuItem && (
<button
type="button"
data-testid="tab-toolbar-project-menu"
onClick={() => onSelectProjectMenuItem(MOCK_CREATE_PROJECT_MENU_ITEM)}
>
Project menu
</button>
)}
{onSelectProjectMenuItem && (
<button
type="button"
data-testid="tab-toolbar-new-project"
onClick={() => onSelectProjectMenuItem(MOCK_NEW_PROJECT_MENU_ITEM)}
>
New project
</button>
)}
{onSelectProjectMenuItem && (
<button
type="button"
data-testid="tab-toolbar-view-project-info"
onClick={() => onSelectProjectMenuItem(MOCK_VIEW_PROJECT_INFO_MENU_ITEM)}
>
View project info
</button>
)}
{onSelectViewInfoMenuItem && (
<button
type="button"
data-testid="tab-toolbar-view-info-menu"
onClick={() =>
onSelectViewInfoMenuItem({
label: '%mock.viewInfo%',
command: 'mock.viewInfo',
group: 'mock.group',
order: 0,
localizeNotes: '',
})
}
>
View info menu
</button>
)}
</div>
);
}
Expand All @@ -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<string, string>;
size?: 'default' | 'sm' | 'lg' | 'icon';
className?: string;
id?: string;
}>): ReactElement {
return (
<select
data-testid="scroll-group-selector"
value={scrollGroupId ?? ''}
onChange={(e) => onChangeScrollGroupId?.(e.target.value === '' ? undefined : Number(e.target.value))}
onChange={(e) => onChangeScrollGroupId(e.target.value === '' ? undefined : Number(e.target.value))}
>
<option value="">—</option>
{availableScrollGroupIds?.map((id) => (
Expand All @@ -60,6 +164,38 @@ export function ScrollGroupSelector({
);
}

export function Button({
children,
onClick,
type,
className,
disabled,
variant: _variant,
size: _size,
'aria-label': ariaLabel,
}: Readonly<{
children?: ReactNode;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
className?: string;
disabled?: boolean;
variant?: 'default' | 'secondary' | 'destructive' | 'ghost' | 'outline' | 'link';
size?: 'default' | 'sm' | 'lg' | 'icon';
'aria-label'?: string;
}>): ReactElement {
return (
<button
type={type ?? 'button'}
onClick={onClick}
className={className}
aria-label={ariaLabel}
disabled={disabled}
>
{children}
</button>
);
}

export function BookChapterControl({
scrRef,
handleSubmit,
Expand All @@ -68,6 +204,8 @@ export function BookChapterControl({
scrRef: SerializedVerseRef;
handleSubmit: (ref: SerializedVerseRef) => void;
className?: string;
getActiveBookIds?: () => string[];
localizedBookNames?: Map<string, { localizedId: string; localizedName: string }>;
localizedStrings?: Record<string, string>;
recentSearches?: SerializedVerseRef[];
onAddRecentSearch?: (scrRef: SerializedVerseRef) => void;
Expand All @@ -76,7 +214,7 @@ export function BookChapterControl({
return (
<div data-testid="book-chapter-control">
{scrRef.book} {scrRef.chapterNum}:{scrRef.verseNum}
<button type="button" onClick={() => {handleSubmit(scrRef); onAddRecentSearch?.(scrRef);}}>
<button type="button" onClick={() => { handleSubmit(scrRef); onAddRecentSearch?.(scrRef); }}>
Submit reference
</button>
</div>
Expand Down
Loading
Loading