|
| 1 | +import { describe, it, expect } from 'vitest'; |
| 2 | +import { resolveHeaderFooterLayout } from './resolveHeaderFooter.js'; |
| 3 | +import type { FlowBlock, HeaderFooterLayout, Measure, ParaFragment, ResolvedFragmentItem } from '@superdoc/contracts'; |
| 4 | + |
| 5 | +describe('resolveHeaderFooterLayout', () => { |
| 6 | + it('resolves a header/footer with one paragraph fragment', () => { |
| 7 | + const paraFragment: ParaFragment = { |
| 8 | + kind: 'para', |
| 9 | + blockId: 'p1', |
| 10 | + fromLine: 0, |
| 11 | + toLine: 1, |
| 12 | + x: 72, |
| 13 | + y: 10, |
| 14 | + width: 468, |
| 15 | + }; |
| 16 | + const layout: HeaderFooterLayout = { |
| 17 | + height: 50, |
| 18 | + pages: [{ number: 1, fragments: [paraFragment] }], |
| 19 | + }; |
| 20 | + const blocks: FlowBlock[] = [{ kind: 'paragraph', id: 'p1', runs: [] }]; |
| 21 | + const measures: Measure[] = [ |
| 22 | + { |
| 23 | + kind: 'paragraph', |
| 24 | + lines: [{ fromRun: 0, fromChar: 0, toRun: 0, toChar: 5, width: 100, ascent: 10, descent: 3, lineHeight: 18 }], |
| 25 | + totalHeight: 18, |
| 26 | + }, |
| 27 | + ]; |
| 28 | + |
| 29 | + const result = resolveHeaderFooterLayout(layout, blocks, measures); |
| 30 | + expect(result.pages).toHaveLength(1); |
| 31 | + const item = result.pages[0].items[0] as ResolvedFragmentItem; |
| 32 | + expect(item.version).toBeDefined(); |
| 33 | + expect(item.block?.kind).toBe('paragraph'); |
| 34 | + expect(item.measure?.kind).toBe('paragraph'); |
| 35 | + }); |
| 36 | + |
| 37 | + it('preserves height, minY, maxY, renderHeight from input', () => { |
| 38 | + const layout: HeaderFooterLayout = { |
| 39 | + height: 100, |
| 40 | + minY: 5, |
| 41 | + maxY: 120, |
| 42 | + renderHeight: 115, |
| 43 | + pages: [], |
| 44 | + }; |
| 45 | + |
| 46 | + const result = resolveHeaderFooterLayout(layout, [], []); |
| 47 | + expect(result.height).toBe(100); |
| 48 | + expect(result.minY).toBe(5); |
| 49 | + expect(result.maxY).toBe(120); |
| 50 | + expect(result.renderHeight).toBe(115); |
| 51 | + }); |
| 52 | + |
| 53 | + it('preserves numberText on pages', () => { |
| 54 | + const layout: HeaderFooterLayout = { |
| 55 | + height: 50, |
| 56 | + pages: [ |
| 57 | + { number: 1, fragments: [], numberText: 'i' }, |
| 58 | + { number: 2, fragments: [], numberText: 'ii' }, |
| 59 | + ], |
| 60 | + }; |
| 61 | + |
| 62 | + const result = resolveHeaderFooterLayout(layout, [], []); |
| 63 | + expect(result.pages[0].numberText).toBe('i'); |
| 64 | + expect(result.pages[1].numberText).toBe('ii'); |
| 65 | + }); |
| 66 | + |
| 67 | + it('returns empty items array for empty fragments array', () => { |
| 68 | + const layout: HeaderFooterLayout = { |
| 69 | + height: 50, |
| 70 | + pages: [{ number: 1, fragments: [] }], |
| 71 | + }; |
| 72 | + |
| 73 | + const result = resolveHeaderFooterLayout(layout, [], []); |
| 74 | + expect(result.pages).toHaveLength(1); |
| 75 | + expect(result.pages[0].items).toEqual([]); |
| 76 | + }); |
| 77 | + |
| 78 | + it('leaves block/measure undefined when block entry is missing', () => { |
| 79 | + const paraFragment: ParaFragment = { |
| 80 | + kind: 'para', |
| 81 | + blockId: 'missing-id', |
| 82 | + fromLine: 0, |
| 83 | + toLine: 1, |
| 84 | + x: 0, |
| 85 | + y: 0, |
| 86 | + width: 100, |
| 87 | + }; |
| 88 | + const layout: HeaderFooterLayout = { |
| 89 | + height: 50, |
| 90 | + pages: [{ number: 1, fragments: [paraFragment] }], |
| 91 | + }; |
| 92 | + |
| 93 | + const result = resolveHeaderFooterLayout(layout, [], []); |
| 94 | + const item = result.pages[0].items[0] as ResolvedFragmentItem; |
| 95 | + expect(item.block).toBeUndefined(); |
| 96 | + expect(item.measure).toBeUndefined(); |
| 97 | + }); |
| 98 | + |
| 99 | + it('resolves each page against its own cloned block data', () => { |
| 100 | + const paraFragment: ParaFragment = { |
| 101 | + kind: 'para', |
| 102 | + blockId: 'page-token', |
| 103 | + fromLine: 0, |
| 104 | + toLine: 1, |
| 105 | + x: 0, |
| 106 | + y: 0, |
| 107 | + width: 120, |
| 108 | + }; |
| 109 | + const pageOneBlocks: FlowBlock[] = [ |
| 110 | + { |
| 111 | + kind: 'paragraph', |
| 112 | + id: 'page-token', |
| 113 | + runs: [ |
| 114 | + { text: 'Page ', fontFamily: 'Arial', fontSize: 16 }, |
| 115 | + { text: '1', token: 'pageNumber', fontFamily: 'Arial', fontSize: 16 }, |
| 116 | + ], |
| 117 | + }, |
| 118 | + ]; |
| 119 | + const pageTwoBlocks: FlowBlock[] = [ |
| 120 | + { |
| 121 | + kind: 'paragraph', |
| 122 | + id: 'page-token', |
| 123 | + runs: [ |
| 124 | + { text: 'Page ', fontFamily: 'Arial', fontSize: 16 }, |
| 125 | + { text: '2', token: 'pageNumber', fontFamily: 'Arial', fontSize: 16 }, |
| 126 | + ], |
| 127 | + }, |
| 128 | + ]; |
| 129 | + const makeMeasure = (text: string): Measure => ({ |
| 130 | + kind: 'paragraph', |
| 131 | + lines: [ |
| 132 | + { fromRun: 0, fromChar: 0, toRun: 1, toChar: text.length, width: 120, ascent: 10, descent: 3, lineHeight: 18 }, |
| 133 | + ], |
| 134 | + totalHeight: 18, |
| 135 | + }); |
| 136 | + const layout: HeaderFooterLayout = { |
| 137 | + height: 50, |
| 138 | + pages: [ |
| 139 | + { number: 1, fragments: [paraFragment], blocks: pageOneBlocks, measures: [makeMeasure('Page 1')] }, |
| 140 | + { number: 2, fragments: [paraFragment], blocks: pageTwoBlocks, measures: [makeMeasure('Page 2')] }, |
| 141 | + ], |
| 142 | + }; |
| 143 | + |
| 144 | + const result = resolveHeaderFooterLayout(layout, pageOneBlocks, [makeMeasure('Page 1')]); |
| 145 | + const firstItem = result.pages[0].items[0] as ResolvedFragmentItem; |
| 146 | + const secondItem = result.pages[1].items[0] as ResolvedFragmentItem; |
| 147 | + |
| 148 | + expect(firstItem.block?.kind).toBe('paragraph'); |
| 149 | + expect(secondItem.block?.kind).toBe('paragraph'); |
| 150 | + expect(firstItem.block?.runs[1]?.text).toBe('1'); |
| 151 | + expect(secondItem.block?.runs[1]?.text).toBe('2'); |
| 152 | + expect(firstItem.version).not.toBe(secondItem.version); |
| 153 | + }); |
| 154 | + |
| 155 | + it('uses document page indices for sparse header/footer pages', () => { |
| 156 | + const paraFragment: ParaFragment = { |
| 157 | + kind: 'para', |
| 158 | + blockId: 'p1', |
| 159 | + fromLine: 0, |
| 160 | + toLine: 1, |
| 161 | + x: 0, |
| 162 | + y: 0, |
| 163 | + width: 100, |
| 164 | + }; |
| 165 | + const layout: HeaderFooterLayout = { |
| 166 | + height: 50, |
| 167 | + pages: [ |
| 168 | + { number: 5, fragments: [paraFragment], numberText: '5' }, |
| 169 | + { number: 50, fragments: [paraFragment], numberText: '50' }, |
| 170 | + { number: 500, fragments: [paraFragment], numberText: '500' }, |
| 171 | + ], |
| 172 | + }; |
| 173 | + const blocks: FlowBlock[] = [{ kind: 'paragraph', id: 'p1', runs: [] }]; |
| 174 | + const measures: Measure[] = [ |
| 175 | + { |
| 176 | + kind: 'paragraph', |
| 177 | + lines: [{ fromRun: 0, fromChar: 0, toRun: 0, toChar: 0, width: 100, ascent: 10, descent: 3, lineHeight: 18 }], |
| 178 | + totalHeight: 18, |
| 179 | + }, |
| 180 | + ]; |
| 181 | + |
| 182 | + const result = resolveHeaderFooterLayout(layout, blocks, measures); |
| 183 | + |
| 184 | + expect((result.pages[0].items[0] as ResolvedFragmentItem).pageIndex).toBe(4); |
| 185 | + expect((result.pages[1].items[0] as ResolvedFragmentItem).pageIndex).toBe(49); |
| 186 | + expect((result.pages[2].items[0] as ResolvedFragmentItem).pageIndex).toBe(499); |
| 187 | + }); |
| 188 | +}); |
0 commit comments