import { describe, it, expect } from 'vitest' import type { BookElement, BookPageSetup, BookSpread } from '@trek/shared' import { bookListElementSchema, normalizeBookDocument } from '@trek/shared' import { render } from '../../helpers/render' import { SpreadView } from '../../../src/components/Studio/SpreadView' import { bookPageSetupSchema } from '@trek/shared' /** * Mood, weather and pros & cons (#1973). * * These come from the journal entry rather than from the trip, and they are the * part of an entry a photo cannot carry. What is checked here is that they draw * from the entry's own vocabulary — the same keys, icons and palette the journey * page uses — rather than from a second copy that would drift. */ const page: BookPageSetup = bookPageSetupSchema.parse({ preset: 'square-210', pageWidth: 210, pageHeight: 210, bleed: 3, safe: 5, }) const typeset = { font: 'sans' as const, color: '#1a1a1a', accent: '#c2410c', textScale: 1, stale: false } const common = { rotation: 0, opacity: 1, locked: false, frame: { x: 10, y: 20, w: 90, h: 40 } } function draw(elements: BookElement[]) { const spread: BookSpread = { id: 's1', role: 'inner', background: null, elements, parked: [], entryId: null, } return render() } const badge = (over: Record): BookElement => ({ ...common, ...typeset, id: 'b1', kind: 'badge', variant: 'mood', text: '', sub: '', code: null, style: 'plain', ...over, } as unknown as BookElement) const list = (over: Record = {}): BookElement => ({ ...common, ...typeset, id: 'l1', kind: 'list', items: [ { text: 'Warm all week', tone: 'pro' }, { text: 'Ferry was late', tone: 'con' }, ], layout: 'columns', showMarks: true, proLabel: 'Pros', conLabel: 'Cons', ...over, } as unknown as BookElement) describe('the mood mark', () => { it('draws the icon the journey page uses for that mood', () => { const { container } = draw([badge({ variant: 'mood', code: 'good', text: 'Good' })]) expect(container.querySelector('svg')).not.toBeNull() expect(container.textContent).toContain('Good') }) /* * The palette comes from MOOD_CONFIG rather than from the element's accent: * a mood has a colour of its own in TREK, and a book that ignored it would * disagree with the page the entry was written on. */ it('takes its colour from the mood, not from the element accent', () => { const { container } = draw([badge({ variant: 'mood', code: 'good', text: 'Good' })]) const icon = container.querySelector('svg')! expect(icon.getAttribute('stroke')).toBe('#B45309') }) it('draws nothing for a mood key TREK does not know', () => { const { container } = draw([badge({ variant: 'mood', code: 'elated', text: 'Elated' })]) expect(container.querySelector('svg')).toBeNull() // The words still print — an unknown key is not a reason to lose the label. expect(container.textContent).toContain('Elated') }) }) describe('the weather mark', () => { it('draws the icon for that weather', () => { const { container } = draw([badge({ variant: 'weather', code: 'cloudy', text: 'Cloudy' })]) expect(container.querySelector('svg')).not.toBeNull() expect(container.textContent).toContain('Cloudy') }) it('uses the element accent, since weather has no palette of its own', () => { const { container } = draw([badge({ variant: 'weather', code: 'sunny', text: 'Sunny' })]) expect(container.querySelector('svg')!.getAttribute('stroke')).toBe('#c2410c') }) }) describe('the pros and cons list', () => { it('prints both columns with their headings', () => { const { container } = draw([list()]) expect(container.textContent).toContain('Warm all week') expect(container.textContent).toContain('Ferry was late') expect(container.textContent).toContain('Pros') expect(container.textContent).toContain('Cons') }) it('marks each line, a plus against a dash', () => { const { container } = draw([list()]) expect(container.textContent).toContain('+') // An en dash, not a hyphen — at 2mm a hyphen reads as a speck. expect(container.textContent).toContain('–') }) it('drops the marks when they are switched off', () => { const { container } = draw([list({ showMarks: false })]) expect(container.textContent).toContain('Warm all week') expect(container.textContent).not.toContain('+') }) it('prints a one-sided list without an empty second column', () => { const { container } = draw([list({ items: [{ text: 'Only good things', tone: 'pro' }], conLabel: '', })]) expect(container.textContent).toContain('Only good things') expect(container.textContent).not.toContain('Cons') }) it('runs as one column when stacked', () => { const { container } = draw([list({ layout: 'stacked' })]) expect(container.textContent).toContain('Warm all week') expect(container.textContent).toContain('Ferry was late') }) it('survives an empty list rather than failing to draw', () => { const { container } = draw([list({ items: [], proLabel: '', conLabel: '' })]) expect(container.querySelector('div')).not.toBeNull() }) }) describe('the list contract', () => { it('defaults to an empty two-column list with marks', () => { const parsed = bookListElementSchema.parse({ id: 'l1', kind: 'list', frame: { x: 0, y: 0, w: 10, h: 10 }, }) expect(parsed.items).toEqual([]) expect(parsed.layout).toBe('columns') expect(parsed.showMarks).toBe(true) }) it('defaults an item with no tone to plain', () => { const parsed = bookListElementSchema.parse({ id: 'l1', kind: 'list', frame: { x: 0, y: 0, w: 10, h: 10 }, items: [{ text: 'something' }], }) expect(parsed.items[0].tone).toBe('plain') }) /* * The rule the whole format rests on: a document written before these * elements existed still opens, and one written with them survives a round * trip through storage. */ it('round-trips inside a document', () => { const doc = normalizeBookDocument({ version: 1, title: 'T', page: { preset: 'square-210', pageWidth: 210, pageHeight: 210, bleed: 3, safe: 5 }, spreads: [{ id: 's1', role: 'inner', entryId: null, background: null, parked: [], elements: [{ id: 'l1', kind: 'list', frame: { x: 1, y: 2, w: 90, h: 40 }, items: [{ text: 'kept', tone: 'con' }], proLabel: 'P', conLabel: 'C', }], }], }) const el = doc.spreads[0].elements[0] expect(el.kind).toBe('list') expect(el.kind === 'list' && el.items[0]).toEqual({ text: 'kept', tone: 'con' }) expect(el.kind === 'list' && el.conLabel).toBe('C') }) it('still opens a document that predates these elements', () => { const doc = normalizeBookDocument({ version: 1, title: 'Old', page: { preset: 'square-210', pageWidth: 210, pageHeight: 210, bleed: 3, safe: 5 }, spreads: [{ id: 's1', role: 'inner', entryId: null, background: null, parked: [], elements: [{ id: 'p1', kind: 'photo', frame: { x: 0, y: 0, w: 50, h: 50 }, photoId: 3 }], }], }) const el = doc.spreads[0].elements[0] expect(el.kind).toBe('photo') // The fields added since default in rather than failing the parse. expect(el.kind === 'photo' && el.mask).toBeNull() expect(el.kind === 'photo' && el.frameStyle).toBe('none') }) })