mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +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.
661 lines
23 KiB
TypeScript
661 lines
23 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest'
|
||
import type { BookDocument, BookElement, JourneyStats } from '@trek/shared'
|
||
import {
|
||
bookBadgeElementSchema, bookCountriesElementSchema, bookListElementSchema,
|
||
bookMapElementSchema, bookPageSetupSchema, bookStatsElementSchema, journeyStatsSchema,
|
||
} from '@trek/shared'
|
||
import { fireEvent, render, screen, within } from '../../helpers/render'
|
||
import { StudioInspector } from '../../../src/components/Studio/StudioInspector'
|
||
import { useStudioStore } from '../../../src/store/studioStore'
|
||
|
||
/**
|
||
* The inspector, on the five travel elements (#1973).
|
||
*
|
||
* The panel was rebuilt: same functions, new structure. Sections fold, booleans
|
||
* are the app's switch instead of a chip, a set of several carries a tick, and
|
||
* duplicate, lock and delete moved out of "Arrange" and into the panel head.
|
||
* Nothing in the suite rendered the inspector at all, so a control that came
|
||
* out of that rebuild wired to nothing would have been found in a printed book
|
||
* rather than here.
|
||
*
|
||
* Two rules for what follows. The whole inspector is mounted rather than the
|
||
* travel half on its own, because the tail every travel element shares, type,
|
||
* colour, position, arrange, is drawn by the outer panel. And what is asserted
|
||
* is the document the store holds, not the markup: the question is whether a
|
||
* control still writes the field it used to write, whichever section it now
|
||
* lives in.
|
||
*/
|
||
|
||
const page = bookPageSetupSchema.parse({
|
||
preset: 'square-210', pageWidth: 210, pageHeight: 210, bleed: 3, safe: 5,
|
||
})
|
||
|
||
const frame = { x: 10, y: 10, w: 120, h: 90 }
|
||
|
||
/** A journey with figures no element below starts out agreeing with. */
|
||
const stats: JourneyStats = journeyStatsSchema.parse({
|
||
journeyId: 1,
|
||
distance: 1234, days: 5, steps: 7, photos: 12, places: 9, furthest: 400,
|
||
countries: [{ code: 'IS', name: 'Iceland', places: 9, firstVisit: '2026-01-01' }],
|
||
points: [
|
||
{ lat: 64.14, lng: -21.94, label: 'Reykjavik', date: '2026-01-01', country: 'IS' },
|
||
{ lat: 65.68, lng: -18.09, label: 'Akureyri', date: '2026-01-04', country: 'IS' },
|
||
],
|
||
start: '2026-01-01', end: '2026-01-05',
|
||
})
|
||
|
||
const mapEl = (over: Record<string, unknown> = {}): BookElement => bookMapElementSchema.parse({
|
||
id: 'm1', kind: 'map', frame, countries: ['IS'],
|
||
points: [{ lat: 64.14, lng: -21.94, label: 'Reykjavik' }],
|
||
...over,
|
||
}) as BookElement
|
||
|
||
const statsEl = (over: Record<string, unknown> = {}): BookElement => bookStatsElementSchema.parse({
|
||
id: 's1', kind: 'stats', frame, ...over,
|
||
}) as BookElement
|
||
|
||
const countriesEl = (over: Record<string, unknown> = {}): BookElement => bookCountriesElementSchema.parse({
|
||
id: 'c1', kind: 'countries', frame, codes: ['IS', 'NO'], names: ['Iceland', 'Norway'], ...over,
|
||
}) as BookElement
|
||
|
||
const badgeEl = (over: Record<string, unknown> = {}): BookElement => bookBadgeElementSchema.parse({
|
||
id: 'b1', kind: 'badge', frame, variant: 'flag', code: 'IS', text: 'ICELAND', ...over,
|
||
}) as BookElement
|
||
|
||
const listEl = (over: Record<string, unknown> = {}): BookElement => bookListElementSchema.parse({
|
||
id: 'l1', kind: 'list', frame,
|
||
items: [{ text: 'Warm all week', tone: 'pro' }, { text: 'Ferry was late', tone: 'con' }],
|
||
proLabel: 'Pros', conLabel: 'Cons',
|
||
...over,
|
||
}) as BookElement
|
||
|
||
function show(el: BookElement, live: JourneyStats | null = null, trail: [number, number][][] = []) {
|
||
useStudioStore.getState().load({
|
||
version: 1, title: 'T', page,
|
||
spreads: [{ id: 'sp1', role: 'inner', background: null, elements: [el], parked: [], entryId: null }],
|
||
} as BookDocument)
|
||
useStudioStore.getState().select([el.id])
|
||
return render(
|
||
<StudioInspector
|
||
spreadIndex={0}
|
||
page={page}
|
||
stats={live}
|
||
path={trail}
|
||
source={{ entries: [], photos: [], photoEntries: {} }}
|
||
setPageNumbers={() => {}}
|
||
t={k => k}
|
||
locale="en"
|
||
/>,
|
||
)
|
||
}
|
||
|
||
/** The element as the store holds it. Every assertion below reads this. */
|
||
function saved(): Record<string, unknown> {
|
||
return useStudioStore.getState().doc!.spreads[0].elements[0] as unknown as Record<string, unknown>
|
||
}
|
||
|
||
/** One of a few. */
|
||
const pick = (name: string) => fireEvent.click(screen.getByRole('radio', { name }))
|
||
/** One of a set, which carries a tick. */
|
||
const tick = (name: string) => fireEvent.click(screen.getByRole('checkbox', { name }))
|
||
/** A switch, or any plain button: both answer to their accessible name. */
|
||
const press = (name: string) => fireEvent.click(screen.getByRole('button', { name }))
|
||
|
||
/** Opens the typeface list from its button and picks a family by the name its row is set in. */
|
||
function pickFamily(name: string) {
|
||
fireEvent.click(screen.getByRole('button', { name: /^journey\.studio\.fonts\.change: / }))
|
||
const row = screen.getAllByRole('option').find(o => o.querySelector('.st-fontpick-face')?.textContent === name)
|
||
if (!row) throw new Error(`no typeface row named ${name}`)
|
||
fireEvent.click(row)
|
||
}
|
||
|
||
/** Drags the weight slider to the stop of a weight among those the family ships. */
|
||
function slideWeight(weights: readonly number[], weight: number) {
|
||
const slider = screen.getByRole('slider', { name: 'journey.studio.weight' })
|
||
fireEvent.change(slider, { target: { value: String(weights.indexOf(weight)) } })
|
||
}
|
||
|
||
/** A section by its heading, since a label can repeat across sections. */
|
||
function section(container: HTMLElement, label: string): HTMLElement {
|
||
const found = [...container.querySelectorAll<HTMLElement>('.st-section')]
|
||
.find(s => s.querySelector('.st-section-label')?.textContent === label)
|
||
if (!found) throw new Error(`no section headed ${label}`)
|
||
return found
|
||
}
|
||
|
||
/** Whether a section is open, read off its head. */
|
||
function isOpen(container: HTMLElement, label: string): boolean {
|
||
return section(container, label).querySelector('.st-section-head')?.getAttribute('aria-expanded') === 'true'
|
||
}
|
||
|
||
/**
|
||
* Some sections start folded, so a control inside one has to be uncovered. A
|
||
* folded section opens by itself on an element that already uses it, and is
|
||
* left open then.
|
||
*/
|
||
function open(container: HTMLElement, label: string) {
|
||
if (!isOpen(container, label)) fireEvent.click(section(container, label).querySelector('.st-section-head')!)
|
||
}
|
||
|
||
/** The control side of one labelled line inside one section. */
|
||
function lineIn(container: HTMLElement, sectionLabel: string, lineLabel: string): HTMLElement {
|
||
const row = [...section(container, sectionLabel).querySelectorAll<HTMLElement>('.st-line')]
|
||
.find(l => l.querySelector('.st-line-label')?.textContent === lineLabel)
|
||
if (!row) throw new Error(`no line labelled ${lineLabel} under ${sectionLabel}`)
|
||
return row.querySelector<HTMLElement>('.st-line-body')!
|
||
}
|
||
|
||
beforeEach(() => {
|
||
useStudioStore.getState().load({ version: 1, title: 'T', page, spreads: [] } as unknown as BookDocument)
|
||
})
|
||
|
||
describe('the route map', () => {
|
||
it('takes the tiles and the credit from the source it is switched to', () => {
|
||
show(mapEl())
|
||
|
||
pick('journey.studio.mapSourceSatellite')
|
||
|
||
expect(saved().source).toBe('tiles')
|
||
expect(saved().tileUrl).toContain('tiles.maps.eox.at')
|
||
// The credit travels with the source, or the page prints the wrong one —
|
||
// and this one is CC BY, so printing it uncredited is a licence breach
|
||
// rather than a discourtesy.
|
||
expect(saved().attribution).toContain('EOX')
|
||
})
|
||
|
||
/*
|
||
* The planner's own street tiles and Mapbox are no longer offered: one is a
|
||
* street map that reads as a screenshot on paper and whose server asks people
|
||
* not to bulk-fetch, the other needs written permission before anything is
|
||
* printed. Removing an option must not remove a page, though.
|
||
*/
|
||
it('still offers the source a map already uses, even one no longer recommended', () => {
|
||
show(mapEl({
|
||
source: 'tiles',
|
||
tileUrl: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
attribution: '© OpenStreetMap contributors',
|
||
}))
|
||
|
||
const chip = screen.getByRole('radio', { name: 'journey.studio.mapSourceTiles' })
|
||
expect(chip.getAttribute('aria-checked')).toBe('true')
|
||
})
|
||
|
||
it('does not offer it on a map that never used it', () => {
|
||
show(mapEl())
|
||
expect(screen.queryByRole('radio', { name: 'journey.studio.mapSourceTiles' })).toBeNull()
|
||
expect(screen.queryByRole('radio', { name: 'journey.studio.mapSourceStatic' })).toBeNull()
|
||
})
|
||
|
||
it('writes the drawn style', () => {
|
||
show(mapEl())
|
||
|
||
pick('journey.studio.mapStyle.paper')
|
||
|
||
expect(saved().style).toBe('paper')
|
||
})
|
||
|
||
it('fits the stops rather than the countries, and back again', () => {
|
||
show(mapEl())
|
||
expect(saved().fitToCountries).toBe(true)
|
||
|
||
pick('journey.studio.mapFitStops')
|
||
expect(saved().fitToCountries).toBe(false)
|
||
|
||
pick('journey.studio.mapFitCountry')
|
||
expect(saved().fitToCountries).toBe(true)
|
||
})
|
||
|
||
it('writes how much room is left around what is drawn', () => {
|
||
show(mapEl())
|
||
|
||
pick('journey.studio.mapPadWide')
|
||
expect(saved().fitPadding).toBe(0.5)
|
||
|
||
pick('journey.studio.mapPadTight')
|
||
expect(saved().fitPadding).toBe(0.04)
|
||
})
|
||
|
||
it('cuts the picture to the coastline when the shape is switched', () => {
|
||
show(mapEl())
|
||
|
||
pick('journey.studio.mapClipCountry')
|
||
|
||
expect(saved().clip).toBe('country')
|
||
})
|
||
|
||
/* A country-shaped hole with no country to cut against is a filled box. */
|
||
it('refuses the country shape on a map that knows no countries', () => {
|
||
show(mapEl({ countries: [] }))
|
||
|
||
const chip = screen.getByRole('radio', { name: 'journey.studio.mapClipCountry' })
|
||
expect(chip).toBeDisabled()
|
||
|
||
fireEvent.click(chip)
|
||
expect(saved().clip).toBe('rect')
|
||
})
|
||
|
||
it('folds the zoom of a map that fits its route, and opens it on one held at a zoom', () => {
|
||
const tiles = { source: 'tiles', tileUrl: 'https://tile.example/{z}/{x}/{y}.png' }
|
||
const fitted = show(mapEl(tiles))
|
||
expect(isOpen(fitted.container, 'journey.studio.mapZoom')).toBe(false)
|
||
fitted.unmount()
|
||
|
||
const held = show(mapEl({ ...tiles, zoom: 11 }))
|
||
expect(isOpen(held.container, 'journey.studio.mapZoom')).toBe(true)
|
||
expect(screen.getByRole('radio', { name: '11' })).toHaveAttribute('aria-checked', 'true')
|
||
})
|
||
|
||
it('writes a zoom from the folded group, and hands it back automatically', () => {
|
||
const { container } = show(mapEl({ source: 'tiles', tileUrl: 'https://tile.example/{z}/{x}/{y}.png' }))
|
||
open(container, 'journey.studio.mapZoom')
|
||
|
||
pick('11')
|
||
expect(saved().zoom).toBe(11)
|
||
|
||
// Automatic is null rather than a number, which is what lets the element
|
||
// choose one that fits the route.
|
||
pick('journey.studio.mapZoomAuto')
|
||
expect(saved().zoom).toBeNull()
|
||
})
|
||
|
||
it('draws each layer, or does not', () => {
|
||
show(mapEl())
|
||
|
||
press('journey.studio.showLand')
|
||
expect(saved().showLand).toBe(false)
|
||
|
||
press('journey.studio.showRoute')
|
||
expect(saved().showRoute).toBe(false)
|
||
|
||
press('journey.studio.showPins')
|
||
expect(saved().showPins).toBe(false)
|
||
|
||
press('journey.studio.showLabels')
|
||
expect(saved().showLabels).toBe(true)
|
||
})
|
||
|
||
/** A journey of two trips, so the map offers its scope at all. */
|
||
const twoTrips: JourneyStats = journeyStatsSchema.parse({
|
||
...stats,
|
||
countries: [
|
||
{ code: 'IS', name: 'Iceland', places: 2, firstVisit: '2026-01-01' },
|
||
{ code: 'PT', name: 'Portugal', places: 1, firstVisit: '2026-03-01' },
|
||
],
|
||
points: [
|
||
{ lat: 64.14, lng: -21.94, label: 'Reykjavik', date: '2026-01-01', country: 'IS', tripId: 11 },
|
||
{ lat: 65.68, lng: -18.09, label: 'Akureyri', date: '2026-01-04', country: 'IS', tripId: 11 },
|
||
{ lat: 38.72, lng: -9.14, label: 'Lisboa', date: '2026-03-01', country: 'PT', tripId: 22 },
|
||
],
|
||
trips: [
|
||
{ id: 11, title: 'North', start: '2026-01-01', end: '2026-01-05', points: 2 },
|
||
{ id: 22, title: 'South', start: '2026-03-01', end: '2026-03-02', points: 1 },
|
||
],
|
||
})
|
||
/** The journey's recorded way, as the Studio hands it to the inspector. */
|
||
const trail: [number, number][][] = [[[64.14, -21.94], [38.72, -9.14]]]
|
||
/** Roads loaded for the whole journey's legs: entry i runs from stop i to stop i + 1. */
|
||
const roads = [[[64.14, -21.94], [64.0, -21.5], [65.68, -18.09]], null]
|
||
|
||
/*
|
||
* The scope switch re-cuts the map as the book engine refreshes one, so a
|
||
* map turned into a trip map here holds what the panel or the assistant
|
||
* would have placed for that trip: its own stops, its own countries, no
|
||
* recorded way, since the trail is the whole journey's, and no roads, since
|
||
* those belonged to the old legs. Switched back, the map takes the trail
|
||
* from the journey again, so the round trip loses nothing.
|
||
*/
|
||
it('STTRV-007: re-cuts a map to one trip as the engine does, and back to the journey with its trail', () => {
|
||
show(mapEl({ countries: ['IS', 'PT'], path: trail, roads }), twoTrips, trail)
|
||
|
||
pick('North')
|
||
expect(saved()).toMatchObject({
|
||
tripId: 11, countries: ['IS'], path: [], roads: [], stale: false,
|
||
points: [{ label: 'Reykjavik' }, { label: 'Akureyri' }],
|
||
})
|
||
|
||
pick('journey.studio.mapWholeJourney')
|
||
expect(saved()).toMatchObject({ tripId: null, countries: ['IS', 'PT'], roads: [], stale: false })
|
||
expect(saved().path).toEqual(trail)
|
||
expect(saved().points).toHaveLength(3)
|
||
})
|
||
|
||
it('STTRV-008: picking the scope a map already has writes nothing, so its loaded roads stay', () => {
|
||
show(mapEl({ countries: ['IS', 'PT'], path: trail, roads }), twoTrips, trail)
|
||
|
||
pick('journey.studio.mapWholeJourney')
|
||
expect(saved()).toMatchObject({ tripId: null, path: trail, roads })
|
||
// Still the one stop it was made with: a re-cut would have brought all three.
|
||
expect(saved().points).toHaveLength(1)
|
||
})
|
||
})
|
||
|
||
describe('the figures', () => {
|
||
it('adds a figure to the set', () => {
|
||
show(statsEl())
|
||
|
||
tick('journey.studio.metric.countries')
|
||
|
||
expect(saved().metrics).toEqual(['distance', 'days', 'steps', 'photos', 'countries'])
|
||
})
|
||
|
||
it('takes one back out', () => {
|
||
show(statsEl())
|
||
|
||
tick('journey.studio.metric.days')
|
||
|
||
expect(saved().metrics).toEqual(['distance', 'steps', 'photos'])
|
||
})
|
||
|
||
/* An empty panel is a blank rectangle nobody could get back out of. */
|
||
it('keeps the last figure', () => {
|
||
show(statsEl({ metrics: ['days'] }))
|
||
|
||
tick('journey.studio.metric.days')
|
||
|
||
expect(saved().metrics).toEqual(['days'])
|
||
})
|
||
|
||
it('writes the layout and the units', () => {
|
||
show(statsEl())
|
||
|
||
pick('journey.studio.layoutRow')
|
||
expect(saved().layout).toBe('row')
|
||
|
||
pick('mi')
|
||
expect(saved().units).toBe('imperial')
|
||
})
|
||
|
||
it('writes the icons switch', () => {
|
||
show(statsEl())
|
||
|
||
press('journey.studio.showIcons')
|
||
|
||
expect(saved().showIcons).toBe(false)
|
||
})
|
||
})
|
||
|
||
describe('the countries', () => {
|
||
it('writes the layout and the alignment', () => {
|
||
show(countriesEl())
|
||
|
||
pick('journey.studio.layoutColumn')
|
||
expect(saved().layout).toBe('column')
|
||
|
||
pick('journey.studio.align.right')
|
||
expect(saved().align).toBe('right')
|
||
})
|
||
|
||
it('writes the three things it draws', () => {
|
||
show(countriesEl())
|
||
|
||
press('journey.studio.showOutline')
|
||
expect(saved().showOutline).toBe(false)
|
||
|
||
press('journey.studio.showFlag')
|
||
expect(saved().showFlag).toBe(true)
|
||
|
||
press('journey.studio.showName')
|
||
expect(saved().showName).toBe(false)
|
||
})
|
||
|
||
it('keeps one name per code when a line is edited', () => {
|
||
show(countriesEl())
|
||
|
||
fireEvent.change(screen.getByLabelText('journey.studio.countryNames'), {
|
||
target: { value: 'Island\nNoreg' },
|
||
})
|
||
|
||
expect(saved().names).toEqual(['Island', 'Noreg'])
|
||
})
|
||
|
||
/*
|
||
* The reason the textarea does not simply store its own lines: a removed line
|
||
* must not shift every name after it onto the wrong outline, and a line
|
||
* nobody has a country for must not be printed at all.
|
||
*/
|
||
it('ignores a line past the last country, and keeps a name whose line is gone', () => {
|
||
show(countriesEl())
|
||
const box = screen.getByLabelText('journey.studio.countryNames')
|
||
|
||
fireEvent.change(box, { target: { value: 'Island\nNoreg\nSverige' } })
|
||
expect(saved().names).toEqual(['Island', 'Noreg'])
|
||
|
||
fireEvent.change(box, { target: { value: 'Island' } })
|
||
expect(saved().names).toEqual(['Island', 'Noreg'])
|
||
})
|
||
})
|
||
|
||
describe('the mark', () => {
|
||
it('writes the words and the caption', () => {
|
||
const { container } = show(badgeEl())
|
||
|
||
fireEvent.change(
|
||
within(lineIn(container, 'journey.studio.marks', 'journey.studio.text')).getByRole('textbox'),
|
||
{ target: { value: 'REYKJAVIK' } },
|
||
)
|
||
expect(saved().text).toBe('REYKJAVIK')
|
||
|
||
fireEvent.change(
|
||
within(lineIn(container, 'journey.studio.marks', 'journey.studio.styleCaption')).getByRole('textbox'),
|
||
{ target: { value: 'Day 1' } },
|
||
)
|
||
expect(saved().sub).toBe('Day 1')
|
||
})
|
||
|
||
it('writes the style, in the panel language rather than in the raw value', () => {
|
||
show(badgeEl())
|
||
|
||
pick('journey.studio.markStyle.chip')
|
||
expect(saved().style).toBe('chip')
|
||
|
||
pick('journey.studio.markStyle.stacked')
|
||
expect(saved().style).toBe('stacked')
|
||
|
||
// These four printed `plain`, `chip` and the rest in all twenty-three
|
||
// languages before, straight out of the value.
|
||
expect(screen.getByRole('radio', { name: 'journey.studio.markStyle.outline' })).toBeTruthy()
|
||
expect(screen.queryByRole('radio', { name: 'outline' })).toBeNull()
|
||
})
|
||
|
||
it('writes the three combinations of picture and words', () => {
|
||
show(badgeEl())
|
||
|
||
pick('journey.studio.iconOnly')
|
||
expect(saved().showIcon).toBe(true)
|
||
expect(saved().showLabel).toBe(false)
|
||
|
||
pick('journey.studio.labelOnly')
|
||
expect(saved().showIcon).toBe(false)
|
||
expect(saved().showLabel).toBe(true)
|
||
|
||
pick('journey.studio.iconAndLabel')
|
||
expect(saved().showIcon).toBe(true)
|
||
expect(saved().showLabel).toBe(true)
|
||
})
|
||
|
||
/* A mark with neither its picture nor its words is a way to lose an element. */
|
||
it('offers no way to switch both off', () => {
|
||
const { container } = show(badgeEl())
|
||
const shows = within(section(container, 'journey.studio.icon')).getAllByRole('radio')
|
||
expect(shows).toHaveLength(3)
|
||
|
||
for (const chip of shows) {
|
||
fireEvent.click(chip)
|
||
expect(saved().showIcon || saved().showLabel).toBe(true)
|
||
}
|
||
})
|
||
|
||
it('writes the icon colour, and stops the icon choosing its own', () => {
|
||
const { container } = show(badgeEl())
|
||
|
||
fireEvent.click(
|
||
within(lineIn(container, 'journey.studio.icon', 'journey.studio.colour'))
|
||
.getByRole('button', { name: '#0f766e' }),
|
||
)
|
||
|
||
expect(saved().iconColor).toBe('#0f766e')
|
||
expect(saved().autoIconColor).toBe(false)
|
||
})
|
||
|
||
it('hands the icon colour back with the automatic switch', () => {
|
||
const { container } = show(badgeEl({ autoIconColor: false, iconColor: '#0f766e' }))
|
||
|
||
fireEvent.click(
|
||
within(section(container, 'journey.studio.icon'))
|
||
.getByRole('button', { name: 'journey.studio.autoColour' }),
|
||
)
|
||
|
||
expect(saved().autoIconColor).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('the pros and cons list', () => {
|
||
it('writes the layout', () => {
|
||
show(listEl())
|
||
|
||
pick('journey.studio.layoutColumn')
|
||
expect(saved().layout).toBe('stacked')
|
||
|
||
pick('journey.studio.layoutGrid')
|
||
expect(saved().layout).toBe('columns')
|
||
})
|
||
|
||
it('writes the marks switch', () => {
|
||
show(listEl())
|
||
|
||
press('journey.studio.showMarks')
|
||
|
||
expect(saved().showMarks).toBe(false)
|
||
})
|
||
|
||
it('writes both headings', () => {
|
||
show(listEl())
|
||
|
||
fireEvent.change(screen.getByLabelText('journey.editor.pros'), { target: { value: 'Worth it' } })
|
||
expect(saved().proLabel).toBe('Worth it')
|
||
|
||
fireEvent.change(screen.getByLabelText('journey.editor.cons'), { target: { value: 'Less so' } })
|
||
expect(saved().conLabel).toBe('Less so')
|
||
})
|
||
})
|
||
|
||
describe('the tail all five share', () => {
|
||
it('writes the family, and moves the weight to one that family really has', () => {
|
||
const { container } = show(statsEl({ font: 'sans', weight: 700 }))
|
||
open(container, 'journey.studio.typography')
|
||
|
||
// Bebas ships one weight; a bold it does not have prints as a smeared
|
||
// regular, so the request moves rather than being passed through.
|
||
pickFamily('Bebas Neue')
|
||
|
||
expect(saved().font).toBe('bebas')
|
||
expect(saved().weight).toBe(400)
|
||
})
|
||
|
||
it('folds the type of an element set as placed, and opens it on one set otherwise', () => {
|
||
const plain = show(statsEl())
|
||
expect(isOpen(plain.container, 'journey.studio.typography')).toBe(false)
|
||
plain.unmount()
|
||
|
||
for (const over of [{ font: 'serif' }, { weight: 500 }, { textScale: 1.5 }]) {
|
||
const view = show(statsEl(over))
|
||
expect(isOpen(view.container, 'journey.studio.typography')).toBe(true)
|
||
view.unmount()
|
||
}
|
||
})
|
||
|
||
it('keeps a weight the new family does have', () => {
|
||
const { container } = show(statsEl({ font: 'sans', weight: 500 }))
|
||
open(container, 'journey.studio.typography')
|
||
|
||
pickFamily('Lora')
|
||
|
||
expect(saved().font).toBe('serif')
|
||
expect(saved().weight).toBe(500)
|
||
})
|
||
|
||
it('writes the weight', () => {
|
||
const { container } = show(countriesEl())
|
||
open(container, 'journey.studio.typography')
|
||
|
||
// Poppins ships 100 to 900, so 500 is the fifth stop.
|
||
slideWeight([100, 200, 300, 400, 500, 600, 700, 800, 900], 500)
|
||
|
||
expect(saved().weight).toBe(500)
|
||
})
|
||
|
||
it('writes the text scale', () => {
|
||
const { container } = show(listEl())
|
||
open(container, 'journey.studio.typography')
|
||
|
||
pick('1.2×')
|
||
|
||
expect(saved().textScale).toBe(1.2)
|
||
})
|
||
|
||
it('writes the accent and the ink, which are two different rows', () => {
|
||
const { container } = show(statsEl())
|
||
|
||
fireEvent.click(
|
||
within(lineIn(container, 'journey.studio.colour', 'journey.studio.accent'))
|
||
.getByRole('button', { name: '#c2410c' }),
|
||
)
|
||
expect(saved().accent).toBe('#c2410c')
|
||
expect(saved().color).toBe('#1a1a1a')
|
||
|
||
fireEvent.click(
|
||
within(lineIn(container, 'journey.studio.colour', 'journey.studio.text'))
|
||
.getByRole('button', { name: '#1e3a8a' }),
|
||
)
|
||
expect(saved().color).toBe('#1e3a8a')
|
||
expect(saved().accent).toBe('#c2410c')
|
||
})
|
||
|
||
/* A chip's words answer to its fill, until somebody chooses otherwise. */
|
||
it('takes a mark off automatic when its ink is picked, and the switch puts it back', () => {
|
||
const { container } = show(badgeEl())
|
||
|
||
fireEvent.click(
|
||
within(lineIn(container, 'journey.studio.colour', 'journey.studio.text'))
|
||
.getByRole('button', { name: '#ffffff' }),
|
||
)
|
||
expect(saved().color).toBe('#ffffff')
|
||
expect(saved().autoColor).toBe(false)
|
||
|
||
fireEvent.click(
|
||
within(section(container, 'journey.studio.colour'))
|
||
.getByRole('button', { name: 'journey.studio.autoColour' }),
|
||
)
|
||
expect(saved().autoColor).toBe(true)
|
||
})
|
||
})
|
||
|
||
describe('an element the journey has moved past', () => {
|
||
it('says so, and offers to bring it up to date', () => {
|
||
show(statsEl(), stats)
|
||
|
||
expect(screen.getByText('journey.studio.staleHint')).toBeTruthy()
|
||
expect(screen.getByRole('button', { name: 'journey.studio.refresh' })).toBeTruthy()
|
||
})
|
||
|
||
it('writes the journey figures as they stand now, and stops asking', () => {
|
||
show(statsEl(), stats)
|
||
|
||
press('journey.studio.refresh')
|
||
|
||
expect(saved().values).toMatchObject({ distance: 1234, days: 5, steps: 7, photos: 12 })
|
||
expect(saved().stale).toBe(false)
|
||
expect(screen.queryByText('journey.studio.staleHint')).toBeNull()
|
||
})
|
||
|
||
/*
|
||
* Compared rather than timestamped: a page showing distance and days does not
|
||
* go stale because somebody fixed a typo in a caption.
|
||
*/
|
||
it('says nothing while the figures it shows still match', () => {
|
||
show(statsEl({ values: { distance: 1234, days: 5, steps: 7, photos: 12 } }), stats)
|
||
|
||
expect(screen.queryByText('journey.studio.staleHint')).toBeNull()
|
||
})
|
||
})
|