Files
TREK/client/e2e/help/fixtures-trip-lists.ts
T
Maurice 31859651d7 feat(help): rewrite the help center for v5 with a Tours section, new pictures in every language, and fix the tour planner bugs it surfaced
Every help screen checked against the v5 redesign. Tours is documented for
the first time, as a screen of its own with twelve screens under it, and
notifications get help of their own. All pictures are taken again on a
realistic demo trip, with short loops where a still cannot show the motion.
The English is plain and every language is written naturally rather than
word for word.

The switcher shows two levels at most, a screen's overview lists its
sub-screens as cards above its guides, each wiki page is linked once, and
the help sidebar is wider.

Fixes found along the way: Save to Collection keeps a tour's route, a track
dropped on the places column opens the Tours import, tour Share on desktop,
the unsaved tour draft lives on the account, a route click no longer adds
an end point, Trip places leaves the trip's tours out, Escape closes a
stage's day menu, confirms before destructive deletes, DWD warnings sharing
one id kept apart, a booking's date moves with it, and smaller fixes across
notifications, bookings, the day plan, stays, Atlas, Collections, Vacay and
the admin screens.
2026-10-22 22:29:35 +02:00

136 lines
5.3 KiB
TypeScript

import type { APIRequestContext } from '@playwright/test';
import { seededTrip } from './fixtures';
/**
* Putting the packing list back the way the seed left it.
*
* The seed's list is long (six lists, most of it ticked) and several guides
* change it in ways that are hard to undo one by one: a list dragged to the
* front, pieces counted in, a weight typed, items imported or brought in by a
* template. So the trip-lists script takes one snapshot of the list before the
* first guide and every packing guide puts that snapshot back, before it starts
* and again when it is done. Nothing here hard-codes the seed's names, so a
* richer seed needs no change in this file.
*/
export interface PackingSnapshotRow {
id: number;
name: string;
category: string | null;
checked: number;
quantity: number;
weight_grams: number | null;
packed_quantity: number | null;
bag_id: number | null;
is_private: number;
sort_order: number;
}
type RawRow = Partial<PackingSnapshotRow> & { id: number; name: string };
function normalise(row: RawRow): PackingSnapshotRow {
return {
id: row.id,
name: row.name,
category: row.category ?? null,
checked: row.checked ? 1 : 0,
quantity: row.quantity || 1,
weight_grams: row.weight_grams ?? null,
packed_quantity: row.packed_quantity ?? null,
bag_id: row.bag_id ?? null,
is_private: row.is_private ? 1 : 0,
sort_order: row.sort_order ?? 0,
};
}
async function packingRows(api: APIRequestContext): Promise<PackingSnapshotRow[]> {
const { tripId } = seededTrip();
const res = await api.get(`/api/trips/${tripId}/packing`);
if (!res.ok()) throw new Error(`could not read the packing list: ${res.status()} ${await res.text()}`);
const body = (await res.json()) as { items?: RawRow[] };
return (body.items ?? []).map(normalise).sort((a, b) => a.sort_order - b.sort_order || a.id - b.id);
}
/** The packing list as it is now, in its order. */
export async function snapshotPacking(api: APIRequestContext): Promise<PackingSnapshotRow[]> {
return packingRows(api);
}
const differs = (a: PackingSnapshotRow, b: PackingSnapshotRow): boolean =>
a.name !== b.name ||
a.category !== b.category ||
a.checked !== b.checked ||
a.quantity !== b.quantity ||
a.weight_grams !== b.weight_grams ||
a.packed_quantity !== b.packed_quantity ||
a.bag_id !== b.bag_id;
/**
* Make the trip's packing list equal to the snapshot again: what a guide added
* goes, what it deleted comes back, ticks, quantities, counts and weights are
* written back, and the order is restored. Cheap when nothing changed: one read.
*/
export async function restorePacking(api: APIRequestContext, snapshot: PackingSnapshotRow[]): Promise<void> {
const { tripId } = seededTrip();
const current = await packingRows(api);
const wanted = new Set(snapshot.map((row) => row.id));
for (const row of current.filter((r) => !wanted.has(r.id))) {
await api.delete(`/api/trips/${tripId}/packing/${row.id}`);
}
const ids: number[] = [];
for (const row of snapshot) {
const now = current.find((r) => r.id === row.id);
if (now) {
ids.push(row.id);
if (!differs(now, row)) continue;
// Naming `checked` resets the piece counter on the server, which is what a
// seeded row has: none. A partial count is written after it when there was one.
const data: Record<string, unknown> = {
name: row.name,
checked: row.checked,
quantity: row.quantity,
weight_grams: row.weight_grams,
};
if (row.category) data.category = row.category;
if (now.bag_id !== row.bag_id) data.bag_id = row.bag_id;
const res = await api.put(`/api/trips/${tripId}/packing/${row.id}`, { data });
if (!res.ok()) throw new Error(`could not restore "${row.name}": ${res.status()} ${await res.text()}`);
if (row.packed_quantity != null) {
await api.put(`/api/trips/${tripId}/packing/${row.id}`, { data: { packed_quantity: row.packed_quantity } });
}
continue;
}
const created = await api.post(`/api/trips/${tripId}/packing`, {
data: {
name: row.name,
...(row.category ? { category: row.category } : {}),
visibility: row.is_private ? 'personal' : 'common',
quantity: row.quantity,
weight_grams: row.weight_grams,
checked: row.checked,
},
});
if (!created.ok())
throw new Error(`could not re-create "${row.name}": ${created.status()} ${await created.text()}`);
const { item } = (await created.json()) as { item: { id: number } };
ids.push(item.id);
}
const after = await packingRows(api);
const inOrder = after.map((r) => r.id);
if (inOrder.length === ids.length && inOrder.every((id, i) => id === ids[i])) return;
await api.put(`/api/trips/${tripId}/packing/reorder`, { data: { orderedIds: ids } });
}
/** Untick one item for a guide that starts from an item still to pack. */
export async function untickPacking(api: APIRequestContext, name: string): Promise<void> {
const { tripId } = seededTrip();
const row = (await packingRows(api)).find((r) => r.name === name);
if (!row) throw new Error(`no packing item "${name}" in the seed`);
if (!row.checked) return;
const res = await api.put(`/api/trips/${tripId}/packing/${row.id}`, { data: { checked: 0 } });
if (!res.ok()) throw new Error(`could not untick "${name}": ${res.status()} ${await res.text()}`);
}