Files
TREK/client/tests/unit/studio/pageSetup.test.tsx
T
Maurice 87c73e50fb feat(studio): give a book its first and its last page (#2317)
A new book was a cover, a spread and a back cover, so it opened straight
onto pages 2 and 3. A bound book does not: the inside of the cover is
blank, and the first thing facing the reader is a single right-hand page,
page 1. The same at the end, where a single left-hand page sits before the
back cover. Both are laid down now, empty, in a new book and in a laid-out
one, and they are fixed the way the covers are: an inner spread only ever
goes between them.

The page numbers follow. They were counted from a spread's position in the
array, which assumed the spread after the cover was the first, and three
places did that sum on their own. One function now counts along the book,
one page for a single leaf and two for a spread, so the sheet on screen and
the label in the print view and the folio on the page all say the same
number. A book with a first page starts at 1; a book from before the first
page existed has none and keeps the numbers it always had.
2026-09-11 11:01:15 +02:00

177 lines
7.3 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import type { BookPageSetup, BookSpread } from '@trek/shared'
import { bookPageSetupSchema, normalizeBookDocument } from '@trek/shared'
import { render } from '../../helpers/render'
import { SpreadView } from '../../../src/components/Studio/SpreadView'
import { PAGE_MAX_MM, PAGE_MIN_MM, PAGE_PRESETS, clampPageSize } from '../../../src/components/Studio/pagePresets'
import { relayoutSpread, type AutoInput } from '../../../src/components/Studio/autoLayout'
/**
* A free trim size, page numbers, and laying out again (#1973).
*/
type PageOver = Partial<Omit<BookPageSetup, 'pageNumbers'>> & {
pageNumbers?: Partial<BookPageSetup['pageNumbers']>
}
const page = (over: PageOver = {}): BookPageSetup =>
bookPageSetupSchema.parse({ preset: 'square-210', pageWidth: 210, pageHeight: 210, ...over })
const spread = (over: Partial<BookSpread> = {}): BookSpread => ({
id: 's1', role: 'inner', background: null, elements: [], parked: [], entryId: null, ...over,
})
describe('a custom trim size', () => {
it('is offered as a preset of its own', () => {
expect(PAGE_PRESETS.custom).toBeTruthy()
expect(PAGE_PRESETS.custom.id).toBe('custom')
})
it('is what the document contract already allowed', () => {
const parsed = page({ preset: 'custom', pageWidth: 240, pageHeight: 180 })
expect(parsed.preset).toBe('custom')
expect(parsed.pageWidth).toBe(240)
})
/*
* The floor is a passport-sized book and the ceiling is what a large press
* takes — past either, the editor is scaling to a smear rather than showing
* a page.
*/
it('holds a typed size inside what a press can print', () => {
expect(clampPageSize(10)).toBe(PAGE_MIN_MM)
expect(clampPageSize(9000)).toBe(PAGE_MAX_MM)
expect(clampPageSize(240)).toBe(240)
})
it('rounds to a tenth of a millimetre, not to whatever was typed', () => {
expect(clampPageSize(210.06)).toBe(210.1)
})
it('refuses nonsense rather than passing NaN into the document', () => {
expect(clampPageSize(Number.NaN)).toBe(PAGE_MIN_MM)
})
})
describe('page numbers', () => {
it('are off until asked for', () => {
expect(page().pageNumbers.show).toBe(false)
const { container } = render(<SpreadView spread={spread()} page={page()} folios={[2, 3]} />)
expect(container.textContent).toBe('')
})
it('number both pages of a spread, left then right', () => {
const { container } = render(
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true } })} folios={[2, 3]} />,
)
expect(container.textContent).toBe('23')
})
/*
* The numbers come from the caller (foliosOf, tested with bookSheets): the
* sheet draws whatever it is handed, so a spread deep in the book reads
* what the book says it is.
*/
it('draw the numbers they are handed, wherever the spread sits', () => {
const p = page({ pageNumbers: { show: true } })
const third = render(<SpreadView spread={spread()} page={p} folios={[6, 7]} />)
expect(third.container.textContent).toBe('67')
})
/* A folio on a cover is a mistake in every book ever bound. */
it('never appear on the cover or the back cover', () => {
const p = page({ pageNumbers: { show: true } })
for (const role of ['cover', 'back'] as const) {
const { container } = render(<SpreadView spread={spread({ role })} page={p} folios={[1]} />)
expect(container.textContent, role).toBe('')
}
})
/* The single first page is a right-hand leaf and the last a left-hand one (#2317). */
it('put one number on a first or last page, on the side it is bound', () => {
const p = page({ pageNumbers: { show: true, position: 'outer' } })
const first = render(<SpreadView spread={spread({ role: 'first' })} page={p} folios={[1]} />)
const last = render(<SpreadView spread={spread({ role: 'last' })} page={p} folios={[9]} />)
expect(first.container.textContent).toBe('1')
expect(last.container.textContent).toBe('9')
const box = (c: HTMLElement) => (c.firstElementChild!.children[0]) as HTMLElement
// Outer on a right-hand page is the right edge; on a left-hand page the left.
expect(box(first.container).style.textAlign).toBe('right')
expect(box(last.container).style.textAlign).toBe('left')
})
it('draw nothing for a numbered page handed no numbers', () => {
const p = page({ pageNumbers: { show: true } })
const { container } = render(<SpreadView spread={spread()} page={p} folios={[]} />)
expect(container.textContent).toBe('')
})
it('sit against the cut edge on outer, and in the gutter on inner', () => {
const outer = render(
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true, position: 'outer' } })} folios={[2, 3]} />,
)
const inner = render(
<SpreadView spread={spread()} page={page({ pageNumbers: { show: true, position: 'inner' } })} folios={[2, 3]} />,
)
const leftOf = (c: HTMLElement) =>
parseFloat(((c.firstElementChild!.children[0]) as HTMLElement).style.left)
// Outer puts the left page's number at the margin; inner pushes it in.
expect(leftOf(outer.container)).toBeLessThan(leftOf(inner.container))
})
it('survive a round trip through the document contract', () => {
const doc = normalizeBookDocument({
version: 1, title: 'T',
page: { preset: 'custom', pageWidth: 240, pageHeight: 180, pageNumbers: { show: true, startAt: 5 } },
spreads: [],
})
expect(doc.page.pageNumbers.show).toBe(true)
expect(doc.page.pageNumbers.startAt).toBe(5)
expect(doc.page.pageWidth).toBe(240)
})
it('default in for a document written before they existed', () => {
const doc = normalizeBookDocument({
version: 1, title: 'T',
page: { preset: 'square-210', pageWidth: 210, pageHeight: 210 },
spreads: [],
})
expect(doc.page.pageNumbers.show).toBe(false)
expect(doc.page.pageNumbers.position).toBe('outer')
})
})
describe('laying a spread out again', () => {
const input: AutoInput = {
locale: 'en', title: 'T', subtitle: null, coverPhotoId: null,
entries: [{ id: 7, title: 'A day', story: 'Words.', location: 'Here', date: '2026-06-02', photos: [] }],
page: page(), stats: null, stationsLabel: 'Stations', dayLabel: 'DAY', summaryLabel: 'Trip summary', countriesLabel: 'Countries',
}
it('rebuilds the spread from the entry it came from', () => {
const next = relayoutSpread(spread({ entryId: 7, elements: [] }), input)
expect(next).not.toBeNull()
expect(next!.entryId).toBe(7)
expect(next!.elements.length).toBeGreaterThan(0)
})
/*
* Keeping the id is what makes it read as the same page rearranged: the rail
* does not scroll and the selection does not jump to a different card.
*/
it('keeps the spread id, so the rail does not jump', () => {
const next = relayoutSpread(spread({ id: 'keep-me', entryId: 7 }), input)
expect(next!.id).toBe('keep-me')
})
it('refuses a spread that did not come from an entry', () => {
expect(relayoutSpread(spread({ entryId: null }), input)).toBeNull()
expect(relayoutSpread(spread({ role: 'cover', entryId: 7 }), input)).toBeNull()
expect(relayoutSpread(spread({ role: 'back', entryId: 7 }), input)).toBeNull()
})
it('refuses when the entry is gone from the journey', () => {
expect(relayoutSpread(spread({ entryId: 999 }), input)).toBeNull()
})
})