From fe348126c8d2e588c9addd6aeccb13c9bbc10dc4 Mon Sep 17 00:00:00 2001 From: Nick Bernal Date: Sat, 28 Mar 2026 10:48:47 -0700 Subject: [PATCH] fix: include injected footnote lookup blocks in resolved layout --- .../presentation-editor/PresentationEditor.ts | 9 +- ...sentationEditor.footnotesPmMarkers.test.ts | 118 +++++++++++++++++- 2 files changed, 119 insertions(+), 8 deletions(-) diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts index 4224a7b7d3..c10f5c510b 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts @@ -4204,11 +4204,16 @@ export class PresentationEditor extends EventEmitter { layout.pageGap = this.#getEffectivePageGap(); (layout as Layout & { layoutEpoch?: number }).layoutEpoch = layoutEpoch; + // Include footnote-injected blocks (separators, footnote paragraphs) so + // resolveLayout can find them when resolving page fragments. + const resolveBlocks = extraBlocks ? [...blocksForLayout, ...extraBlocks] : blocksForLayout; + const resolveMeasures = extraMeasures ? [...measures, ...extraMeasures] : measures; + resolvedLayout = resolveLayout({ layout, flowMode: this.#layoutOptions.flowMode ?? 'paginated', - blocks: blocksForLayout, - measures, + blocks: resolveBlocks, + measures: resolveMeasures, }); headerLayouts = result.headers; diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts index d83ada97b1..2eb15147f9 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.footnotesPmMarkers.test.ts @@ -4,6 +4,15 @@ import { PresentationEditor } from '../PresentationEditor.js'; let capturedLayoutOptions: any; let capturedBlocksForLayout: any[] | undefined; +const { mockIncrementalLayout, mockResolveLayout } = vi.hoisted(() => ({ + mockIncrementalLayout: vi.fn(async (...args: any[]) => { + capturedLayoutOptions = args[3]; + capturedBlocksForLayout = args[2]; + return { layout: { pages: [] }, measures: [] }; + }), + mockResolveLayout: vi.fn(() => ({ version: 1, flowMode: 'paginated', pageGap: 0, pages: [] })), +})); + vi.mock('../../Editor', () => ({ Editor: vi.fn().mockImplementation(() => ({ on: vi.fn(), @@ -56,11 +65,7 @@ vi.mock('@superdoc/pm-adapter', async (importOriginal) => { }); vi.mock('@superdoc/layout-bridge', () => ({ - incrementalLayout: vi.fn(async (...args: any[]) => { - capturedLayoutOptions = args[3]; - capturedBlocksForLayout = args[2]; - return { layout: { pages: [] }, measures: [] }; - }), + incrementalLayout: mockIncrementalLayout, normalizeMargin: (value: number | undefined, fallback: number) => Number.isFinite(value) ? (value as number) : fallback, selectionToRects: vi.fn(() => []), @@ -105,7 +110,7 @@ vi.mock('@superdoc/painter-dom', () => ({ vi.mock('@superdoc/measuring-dom', () => ({ measureBlock: vi.fn(() => ({ width: 100, height: 100 })) })); vi.mock('@superdoc/layout-resolved', () => ({ - resolveLayout: vi.fn(() => ({ version: 1, flowMode: 'paginated', pageGap: 0, pages: [] })), + resolveLayout: mockResolveLayout, })); vi.mock('../../header-footer/HeaderFooterRegistry', () => ({ @@ -151,6 +156,14 @@ describe('PresentationEditor - footnote number marker PM position', () => { document.body.appendChild(container); capturedLayoutOptions = undefined; capturedBlocksForLayout = undefined; + mockIncrementalLayout.mockClear(); + mockResolveLayout.mockClear(); + mockIncrementalLayout.mockImplementation(async (...args: any[]) => { + capturedLayoutOptions = args[3]; + capturedBlocksForLayout = args[2]; + return { layout: { pages: [] }, measures: [] }; + }); + mockResolveLayout.mockImplementation(() => ({ version: 1, flowMode: 'paginated', pageGap: 0, pages: [] })); }); afterEach(() => { @@ -210,4 +223,97 @@ describe('PresentationEditor - footnote number marker PM position', () => { expect(firstRun?.pmStart).toBeUndefined(); expect(firstRun?.pmEnd).toBeUndefined(); }); + + it('passes footnote-injected lookup blocks to resolveLayout', async () => { + mockIncrementalLayout.mockImplementationOnce(async (...args: any[]) => { + capturedLayoutOptions = args[3]; + capturedBlocksForLayout = args[2]; + return { + layout: { + pageSize: { w: 612, h: 792 }, + pages: [ + { + number: 1, + size: { w: 612, h: 792 }, + fragments: [ + { + kind: 'drawing', + blockId: 'footnote-separator-page-1-col-0', + drawingKind: 'vectorShape', + x: 0, + y: 0, + width: 100, + height: 1, + geometry: { width: 100, height: 1 }, + scale: 1, + }, + { + kind: 'para', + blockId: 'footnote-body-1', + fromLine: 0, + toLine: 1, + x: 0, + y: 2, + width: 100, + }, + ], + }, + ], + }, + measures: [], + extraBlocks: [ + { kind: 'paragraph', id: 'footnote-body-1', runs: [{ kind: 'text', text: 'Body' }] }, + { + kind: 'drawing', + id: 'footnote-separator-page-1-col-0', + drawingKind: 'vectorShape', + geometry: { width: 100, height: 1 }, + shapeKind: 'rect', + fillColor: '#000000', + strokeColor: null, + strokeWidth: 0, + }, + ], + extraMeasures: [ + { + kind: 'paragraph', + lines: [ + { + fromRun: 0, + fromChar: 0, + toRun: 0, + toChar: 4, + width: 100, + ascent: 8, + descent: 2, + lineHeight: 10, + }, + ], + totalHeight: 10, + }, + { + kind: 'drawing', + drawingKind: 'vectorShape', + width: 100, + height: 1, + scale: 1, + naturalWidth: 100, + naturalHeight: 1, + geometry: { width: 100, height: 1 }, + }, + ], + }; + }); + + editor = new PresentationEditor({ element: container }); + await new Promise((r) => setTimeout(r, 100)); + + expect(mockResolveLayout).toHaveBeenCalled(); + const lastResolveInput = mockResolveLayout.mock.calls.at(-1)?.[0]; + expect(lastResolveInput).toBeTruthy(); + expect(lastResolveInput.blocks.map((block: { id: string }) => block.id)).toEqual( + expect.arrayContaining(['footnote-body-1', 'footnote-separator-page-1-col-0']), + ); + expect(lastResolveInput.measures).toHaveLength(lastResolveInput.blocks.length); + }); });