Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -494,7 +494,7 @@ export function ResourceTextPanel({
highlightedVerseElement?.classList.remove('highlighted');
};
// The rule wants `scrRef` itself, but this effect is keyed on the three fields that decide where
// to scroll. `useWebViewScrollGroupScrRef` hands back a fresh object whenever the scroll group
// to scroll. `useScrollGroupScrRef` hands back a fresh object whenever the scroll group
// publishes, including for a reference that did not change, so depending on the object would
// restart the settle loop on updates that cannot move the target.
// eslint-disable-next-line react-hooks/exhaustive-deps
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import type { Usj } from '@eten-tech-foundation/scripture-utilities';
import type { DblResourceData } from 'platform-bible-utils';
import type { WebViewProps } from '@papi/core';
import type { PickerResource } from './downloaded-resources.utils';

// ---------------------------------------------------------------------------
// Hoisted mocks — must be before any import that touches the component
Expand All @@ -19,6 +20,7 @@ const {
mockUseInstallDblResource,
mockUseProjectData,
mockFindCachedDblResource,
mockUseScrollGroupScrRef,
} = vi.hoisted(() => ({
mockUseEffectiveResourceReferenceList: vi.fn(),
mockUseDblResourceAutoInstall: vi.fn(),
Expand All @@ -27,6 +29,7 @@ const {
mockUseInstallDblResource: vi.fn(),
mockUseProjectData: vi.fn(),
mockFindCachedDblResource: vi.fn(),
mockUseScrollGroupScrRef: vi.fn(),
}));

// @papi/frontend — papi default export used for themes subscription and commands
Expand Down Expand Up @@ -75,6 +78,7 @@ vi.mock('@papi/frontend/react', () => ({
useSetting: vi.fn(() => ['simple', false]),
useDialogCallback: vi.fn(() => vi.fn()),
usePromise: vi.fn(() => [undefined, false]),
useScrollGroupScrRef: (...args: unknown[]) => mockUseScrollGroupScrRef(...args),
}));

// platform-bible-react — keep UI components real; stub hooks that hit runtime
Expand Down Expand Up @@ -193,10 +197,6 @@ function makeProps(
return [defaultValue, vi.fn()];
},
),
useWebViewScrollGroupScrRef: vi.fn(() => [
{ book: 'GEN', chapterNum: 1, verseNum: 1, versificationStr: 'English' },
vi.fn(),
]),
...overrides,
} as unknown as WebViewProps;
}
Expand Down Expand Up @@ -261,6 +261,10 @@ function resetPanelHooks() {
mockUseInstallDblResource.mockImplementation(() => vi.fn(async () => {}));
mockUseProjectData.mockReturnValue({ ChapterUSJ: vi.fn(() => [undefined, false]) });
mockFindCachedDblResource.mockReturnValue(undefined);
mockUseScrollGroupScrRef.mockReturnValue([
{ book: 'GEN', chapterNum: 1, verseNum: 1, versificationStr: 'English' },
vi.fn(),
]);
// Module-scoped, so it outlives `restoreAllMocks` and has to be cleared explicitly.
setUsjSpy.mockClear();
}
Expand Down Expand Up @@ -439,3 +443,71 @@ describe('ResourceTextPanel — install against a stale catalog', () => {
expect(mockUseProjectData).toHaveBeenLastCalledWith('platformScripture.USJ_Chapter', 'WEB1');
});
});

// ---------------------------------------------------------------------------
// Versification conversion frame
// ---------------------------------------------------------------------------

/**
* A picker row for a project-reference resource whose project is `projectId` — i.e. a resource that
* lives in a DIFFERENT project from the panel's own container project.
*/
function makeProjectRow(projectId: string | undefined): PickerResource {
return {
reference: { type: 'project', name: 'Vulgate', id: 'resource-reference-id' },
source: 'user',
isAdminLocked: false,
type: 'ScriptureResource',
installed: true,
projectId,
};
}

/** Renders the panel over `rows`, with the referenced list settled so selection can resolve. */
function renderWithRows(rows: PickerResource[]) {
mockUseEffectiveResourceReferenceList.mockReturnValue({
status: 'ready',
list: { dataVersion: '1.0.0', items: [] },
});
mockUseResourcePickerResources.mockReturnValue([rows, false]);

const ResourceTextPanel = getResourceTextPanel();
return render(<ResourceTextPanel {...makeProps()} />);
}

/** The conversion-frame argument of every `useScrollGroupScrRef` call made during a render. */
function conversionFrames(): unknown[] {
return mockUseScrollGroupScrRef.mock.calls.map((call) => call[2]);
}

// The panel's own `projectId` prop is the CONTAINER project whose reference list is shown; the
// chapter on screen comes from the selected resource's project, which may use a different
// versification. Converting into the container's frame would show the wrong verse and would stamp
// the container as the scroll group's source on a verse click. Asserted on the conversion-frame
// argument rather than on rendered text because the conversion itself lives in the platform hook,
// which is mocked here — what belongs to this consumer is which project it asks to convert into.
describe('ResourceTextPanel — versification conversion frame', () => {
it('follows the scroll group in the displayed resource’s versification, not the container project’s', () => {
renderWithRows([makeProjectRow('resource-project-id')]);

expect(mockUseScrollGroupScrRef).toHaveBeenCalled();
expect(conversionFrames()).toContain('resource-project-id');
expect(conversionFrames()).not.toContain('test-project-id');
});

it('passes no conversion frame before a resource resolves', () => {
renderWithRows([]);

expect(mockUseScrollGroupScrRef).toHaveBeenCalled();
conversionFrames().forEach((frame) => expect(frame).toBeUndefined());
});

// A row can be in the list with no local project yet (a DBL resource still installing). There is
// nothing to convert into, and passing the container project would reintroduce the wrong frame.
it('passes no conversion frame for a selected row that has no project yet', () => {
renderWithRows([makeProjectRow(undefined)]);

expect(mockUseScrollGroupScrRef).toHaveBeenCalled();
conversionFrames().forEach((frame) => expect(frame).toBeUndefined());
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
useProjectData,
useProjectDataProvider,
useProjectSetting,
useScrollGroupScrRef,
useSetting,
} from '@papi/frontend/react';
import { useTabIconSelection, type TabIconUrls } from 'platform-bible-react';
Expand Down Expand Up @@ -86,14 +87,12 @@ const RESOURCE_PICKER_OPTIONS = { includeDownloaded: true } as const;
globalThis.webViewComponent = function ResourceTextPanelWebView({
id: webViewId,
projectId,
scrollGroupScrRef,
updateWebViewDefinition,
useWebViewState,
useWebViewScrollGroupScrRef,
}: WebViewProps) {
const [localizedStrings] = useLocalizedStrings(ALL_STRING_KEYS);

const [scrRef, setScrRef] = useWebViewScrollGroupScrRef();

// #region Web view state

// resourceType is injected by the web view provider at open time
Expand Down Expand Up @@ -265,6 +264,29 @@ globalThis.webViewComponent = function ResourceTextPanelWebView({
// reference list is shown). `PickerResource` resolves it for every reference kind.
const resourceProjectId = selectedRef?.projectId;

// Follow the scroll group in the DISPLAYED RESOURCE's versification, not this panel's own
// (container) project's. This web view's definition `projectId` is the container project (it
// reads that project's reference list and text-connection settings), but it renders the selected
// resource — so passing the resource's id as the conversion project makes `scrRef` come back
// converted into the resource's versification and makes a verse click here stamp the resource as
// the scroll group's source project (other web views then convert FROM it). We call
// `useScrollGroupScrRef` directly (rather than the `useWebViewScrollGroupScrRef` prop) so we can
// pass that resource id instead of this web view's own `projectId`; the `scrollGroupScrRef` prop
// is kept live by the web-view host re-rendering the component on definition updates.
// `undefined` until a resource with a local project resolves — no conversion, and there is
// nothing to display yet anyway.
//
// This call must stay below the selection region: `resourceProjectId` is what it converts into.
const [scrRef, setScrRef] = useScrollGroupScrRef(
scrollGroupScrRef,
useCallback(
(newScrollGroupScrRef) =>
updateWebViewDefinition({ scrollGroupScrRef: newScrollGroupScrRef }),
[updateWebViewDefinition],
),
resourceProjectId,
);

// The catalog entry behind the selection, for the dynamic title's display name.
const dblMatch =
selectedRef && isDblResourceReference(selectedRef.reference)
Expand Down
Loading