mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 23:46:35 +00:00
317c1ee5ad
The day word of every language now says where the number goes ("DAY {day}",
"{day}日目", "第{day}天", "{day}. GÜN"), and one helper builds the mark for the
engine, the auto layout, a template and the Travel panel, so Japanese,
Korean and Chinese no longer read "日目 3". A word stored without the
placeholder is followed by the number as before. The panel's own day word is
gone with its only reader.
A country mark carries the name as its words beside the outline and a flag
mark sets it small beside the flag, the same for the engine's value, the
panel's tile, the assistant's add and a template's country mark; a flag in a
template keeps the place its name was drawn in. A flag or country mark asked
for by its own code is named after that country without the journey having
to name one.
258 lines
9.2 KiB
TypeScript
258 lines
9.2 KiB
TypeScript
// STTRV-001 to STTRV-006 and STTRV-009: the Travel panel places what the book engine's factories build, so a figure,
|
|
// a map, a country or a mark added by hand is the element the assistant's `add` adds. Only the frame is the panel's
|
|
// own: centred across the spread rather than on the right-hand page.
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { BookDocument, BookElement, BookFrame, BookTravelContext, JourneyStats } from '@trek/shared';
|
|
import {
|
|
BOOK_SATELLITE_TILES,
|
|
bookBadgeValue,
|
|
bookPageSetupSchema,
|
|
bookTravelBadge,
|
|
bookTravelCountries,
|
|
bookTravelMap,
|
|
bookTravelSize,
|
|
bookTravelStats,
|
|
} from '@trek/shared';
|
|
|
|
import { StudioTravelPanel } from '../../../src/components/Studio/StudioTravelPanel';
|
|
import { useStudioStore } from '../../../src/store/studioStore';
|
|
import { fireEvent, render, screen } from '../../helpers/render';
|
|
|
|
// A placed map asks for its roads. The answer never comes, so every map stays as it was placed.
|
|
vi.mock('../../../src/components/Map/RouteCalculator', () => ({
|
|
calculateRouteWithLegs: vi.fn(() => new Promise(() => {})),
|
|
}));
|
|
|
|
const ICELAND = 11;
|
|
const PORTUGAL = 22;
|
|
|
|
const page = bookPageSetupSchema.parse({ preset: 'square-210' });
|
|
|
|
/** A stop on the route, read from a trip's place rather than from an entry. */
|
|
const stop = (
|
|
label: string,
|
|
[lat, lng]: [number, number],
|
|
country: string,
|
|
tripId: number,
|
|
date: string
|
|
): JourneyStats['points'][number] => ({
|
|
lat,
|
|
lng,
|
|
label,
|
|
date,
|
|
country,
|
|
tripId,
|
|
photoId: null,
|
|
entryId: null,
|
|
});
|
|
|
|
const stats: JourneyStats = {
|
|
journeyId: 5,
|
|
distance: 4_000_000,
|
|
days: 21,
|
|
steps: 6,
|
|
photos: 40,
|
|
places: 12,
|
|
furthest: 3_000_000,
|
|
countries: [
|
|
{ code: 'IS', name: 'Iceland', places: 2, firstVisit: '2026-06-02' },
|
|
{ code: 'PT', name: 'Portugal', places: 2, firstVisit: '2026-09-04' },
|
|
],
|
|
points: [
|
|
{ ...stop('Reykjavík', [64.14, -21.94], 'IS', ICELAND, '2026-06-02'), photoId: 3 },
|
|
stop('Akureyri', [65.68, -18.12], 'IS', ICELAND, '2026-06-06'),
|
|
stop('Lisboa', [38.72, -9.14], 'PT', PORTUGAL, '2026-09-04'),
|
|
stop('Porto', [41.15, -8.61], 'PT', PORTUGAL, '2026-09-08'),
|
|
],
|
|
// Named apart from the countries, so a trip card and a country card never share a name.
|
|
trips: [
|
|
{ id: ICELAND, title: 'June', start: '2026-06-02', end: '2026-06-14', points: 2 },
|
|
{ id: PORTUGAL, title: 'September', start: '2026-09-04', end: '2026-09-12', points: 2 },
|
|
],
|
|
excluded: [],
|
|
start: '2026-06-02',
|
|
end: '2026-09-12',
|
|
};
|
|
|
|
/** The recorded way, as the hook hands it to the panel. */
|
|
const PATH: [number, number][][] = [
|
|
[
|
|
[64.14, -21.94],
|
|
[65.68, -18.12],
|
|
],
|
|
[
|
|
[38.72, -9.14],
|
|
[41.15, -8.61],
|
|
],
|
|
];
|
|
|
|
/** What the panel hands the factories: `t` answers with the key in this suite. */
|
|
const travel: BookTravelContext = {
|
|
locale: 'en',
|
|
stats,
|
|
path: PATH,
|
|
entries: [],
|
|
labels: {
|
|
day: 'journey.studio.day',
|
|
stations: 'journey.studio.stations',
|
|
summary: 'journey.studio.summary',
|
|
countries: 'journey.studio.countries',
|
|
distance: 'journey.studio.metric.distance',
|
|
pros: 'journey.editor.pros',
|
|
cons: 'journey.editor.cons',
|
|
},
|
|
};
|
|
|
|
/** A size centred across the open spread, the panel's own placement. */
|
|
const spreadCentre = (size: { w: number; h: number }): BookFrame => ({
|
|
x: (page.pageWidth * 2 - size.w) / 2,
|
|
y: (page.pageHeight - size.h) / 2,
|
|
w: size.w,
|
|
h: size.h,
|
|
});
|
|
|
|
const ANY_FRAME: BookFrame = { x: 0, y: 0, w: 1, h: 1 };
|
|
|
|
/** An element apart from what the panel decides itself: the id it mints and where the frame goes. */
|
|
const content = (el: BookElement) => ({ ...el, id: '', frame: ANY_FRAME });
|
|
|
|
/** The panel on a journey, its words answered by `t`: the key itself unless a case says otherwise. */
|
|
function open(journey: JourneyStats = stats, t: (k: string) => string = (k) => k) {
|
|
return render(
|
|
<StudioTravelPanel
|
|
page={page}
|
|
stats={journey}
|
|
path={PATH}
|
|
t={t}
|
|
locale="en"
|
|
canEdit={false}
|
|
onToggleStop={async () => true}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const card = (name: string) => screen.getByRole('button', { name });
|
|
|
|
/** The element the last click put on the page. */
|
|
function placed(): BookElement {
|
|
const elements = useStudioStore.getState().doc?.spreads[0]?.elements ?? [];
|
|
const last = elements[elements.length - 1];
|
|
if (!last) throw new Error('nothing was placed');
|
|
return last;
|
|
}
|
|
|
|
beforeEach(() => {
|
|
useStudioStore.getState().load({
|
|
version: 1,
|
|
title: 'T',
|
|
page,
|
|
spreads: [{ id: 's1', role: 'inner', background: null, elements: [], parked: [], entryId: null }],
|
|
} as unknown as BookDocument);
|
|
});
|
|
|
|
describe('the Travel panel builds through the book engine', () => {
|
|
it('STTRV-001: places a single figure as the engine builds it, centred across the spread', () => {
|
|
open();
|
|
|
|
fireEvent.click(card('journey.studio.metric.days'));
|
|
|
|
const size = bookTravelSize('stats', page);
|
|
expect(content(placed())).toEqual(
|
|
content(bookTravelStats(stats, { id: 'st', frame: ANY_FRAME, metrics: ['days'] }))
|
|
);
|
|
expect(placed().frame).toEqual(spreadCentre(size));
|
|
});
|
|
|
|
it('STTRV-002: places a journey map with the recorded way, in the style of its card', () => {
|
|
open();
|
|
|
|
fireEvent.click(card('journey.studio.mapStyle.dark'));
|
|
|
|
const engine = bookTravelMap(travel, stats, { id: 'mp', frame: ANY_FRAME, scope: 'journey' });
|
|
expect(content(placed())).toEqual(content({ ...engine, style: 'dark' as const }));
|
|
expect(placed()).toMatchObject({ kind: 'map', style: 'dark', source: 'vector', tripId: null, path: PATH });
|
|
expect(placed().frame).toEqual(spreadCentre(bookTravelSize('map', page)));
|
|
});
|
|
|
|
it('STTRV-003: places a map per trip of that trip alone, with its own countries and no journey trail', () => {
|
|
open();
|
|
|
|
fireEvent.click(card('June'));
|
|
|
|
const engine = bookTravelMap(travel, stats, { id: 'mp', frame: ANY_FRAME, scope: { tripId: ICELAND } });
|
|
expect(content(placed())).toEqual(content(engine));
|
|
expect(placed()).toMatchObject({
|
|
tripId: ICELAND,
|
|
countries: ['IS'],
|
|
path: [],
|
|
points: [{ label: 'Reykjavík', photoId: 3 }, { label: 'Akureyri' }],
|
|
});
|
|
});
|
|
|
|
it('STTRV-004: places the satellite map the engine places, with its tiles, its credit and a white line', () => {
|
|
open();
|
|
|
|
fireEvent.click(card('journey.studio.mapSourceSatellite'));
|
|
|
|
const engine = bookTravelMap(travel, stats, { id: 'mp', frame: ANY_FRAME, scope: 'journey', source: 'satellite' });
|
|
expect(content(placed())).toEqual(content(engine));
|
|
expect(placed()).toMatchObject({ source: 'tiles', tileUrl: BOOK_SATELLITE_TILES, accent: '#ffffff' });
|
|
});
|
|
|
|
it('STTRV-005: places each mark with the words, size and style the engine gives it', () => {
|
|
open();
|
|
|
|
const marks = [
|
|
['journey.studio.dateMark', 'date'],
|
|
['journey.studio.dayMark', 'day'],
|
|
['journey.studio.coordsMark', 'coords'],
|
|
['journey.studio.flagMark', 'flag'],
|
|
['journey.studio.distanceMark', 'distance'],
|
|
] as const;
|
|
for (const [name, variant] of marks) {
|
|
fireEvent.click(card(name));
|
|
const value = bookBadgeValue(travel, variant, null);
|
|
if (!value) throw new Error(`the engine has no ${variant} mark for this journey`);
|
|
expect(content(placed())).toEqual(content(bookTravelBadge({ id: 'bd', frame: ANY_FRAME, variant, value })));
|
|
expect(placed().frame).toEqual(spreadCentre(bookTravelSize('badge', page, { variant })));
|
|
}
|
|
// The last one, read back as words: the distance in an outline.
|
|
expect(placed()).toMatchObject({ variant: 'distance', text: '4,000 km', style: 'outline' });
|
|
});
|
|
|
|
it('STTRV-006: offers no date mark without a start, and places a country and its mark by the name', () => {
|
|
open({ ...stats, start: null });
|
|
|
|
expect(screen.queryByRole('button', { name: 'journey.studio.dateMark' })).toBeNull();
|
|
fireEvent.click(card('journey.studio.dayMark'));
|
|
expect(placed()).toMatchObject({ variant: 'day', text: 'journey.studio.day 1', style: 'chip' });
|
|
|
|
const [outline, mark] = screen.getAllByRole('button', { name: 'Iceland' });
|
|
fireEvent.click(outline);
|
|
expect(content(placed())).toEqual(
|
|
content(bookTravelCountries(travel, { id: 'co', frame: ANY_FRAME, codes: ['IS'] }))
|
|
);
|
|
expect(placed()).toMatchObject({ codes: ['IS'], names: ['Iceland'] });
|
|
|
|
fireEvent.click(mark);
|
|
expect(placed()).toMatchObject({ kind: 'badge', variant: 'country', text: 'Iceland', sub: '', code: 'IS' });
|
|
});
|
|
|
|
it('STTRV-009: places a country mark as the engine makes one, and the day where its word puts it', () => {
|
|
open(stats, (k) => (k === 'journey.studio.day' ? '{day}日目' : k));
|
|
|
|
const [, mark] = screen.getAllByRole('button', { name: 'Iceland' });
|
|
fireEvent.click(mark);
|
|
const value = bookBadgeValue(travel, 'country', null);
|
|
expect(value).toEqual({ text: 'Iceland', sub: '', code: 'IS' });
|
|
if (!value) throw new Error('the engine has no country mark for this journey');
|
|
const engine = bookTravelBadge({ id: 'bd', frame: ANY_FRAME, variant: 'country', value });
|
|
expect(content(placed())).toEqual(content(engine));
|
|
expect(placed().frame).toEqual(spreadCentre(bookTravelSize('badge', page, { variant: 'country' })));
|
|
|
|
fireEvent.click(card('journey.studio.dayMark'));
|
|
expect(placed()).toMatchObject({ variant: 'day', text: '1日目' });
|
|
});
|
|
});
|