mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 16:03:02 +00:00
a7b53988ff
The travel tab is four tabs now, figures, maps, countries and marks, and offers only the newer looks, each dressed in the book's theme and linked to it. The looks books already hold still draw on their pages exactly as before; they are just no longer offered. - Figures: a summary as a poster, a ticket, tiles, a road sign, an odometer, a handwritten note, a pencil sketch or a band, and each figure on its own as a medal, on odometer drums or ringed by hand. - Maps: the route with its photographs, its stops numbered, its two ends marked or on a dark ground, over satellite or relief imagery, a map per trip, and a map of each country with its stops alone, cut to its shape. - Countries: each country as a passport stamp, a visa, an airmail postcard, its flag in cloth, a map pin, a luggage label or a sketch. - Marks: the date, the day, the distance, the coordinates and the flag as stamps, tags, pills, ribbons, die-cut stickers, embroidered patches and sketches. The schema gains a summary's and a country's look, seven mark styles and the numbered and ends stops of a map, all additive, as revision 11. Also in Studio: the save state is a tile beside who is here, a book a newer Studio saved is said by an overlay that holds the whole Studio until it reloads, and the page rail draws the first and the last page beside the blank inside of their cover. The notifications page and the bell's panel take the dialogs' look: a head with a tile, cards with an unread dot, and tools that come forward on hover.
124 lines
4.3 KiB
TypeScript
124 lines
4.3 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import type { BookRecord } from '@trek/shared'
|
|
import { fireEvent, render, screen } from '../../helpers/render'
|
|
import { NewerBookOverlay, SaveIndicator } from '../../../src/components/Studio/SaveIndicator'
|
|
|
|
/**
|
|
* The save status (#1973).
|
|
*
|
|
* Autosave without a sign of it is a promise the user has to take on faith, and
|
|
* the moment they stop doing that is the moment they close the tab to check. So
|
|
* the interesting cases here are the two that are not "fine": an error the user
|
|
* can retry, and a conflict that has to offer a real choice rather than
|
|
* announcing that one exists.
|
|
*/
|
|
|
|
const t = (k: string) => k
|
|
|
|
const current = (over: Partial<BookRecord> = {}): BookRecord => ({
|
|
id: 3, journeyId: 9, title: 'Theirs', version: 6,
|
|
updatedAt: '2026-08-19 10:00:00', updatedBy: 2,
|
|
document: { version: 1, title: 'Theirs', page: {}, spreads: [] },
|
|
...over,
|
|
} as BookRecord)
|
|
|
|
function draw(state: Parameters<typeof SaveIndicator>[0]['state'], handlers = {}) {
|
|
const props = {
|
|
state, t,
|
|
onAcceptTheirs: vi.fn(), onKeepMine: vi.fn(), onRetry: vi.fn(),
|
|
...handlers,
|
|
}
|
|
render(<SaveIndicator {...props} />)
|
|
return props
|
|
}
|
|
|
|
describe('the quiet states', () => {
|
|
/*
|
|
* Idle shows nothing at all. A status that is always there is a status people
|
|
* learn to stop reading, which is exactly the wrong habit for the one time it
|
|
* says something else.
|
|
*/
|
|
it('renders nothing while idle', () => {
|
|
const { container } = render(
|
|
<SaveIndicator
|
|
state={{ status: 'idle' }} t={t}
|
|
onAcceptTheirs={vi.fn()} onKeepMine={vi.fn()} onRetry={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(container.textContent).toBe('')
|
|
})
|
|
|
|
it('says it is saving, and then that it saved', () => {
|
|
const { unmount } = render(
|
|
<SaveIndicator
|
|
state={{ status: 'saving' }} t={t}
|
|
onAcceptTheirs={vi.fn()} onKeepMine={vi.fn()} onRetry={vi.fn()}
|
|
/>,
|
|
)
|
|
expect(screen.getByText('journey.studio.saving')).toBeTruthy()
|
|
unmount()
|
|
|
|
draw({ status: 'saved', at: 0 })
|
|
expect(screen.getByText('journey.studio.saved')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('an error', () => {
|
|
it('is a button, because the way out is to try again', () => {
|
|
const props = draw({ status: 'error' })
|
|
fireEvent.click(screen.getByText('journey.studio.saveFailed'))
|
|
expect(props.onRetry).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
describe('a conflict', () => {
|
|
/*
|
|
* There is no merge here, and pretending otherwise would be worse than saying
|
|
* so: one of these two documents is going to be the book. Both ways out are
|
|
* on screen, stated plainly.
|
|
*/
|
|
it('offers both ways out, and hands back the record it was given', () => {
|
|
const record = current()
|
|
const props = draw({ status: 'conflict', current: record })
|
|
|
|
expect(screen.getByText('journey.studio.saveConflict')).toBeTruthy()
|
|
|
|
fireEvent.click(screen.getByText('journey.studio.saveTakeTheirs'))
|
|
expect(props.onAcceptTheirs).toHaveBeenCalledWith(record)
|
|
|
|
fireEvent.click(screen.getByText('journey.studio.saveKeepMine'))
|
|
expect(props.onKeepMine).toHaveBeenCalledWith(record)
|
|
})
|
|
|
|
/*
|
|
* Announced, not just drawn. Losing an edit to someone else's save is the one
|
|
* thing here a screen-reader user must not have to notice by scanning.
|
|
*/
|
|
it('is announced rather than only drawn', () => {
|
|
draw({ status: 'conflict', current: current() })
|
|
expect(screen.getByRole('alert')).toBeTruthy()
|
|
})
|
|
})
|
|
|
|
describe('a book a newer Studio saved', () => {
|
|
/*
|
|
* Nothing done in this tab is kept until it reloads, so the bar says nothing
|
|
* of its own and the overlay over the whole Studio offers the one way out.
|
|
*/
|
|
it('leaves the bar to the overlay, which says why, takes the focus and reloads on request', () => {
|
|
const { container } = render(
|
|
<SaveIndicator state={{ status: 'newer' }} t={t} onAcceptTheirs={vi.fn()} onKeepMine={vi.fn()} onRetry={vi.fn()} />,
|
|
)
|
|
expect(container.textContent).toBe('')
|
|
|
|
const onReload = vi.fn()
|
|
render(<NewerBookOverlay t={t} onReload={onReload} />)
|
|
expect(screen.getByRole('alertdialog')).toBeTruthy()
|
|
expect(screen.getByText('journey.studio.saveNewer')).toBeTruthy()
|
|
const reload = screen.getByRole('button', { name: 'journey.studio.saveReload' })
|
|
expect(document.activeElement).toBe(reload)
|
|
fireEvent.click(reload)
|
|
expect(onReload).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|