mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 21:46:49 +00:00
27be764164
Left out of the commits they belong to by hand, not by intent: the mobile screen suites, the tripRepo cache-first read behind the startup-destination fix, and the MSW handlers the stricter unhandled-request setting needs.
183 lines
8.1 KiB
TypeScript
183 lines
8.1 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import type { BookElement, JourneyStats } from '@trek/shared'
|
|
import { bookPageSetupSchema } from '@trek/shared'
|
|
import { applyTemplate, templateFit, type TemplateEntry } from '../../../src/components/Studio/applyTemplate'
|
|
import type { SpreadTemplate } from '../../../src/components/Studio/bookTemplates.data'
|
|
|
|
/**
|
|
* Laying an entry onto a template somebody drew (#1973).
|
|
*
|
|
* The contract between the person drawing a template and the code using it is
|
|
* a division: what the entry can answer for gets filled, everything else is the
|
|
* design and is copied as drawn. These cases are that division, and the sizing
|
|
* that lets a template drawn on one trim size land on another.
|
|
*/
|
|
|
|
const page = bookPageSetupSchema.parse({ preset: 'square-210', pageWidth: 210, pageHeight: 210 })
|
|
const ctx = { page, locale: 'en', stats: null, dayLabel: 'DAY' }
|
|
|
|
/** Fractions of a page, the way the templates store them. */
|
|
const el = (over: Record<string, unknown>): BookElement => ({
|
|
id: 't', rotation: 0, opacity: 1, locked: false,
|
|
frame: { x: 0.1, y: 0.1, w: 0.5, h: 0.25 },
|
|
...over,
|
|
} as BookElement)
|
|
|
|
const template = (elements: BookElement[]): SpreadTemplate => ({
|
|
id: 'ref', background: '#faf8f4', elements,
|
|
})
|
|
|
|
const entry = (over: Partial<TemplateEntry> = {}): TemplateEntry => ({
|
|
id: 7, title: 'A day', story: 'What happened.', location: 'Berlin', date: '2026-07-08',
|
|
photos: [], lat: 52.52, lng: 13.4, country: 'DE', dayNumber: 3, dayCount: 10,
|
|
...over,
|
|
})
|
|
|
|
const titleEl = el({ kind: 'text', text: 'TITLE', size: 0.1, binding: { source: 'entry.title' } })
|
|
const storyEl = el({ kind: 'text', text: 'Lorem', size: 0.05, binding: { source: 'entry.story' } })
|
|
const frameEl = el({ kind: 'photo', photoId: null, fit: 'cover', focalX: 0.5, focalY: 0.5, radius: 0, filter: 'none', mask: null, frameStyle: 'none' })
|
|
const panelEl = el({ kind: 'shape', shape: 'rect', fill: '#11224f', gradient: 'none', stroke: null, strokeWidth: 0, strokeStyle: 'solid', radius: 0 })
|
|
|
|
describe('sizing', () => {
|
|
/* Drawn as fractions so a template built on a square lands on an A4 too. */
|
|
it('multiplies the frame back up to the page it is laid on', () => {
|
|
const spread = applyTemplate(template([panelEl]), entry(), ctx)
|
|
expect(spread.elements[0].frame).toEqual({ x: 21, y: 21, w: 105, h: 52.5 })
|
|
})
|
|
|
|
it('scales to a different trim size without being redrawn', () => {
|
|
const a4 = bookPageSetupSchema.parse({ preset: 'a4-landscape', pageWidth: 297, pageHeight: 210 })
|
|
const spread = applyTemplate(template([panelEl]), entry(), { ...ctx, page: a4 })
|
|
expect(spread.elements[0].frame.x).toBe(29.7)
|
|
expect(spread.elements[0].frame.w).toBe(148.5)
|
|
})
|
|
|
|
it('takes type size off the page height, so it scales with the sheet', () => {
|
|
const spread = applyTemplate(template([titleEl]), entry(), ctx)
|
|
expect((spread.elements[0] as { size: number }).size).toBe(21)
|
|
})
|
|
})
|
|
|
|
describe('what gets filled', () => {
|
|
it('puts the entry title where the template asked for one', () => {
|
|
const spread = applyTemplate(template([titleEl]), entry(), ctx)
|
|
expect((spread.elements[0] as { text: string }).text).toBe('A day')
|
|
})
|
|
|
|
it('puts the story in', () => {
|
|
const spread = applyTemplate(template([storyEl]), entry(), ctx)
|
|
expect((spread.elements[0] as { text: string }).text).toBe('What happened.')
|
|
})
|
|
|
|
it('takes the photographs in order', () => {
|
|
const photos = [
|
|
{ photoId: 11, width: null, height: null },
|
|
{ photoId: 22, width: null, height: null },
|
|
]
|
|
const spread = applyTemplate(template([frameEl, frameEl]), entry({ photos }), ctx)
|
|
expect(spread.elements.map(e => (e as { photoId: number | null }).photoId)).toEqual([11, 22])
|
|
})
|
|
|
|
it('leaves a frame empty when the entry runs out of pictures', () => {
|
|
const spread = applyTemplate(
|
|
template([frameEl, frameEl]),
|
|
entry({ photos: [{ photoId: 11, width: null, height: null }] }),
|
|
ctx,
|
|
)
|
|
expect((spread.elements[1] as { photoId: number | null }).photoId).toBeNull()
|
|
})
|
|
|
|
it('fills the day chip from the stop', () => {
|
|
const chip = el({ kind: 'badge', variant: 'day', text: 'DAY 1', sub: '', code: null, style: 'chip' })
|
|
const spread = applyTemplate(template([chip]), entry(), ctx)
|
|
expect((spread.elements[0] as { text: string }).text).toBe('DAY 3')
|
|
})
|
|
|
|
it('fills the coordinates', () => {
|
|
const mark = el({ kind: 'badge', variant: 'coords', text: '', sub: '', code: null, style: 'plain' })
|
|
const spread = applyTemplate(template([mark]), entry(), ctx)
|
|
expect((spread.elements[0] as { text: string }).text).toContain('52°')
|
|
expect((spread.elements[0] as { text: string }).text).toContain('13°')
|
|
})
|
|
|
|
/*
|
|
* A mark the entry cannot answer keeps what it was drawn with. "DAY 1" on an
|
|
* entry with no date reads as a placeholder, which is what it is.
|
|
*/
|
|
it('leaves a mark alone when the entry has nothing to put in it', () => {
|
|
const chip = el({ kind: 'badge', variant: 'day', text: 'DAY 1', sub: '', code: null, style: 'chip' })
|
|
const spread = applyTemplate(template([chip]), entry({ dayNumber: null }), ctx)
|
|
expect((spread.elements[0] as { text: string }).text).toBe('DAY 1')
|
|
})
|
|
|
|
/* Every other figure on the page follows the book language; so does this one. */
|
|
it('sets the distance in the book language', () => {
|
|
const mark = el({ kind: 'badge', variant: 'distance', text: '', sub: '', code: null, style: 'plain' })
|
|
const travelled = { distance: 1_189_000, countries: [] } as unknown as JourneyStats
|
|
const spread = applyTemplate(template([mark]), entry(), { ...ctx, locale: 'de', stats: travelled })
|
|
expect((spread.elements[0] as { text: string }).text).toBe('1.189 km')
|
|
})
|
|
|
|
it('belongs to the entry it was filled from', () => {
|
|
expect(applyTemplate(template([titleEl]), entry(), ctx).entryId).toBe(7)
|
|
})
|
|
|
|
it('keeps the paper colour the template was drawn on', () => {
|
|
expect(applyTemplate(template([panelEl]), entry(), ctx).background).toBe('#faf8f4')
|
|
})
|
|
})
|
|
|
|
describe('what is left alone', () => {
|
|
/* The design is the design: a panel stays where it was put. */
|
|
it('copies a decorative shape as drawn, colour and all', () => {
|
|
const spread = applyTemplate(template([panelEl]), entry(), ctx)
|
|
const shape = spread.elements[0] as { fill: string; kind: string }
|
|
expect(shape.kind).toBe('shape')
|
|
expect(shape.fill).toBe('#11224f')
|
|
})
|
|
|
|
it('gives every element a fresh id, so two spreads never share one', () => {
|
|
const a = applyTemplate(template([panelEl]), entry(), ctx)
|
|
const b = applyTemplate(template([panelEl]), entry(), ctx)
|
|
expect(a.elements[0].id).not.toBe(b.elements[0].id)
|
|
expect(a.id).not.toBe(b.id)
|
|
})
|
|
})
|
|
|
|
describe('choosing a template', () => {
|
|
/*
|
|
* A template is one *for an entry* when it uses one. The distinction has to
|
|
* come out of the drawing rather than a label: a summary spread of figures
|
|
* and country outlines reads perfectly well on its own, and dropping a day's
|
|
* photographs into it would be nonsense.
|
|
*/
|
|
it('refuses one that uses nothing from the entry', () => {
|
|
const summary = template([panelEl, el({ kind: 'text', text: 'SUMMARY', size: 0.1 })])
|
|
expect(templateFit(summary, entry())).toBe(-1)
|
|
})
|
|
|
|
it('refuses one that wants a story from an entry with none', () => {
|
|
expect(templateFit(template([storyEl]), entry({ story: null }))).toBe(-1)
|
|
})
|
|
|
|
/* Two empty frames on a page with one picture is a finished page with a hole. */
|
|
it('refuses one with more frames than the entry can fill', () => {
|
|
expect(templateFit(template([frameEl, frameEl, frameEl]), entry({ photos: [] }))).toBe(-1)
|
|
})
|
|
|
|
it('prefers the one whose frame count matches', () => {
|
|
const photos = [
|
|
{ photoId: 1, width: null, height: null },
|
|
{ photoId: 2, width: null, height: null },
|
|
]
|
|
const two = templateFit(template([frameEl, frameEl]), entry({ photos }))
|
|
const one = templateFit(template([frameEl]), entry({ photos }))
|
|
expect(two).toBeGreaterThan(one)
|
|
})
|
|
|
|
it('accepts more pictures than frames — the rest are parked', () => {
|
|
const photos = Array.from({ length: 5 }, (_, i) => ({ photoId: i, width: null, height: null }))
|
|
expect(templateFit(template([frameEl]), entry({ photos }))).toBeGreaterThanOrEqual(0)
|
|
})
|
|
})
|