mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
4570ace78c
Every element kind and Studio tool gets an op, the auto layout and its templates move from the client to shared with a parity fixture, and a pure design engine turns directives into a finished book with design findings.
208 lines
8.8 KiB
TypeScript
208 lines
8.8 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import type { BookDocument, BookElement } from '@trek/shared'
|
|
import { bookCoordValue as coordValue, normalizeBookDocument } from '@trek/shared'
|
|
import { resolveBindings, type BindingSource } from '../../../src/components/Studio/resolveBindings'
|
|
|
|
/**
|
|
* A bound element catching up with the journal (#1973).
|
|
*
|
|
* The contract has promised since the format was written that fixing a typo in
|
|
* an entry fixes it in the book, and nothing did it: the words placed from the
|
|
* Entries tab were a copy taken once. What is protected here is that the link
|
|
* now works AND that it is not eager — a resolver that rewrote more than it
|
|
* should would repaint pages people have already set, and because the editor
|
|
* autosaves, it would write those repaints down.
|
|
*
|
|
* The identity assertion at the end is the one to keep. `useBookStore` decides
|
|
* whether a document is an edit by comparing object references, so a resolver
|
|
* that always returned a new object would turn opening a book into saving it.
|
|
*/
|
|
|
|
const source: BindingSource = {
|
|
title: 'Iceland, all the way round',
|
|
subtitle: 'Two weeks in June',
|
|
entries: [{
|
|
id: 7,
|
|
title: 'The black beach',
|
|
story: ' Wind, and then more wind. ',
|
|
location: 'Reynisfjara',
|
|
date: '2026-06-12',
|
|
lat: 63.4027,
|
|
lng: -19.0448,
|
|
}],
|
|
photos: [{ photoId: 31, caption: 'Basalt columns' }],
|
|
}
|
|
|
|
const common = { rotation: 0, opacity: 1, locked: false, frame: { x: 10, y: 10, w: 80, h: 12 } }
|
|
|
|
function text(over: Record<string, unknown>): BookElement {
|
|
return {
|
|
...common,
|
|
id: 't1', kind: 'text', text: '', font: 'sans', size: 11, weight: 400, italic: false,
|
|
align: 'left', leading: 1.45, tracking: 0, color: '#1a1a1a',
|
|
binding: null, overridden: false, ...over,
|
|
} as unknown as BookElement
|
|
}
|
|
|
|
function doc(elements: BookElement[]): BookDocument {
|
|
return normalizeBookDocument({
|
|
version: 1,
|
|
title: 'A book',
|
|
spreads: [{ id: 's1', role: 'inner', background: null, elements, parked: [], entryId: null }],
|
|
})
|
|
}
|
|
|
|
const firstText = (d: BookDocument) => d.spreads[0].elements[0] as Extract<BookElement, { kind: 'text' }>
|
|
|
|
describe('what a bound element picks up', () => {
|
|
it('takes the entry title someone has since corrected', () => {
|
|
const before = doc([text({ text: 'TEST1', binding: { source: 'entry.title', entryId: 7 } })])
|
|
expect(firstText(resolveBindings(before, source, 'en-GB')).text).toBe('The black beach')
|
|
})
|
|
|
|
it('takes the story, trimmed the way the layout trims it', () => {
|
|
const before = doc([text({ text: 'old', binding: { source: 'entry.story', entryId: 7 } })])
|
|
expect(firstText(resolveBindings(before, source, 'en-GB')).text).toBe('Wind, and then more wind.')
|
|
})
|
|
|
|
it('falls back to the place name for a stop nobody has titled', () => {
|
|
const src = { ...source, entries: [{ ...source.entries[0], title: null }] }
|
|
const before = doc([text({ text: 'old', binding: { source: 'entry.title', entryId: 7 } })])
|
|
expect(firstText(resolveBindings(before, src, 'en-GB')).text).toBe('Reynisfjara')
|
|
})
|
|
|
|
it('follows a renamed journey on the cover', () => {
|
|
const before = doc([text({ text: 'Untitled', binding: { source: 'journey.title' } })])
|
|
expect(firstText(resolveBindings(before, source, 'en-GB')).text).toBe('Iceland, all the way round')
|
|
})
|
|
|
|
it('follows a photo caption', () => {
|
|
const before = doc([text({ text: 'old', binding: { source: 'photo.caption', entryId: 7, photoId: 31 } })])
|
|
expect(firstText(resolveBindings(before, source, 'en-GB')).text).toBe('Basalt columns')
|
|
})
|
|
})
|
|
|
|
describe('what it refuses to touch', () => {
|
|
it('leaves an element alone once a human has edited it', () => {
|
|
const before = doc([text({
|
|
text: 'My own words', overridden: true, binding: { source: 'entry.title', entryId: 7 },
|
|
})])
|
|
expect(resolveBindings(before, source, 'en-GB')).toBe(before)
|
|
})
|
|
|
|
/*
|
|
* A journal entry that has been deleted must not take a page down with it.
|
|
* Blanking the words would turn a change in the journal into a hole in a book
|
|
* nobody would connect back to it.
|
|
*/
|
|
it('leaves an element whose entry is gone', () => {
|
|
const before = doc([text({ text: 'The black beach', binding: { source: 'entry.title', entryId: 999 } })])
|
|
expect(resolveBindings(before, source, 'en-GB')).toBe(before)
|
|
})
|
|
|
|
it('leaves a heading standing when the title has been cleared', () => {
|
|
const cleared = { ...source, entries: [{ ...source.entries[0], title: null, location: null }] }
|
|
const before = doc([text({ text: 'A heading somebody set', binding: { source: 'entry.title', entryId: 7 } })])
|
|
// The same element does follow a title that still exists, so the case
|
|
// below is the empty answer being refused rather than nothing happening.
|
|
expect(firstText(resolveBindings(before, source, 'en-GB')).text).toBe('The black beach')
|
|
expect(resolveBindings(before, cleared, 'en-GB')).toBe(before)
|
|
})
|
|
})
|
|
|
|
describe('a date, which is formatted rather than copied', () => {
|
|
it('is left alone when the element predates the stored raw value', () => {
|
|
// Every date in every book made so far is in this state, and most of them
|
|
// are set in another language or in small caps by the auto layout. Reading
|
|
// them would rewrite pages that are finished.
|
|
const before = doc([text({ text: 'MAY 28, 2026', binding: { source: 'entry.date', entryId: 7 } })])
|
|
expect(resolveBindings(before, source, 'de-DE')).toBe(before)
|
|
})
|
|
|
|
it('is left alone when the day has not moved, whatever language the reader has', () => {
|
|
const before = doc([text({
|
|
text: '12 June 2026',
|
|
binding: { source: 'entry.date', entryId: 7, value: '2026-06-12' },
|
|
})])
|
|
expect(resolveBindings(before, source, 'de-DE')).toBe(before)
|
|
})
|
|
|
|
it('is re-set when the day itself moved', () => {
|
|
const src = { ...source, entries: [{ ...source.entries[0], date: '2026-06-14' }] }
|
|
const before = doc([text({
|
|
text: '12 June 2026',
|
|
binding: { source: 'entry.date', entryId: 7, value: '2026-06-12' },
|
|
})])
|
|
const after = firstText(resolveBindings(before, src, 'en-GB'))
|
|
expect(after.text).toBe('14 June 2026')
|
|
// And the raw value moves with it, or the next open would do it all again.
|
|
expect(after.binding?.value).toBe('2026-06-14')
|
|
})
|
|
})
|
|
|
|
describe('coordinates', () => {
|
|
it('are re-set when the stop moved, in the format the element was placed with', () => {
|
|
const src = { ...source, entries: [{ ...source.entries[0], lat: 64.1466, lng: -21.9426 }] }
|
|
const before = doc([text({
|
|
text: `63° 24' 10" S 19° 2' 41" W`,
|
|
binding: {
|
|
source: 'entry.location', entryId: 7, format: 'decimal', value: coordValue(63.4027, -19.0448),
|
|
},
|
|
})])
|
|
const after = firstText(resolveBindings(before, src, 'en-GB'))
|
|
expect(after.text).toBe('64.1466° N 21.9426° W')
|
|
expect(after.binding?.value).toBe('64.1466,-21.9426')
|
|
})
|
|
|
|
it('are left alone when the stop has not moved', () => {
|
|
const before = doc([text({
|
|
text: 'anything at all',
|
|
binding: {
|
|
source: 'entry.location', entryId: 7, format: 'dms', value: coordValue(63.4027, -19.0448),
|
|
},
|
|
})])
|
|
expect(resolveBindings(before, source, 'en-GB')).toBe(before)
|
|
})
|
|
|
|
/*
|
|
* The same source without a format is the place *name* — that is what the
|
|
* "Place" chip has always placed, and it is words rather than a reading, so
|
|
* it follows the journal directly.
|
|
*/
|
|
it('are not what an unformatted location binding means', () => {
|
|
const before = doc([text({ text: 'Vík', binding: { source: 'entry.location', entryId: 7 } })])
|
|
expect(firstText(resolveBindings(before, source, 'en-GB')).text).toBe('Reynisfjara')
|
|
})
|
|
})
|
|
|
|
describe('the document it hands back', () => {
|
|
it('is the very same object when nothing resolved differently', () => {
|
|
const before = doc([
|
|
text({ id: 't1', text: 'The black beach', binding: { source: 'entry.title', entryId: 7 } }),
|
|
text({ id: 't2', text: 'Iceland, all the way round', binding: { source: 'journey.title' } }),
|
|
])
|
|
expect(resolveBindings(before, source, 'en-GB')).toBe(before)
|
|
})
|
|
|
|
it('shares through every spread and element it did not change', () => {
|
|
const before = normalizeBookDocument({
|
|
version: 1,
|
|
title: 'A book',
|
|
spreads: [
|
|
{
|
|
id: 's1', role: 'inner', background: null, entryId: null, parked: [],
|
|
elements: [text({ id: 'keep', text: 'Iceland, all the way round', binding: { source: 'journey.title' } })],
|
|
},
|
|
{
|
|
id: 's2', role: 'inner', background: null, entryId: null, parked: [],
|
|
elements: [text({ id: 'move', text: 'TEST1', binding: { source: 'entry.title', entryId: 7 } })],
|
|
},
|
|
],
|
|
})
|
|
const after = resolveBindings(before, source, 'en-GB')
|
|
expect(after).not.toBe(before)
|
|
expect(after.spreads[0]).toBe(before.spreads[0])
|
|
expect(after.spreads[1]).not.toBe(before.spreads[1])
|
|
})
|
|
})
|