mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-12 23:31:02 +00:00
bdf5767a5f
The rail lists its sections in the order a book is made: Pages, Content, Layouts, Elements and Travel, then Theme and Layers set apart below a rule. Stickers are a tab of Elements beside the icons instead of a rail section of their own, and Elements has six tabs (text, shapes, lines, frames with the grids, icons, stickers). The quotation mark moved to the text styles, the outline rectangle to the basic shapes and the plain rule to the lines. The rail and every tab row are tab lists the arrow keys walk through, and a long name wraps instead of being cut. The icon and sticker searches say in the field how many match, which the sticker library's own head used to. Every panel on both sides opens on the same head, and every labelled control uses one row that moves the control under its label when the two do not fit, so no label sits in a fixed column any more. Number fields, chips, sliders, tab labels, theme names and travel cards wrap the same way, a country beside a stop wraps under itself, and an upload cell holds its words to two lines with the whole label in its tooltip. The heads, shelves and groups are real headings, so a screen reader can walk the panels by them. The inspector shows every kind in one order: what the element is and says, its position and size, its look, its effects, then Arrange. Align and the stacking buttons are one Arrange group with a button that opens the page in Layers; its rows sit under their labels, each in two sets that move to a second line whole. Rarely used groups start folded and open on an element that already uses them (a turned element, a map held at a zoom, a summary set in another face), and every group mounts afresh for the next selection. A sticker's duplicate angle and opacity are gone in favour of the shared position group, which now has the turn buttons for every kind. With several elements selected it keeps their angle, opacity and turn, and the mirror when all of them are stickers, and leaves the frame's numbers to one element. A photo's fill page and fill spread sit under its crop. With nothing selected the inspector shows the page background, the book's theme with the way to the Theme panel, and the page numbers. Seventeen translation keys nothing has used since before the rebuild (the old inspector title, the empty spread note, the summary and country list names, the roads buttons and others) are gone from every language. The broad key template the old panels used had counted them as reached; the format names, the empty photo grid messages and the map source hints are now named where the key check can read them. Also fixed on the way: the travel stops list shared its class with the gradient stops and was laid out as sixteen pixel handles; the alignment buttons were named in raw English; the map's print resolution note joined its two halves with a dash.
221 lines
7.9 KiB
TypeScript
221 lines
7.9 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
|
import type { BookDocument, BookPageSetup, BookTextElement } from '@trek/shared'
|
|
import { bookPageSetupSchema, normalizeBookDocument } from '@trek/shared'
|
|
import { fireEvent, render, screen } from '../../helpers/render'
|
|
import { StudioSidebar, type JourneySource } from '../../../src/components/Studio/StudioSidebar'
|
|
import type { StudioUploader } from '../../../src/components/Studio/studioUpload'
|
|
import { useStudioStore } from '../../../src/store/studioStore'
|
|
|
|
/**
|
|
* The date and coordinate chips in Content > Entries (#1973).
|
|
*
|
|
* An entry knows two things nobody wants to re-type onto a page: which day it
|
|
* was and where it happened. What these check is that both arrive still
|
|
* attached to the entry, carrying the raw fact alongside the printed words,
|
|
* because that pair is what lets a book re-read its own text when it opens
|
|
* without rewriting a line that was only ever set in another language.
|
|
*
|
|
* The last test is the one that earns its place. Placing an element is half of
|
|
* it; the document has to survive the contract afterwards. A mood mark once
|
|
* failed the union over a two-character cap and took the whole book with it,
|
|
* normalising to an empty document that the autosave then wrote down, so a new
|
|
* binding field that quietly fails to parse is not a cosmetic problem.
|
|
*/
|
|
|
|
const page: BookPageSetup = bookPageSetupSchema.parse({
|
|
preset: 'square-210', pageWidth: 210, pageHeight: 210, bleed: 3, safe: 5,
|
|
})
|
|
|
|
type Entry = JourneySource['entries'][number]
|
|
|
|
const entry = (over: Partial<Entry> = {}): Entry => ({
|
|
id: 7,
|
|
title: 'Kirkjufell',
|
|
story: null,
|
|
location: 'Grundarfjordur',
|
|
date: '2026-06-04',
|
|
lat: 64.9275,
|
|
lng: -23.3072,
|
|
mood: null,
|
|
weather: null,
|
|
pros: [],
|
|
cons: [],
|
|
photoIds: [],
|
|
...over,
|
|
})
|
|
|
|
const sourceOf = (...entries: Entry[]): JourneySource => ({ entries, photos: [], photoEntries: {} })
|
|
|
|
/** Nothing ever arrives through these; the chips under test place text. */
|
|
const noUpload: StudioUploader = async () => ({ photoIds: [], failed: 0, skippedVideos: 0 })
|
|
|
|
/** One empty inner spread, parsed rather than cast, so the store holds a real document. */
|
|
const emptyBook = (): BookDocument => normalizeBookDocument({
|
|
version: 1,
|
|
title: 'Iceland',
|
|
page: { preset: 'square-210', pageWidth: 210, pageHeight: 210, bleed: 3, safe: 5 },
|
|
spreads: [{ id: 'sp1', role: 'inner', entryId: null, background: null, elements: [], parked: [] }],
|
|
})
|
|
|
|
beforeEach(() => {
|
|
useStudioStore.getState().load(emptyBook())
|
|
})
|
|
|
|
/**
|
|
* Render the sidebar and walk to the panel the chips live in.
|
|
*
|
|
* ContentPanel is private to the sidebar, so the way in is the way a person
|
|
* takes: the rail, then the Entries tab. `t` is the identity function, which is
|
|
* why every label below is a raw key.
|
|
*/
|
|
function openEntries(source: JourneySource) {
|
|
const view = render(
|
|
<StudioSidebar
|
|
page={page}
|
|
pxPerMm={96 / 25.4}
|
|
bookView={false}
|
|
source={source}
|
|
stats={null}
|
|
path={[]}
|
|
t={(k: string) => k}
|
|
locale="en-US"
|
|
canEdit
|
|
onUpload={noUpload}
|
|
onToggleStop={async () => true}
|
|
/>,
|
|
)
|
|
fireEvent.click(screen.getByLabelText('journey.studio.content'))
|
|
fireEvent.click(screen.getByRole('tab', { name: /journey\.studio\.entries/ }))
|
|
return view
|
|
}
|
|
|
|
/** The element the chip just dropped, narrowed so its binding can be read. */
|
|
function lastText(): BookTextElement {
|
|
const els = useStudioStore.getState().doc!.spreads[0].elements
|
|
const el = els[els.length - 1]
|
|
if (!el || el.kind !== 'text') throw new Error(`expected a text element, found ${el?.kind ?? 'nothing'}`)
|
|
return el
|
|
}
|
|
|
|
describe('which chips an entry offers', () => {
|
|
it('offers no coordinates for an entry that never recorded a point', () => {
|
|
openEntries(sourceOf(entry({ lat: null, lng: null })))
|
|
|
|
expect(screen.queryByText('journey.studio.coordsMark')).toBeNull()
|
|
// The place name is still on offer: a written location is not a point.
|
|
expect(screen.getByText('journey.studio.addPlace')).toBeInTheDocument()
|
|
})
|
|
|
|
it('offers no coordinates when only one half of the point survived', () => {
|
|
openEntries(sourceOf(entry({ lng: null })))
|
|
|
|
expect(screen.queryByText('journey.studio.coordsMark')).toBeNull()
|
|
})
|
|
|
|
it('offers coordinates once the entry has both halves', () => {
|
|
openEntries(sourceOf(entry()))
|
|
|
|
expect(screen.getByText('journey.studio.coordsMark')).toBeInTheDocument()
|
|
})
|
|
|
|
it('offers no date for an undated entry', () => {
|
|
openEntries(sourceOf(entry({ date: null })))
|
|
|
|
expect(screen.queryByText('journey.studio.dateMark')).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('the date chip', () => {
|
|
it('prints the day spelled out, with its year', () => {
|
|
openEntries(sourceOf(entry()))
|
|
fireEvent.click(screen.getByText('journey.studio.dateMark'))
|
|
|
|
// Not the sidebar's short weekday form: a book outlives the year it was
|
|
// made in, so the year belongs on the page.
|
|
expect(lastText().text).toBe('June 4, 2026')
|
|
})
|
|
|
|
it('stays attached to the entry it came from, and unedited', () => {
|
|
openEntries(sourceOf(entry()))
|
|
fireEvent.click(screen.getByText('journey.studio.dateMark'))
|
|
const el = lastText()
|
|
|
|
expect(el.binding).toMatchObject({ source: 'entry.date', entryId: 7 })
|
|
expect(el.overridden).toBe(false)
|
|
})
|
|
|
|
/*
|
|
* The words are a rendering of the date, so they cannot answer "has the
|
|
* journal changed?" on their own. The ISO date rides along to answer it.
|
|
*/
|
|
it('carries the entry ISO date as the fact behind the words', () => {
|
|
openEntries(sourceOf(entry()))
|
|
fireEvent.click(screen.getByText('journey.studio.dateMark'))
|
|
|
|
expect(lastText().binding!.value).toBe('2026-06-04')
|
|
// A date has one printed form, so it claims no coordinate format.
|
|
expect(lastText().binding!.format).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('the coordinates chip', () => {
|
|
it('prints the point in degrees, minutes and seconds', () => {
|
|
openEntries(sourceOf(entry()))
|
|
fireEvent.click(screen.getByText('journey.studio.coordsMark'))
|
|
|
|
expect(lastText().text).toBe(`64° 55' 39" N 23° 18' 26" W`)
|
|
// A printed page says W, never a minus sign.
|
|
expect(lastText().text).not.toContain('-')
|
|
})
|
|
|
|
it('binds to the entry location, marked as the coordinate reading of it', () => {
|
|
openEntries(sourceOf(entry()))
|
|
fireEvent.click(screen.getByText('journey.studio.coordsMark'))
|
|
|
|
expect(lastText().binding).toMatchObject({
|
|
source: 'entry.location',
|
|
entryId: 7,
|
|
format: 'dms',
|
|
value: '64.9275,-23.3072',
|
|
})
|
|
})
|
|
|
|
it('lands on a short line rather than across the whole column', () => {
|
|
openEntries(sourceOf(entry()))
|
|
fireEvent.click(screen.getByText('journey.studio.addPlace'))
|
|
const placeWidth = lastText().frame.w
|
|
|
|
fireEvent.click(screen.getByText('journey.studio.coordsMark'))
|
|
|
|
expect(lastText().frame.w).toBeLessThan(placeWidth)
|
|
})
|
|
})
|
|
|
|
describe('a spread holding both marks', () => {
|
|
/*
|
|
* Normalising is not a formality here: the client's load seam and both of the
|
|
* server's paths run through it, and it used to answer a single unreadable
|
|
* element with an empty book. Whatever the chips write has to come back out.
|
|
*/
|
|
it('survives normalizeBookDocument with the spread and both bindings intact', () => {
|
|
openEntries(sourceOf(entry()))
|
|
fireEvent.click(screen.getByText('journey.studio.dateMark'))
|
|
fireEvent.click(screen.getByText('journey.studio.coordsMark'))
|
|
|
|
const doc = normalizeBookDocument(useStudioStore.getState().doc)
|
|
|
|
expect(doc.title).toBe('Iceland')
|
|
expect(doc.spreads).toHaveLength(1)
|
|
expect(doc.spreads[0].elements).toHaveLength(2)
|
|
|
|
const [date, coords] = doc.spreads[0].elements
|
|
expect(date.kind === 'text' && date.text).toBe('June 4, 2026')
|
|
expect(date.kind === 'text' && date.binding).toMatchObject({
|
|
source: 'entry.date', entryId: 7, value: '2026-06-04',
|
|
})
|
|
expect(coords.kind === 'text' && coords.binding).toMatchObject({
|
|
source: 'entry.location', entryId: 7, format: 'dms', value: '64.9275,-23.3072',
|
|
})
|
|
})
|
|
})
|