mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 07:40:59 +00:00
8955790f17
- Top bar: the segmented control keeps to the room between the back button and the trailing button, draws its segments tighter and then scrolls with faded edges instead of covering the back button. On a wide window every list tab puts its primary pill in the middle and its round buttons at the trailing end, so the pill no longer jumps between tabs. - Day strip: a chevron over the faded end pages the days along on a wide window, and the fade reaches further; the chips keep clear of it. - Plan pane: the day card ends on the rail's bottom line beside the map (as tall as its day across the page), scrolls inside a frame and fades the rows at its edges. Addresses and the hotel chip end in an ellipsis instead of being cut mid-letter; the weather chip keeps its width. - Map: in the dark theme the map beside the pane takes the dark basemap. - Places: the All / Unplanned / Planned chips are never narrower than their names and scroll with a faded edge; on a short screen Places/Tours shares their row. - Chat: the list fades out under the floating controls instead of cutting a bubble. Notes and to-do chip rows fade at their edges and New Note stays outside the scrolling chips. - Costs: the balance amounts share a line, the settle-up action wraps to a line of its own, the totals stand under their labels. - Bookings and transports: one column until two cards fit, titles take two lines with the type under them, the time cell is never cut and a range names its unit once. - Files: the preview pane shows a type tile beside the name where no PDF can be drawn in place, its actions keep their labels on one line and wrap as a row, link labels are badges, and a tile that opens its file is no toggle. - Polls get a fill that differs from card and track, packing filters are two segmented controls, empty checkboxes get an edge with 3:1 contrast, and the middle dots between values are gone.
185 lines
8.2 KiB
TypeScript
185 lines
8.2 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { FileText, Image as ImageIcon, Ticket } from 'lucide-react';
|
|
import {
|
|
FILE_FILTERS,
|
|
buildFileLinkLabels,
|
|
formatFileDate,
|
|
getFileTypeMeta,
|
|
matchesFileFilter,
|
|
sortFilesStarredFirst,
|
|
type FileFilterId,
|
|
} from '../../../../src/mobile/screens/trip/tabs/filesModel';
|
|
import { getFileTypeCategory } from '../../../../src/components/Files/fileListRules';
|
|
import { buildPlace, buildReservation, buildTripFile } from '../../../helpers/factories';
|
|
import type { TranslationFn, TripFile } from '../../../../src/types';
|
|
|
|
// FE-MOB-FILM-001 to FE-MOB-FILM-017
|
|
|
|
const t: TranslationFn = key => key;
|
|
|
|
describe('filesModel — type categorisation', () => {
|
|
it('FE-MOB-FILM-001: detects wallet passes ahead of everything else', () => {
|
|
expect(getFileTypeCategory({ mime_type: 'application/vnd.apple.pkpass', original_name: 'boardingpass' })).toBe('pass');
|
|
// extension wins even when the browser sent a generic MIME
|
|
expect(getFileTypeCategory({ mime_type: 'application/octet-stream', original_name: 'ticket.pkpass' })).toBe('pass');
|
|
});
|
|
|
|
it('FE-MOB-FILM-002: detects PDFs and media', () => {
|
|
expect(getFileTypeCategory({ mime_type: 'application/pdf', original_name: 'invoice.pdf' })).toBe('pdf');
|
|
expect(getFileTypeCategory({ mime_type: 'image/jpeg', original_name: 'beach.jpg' })).toBe('image');
|
|
// video folds into the image bucket (#823) since both open in the lightbox
|
|
expect(getFileTypeCategory({ mime_type: 'video/mp4', original_name: 'clip.mp4' })).toBe('image');
|
|
});
|
|
|
|
it('FE-MOB-FILM-003: detects spreadsheets by extension and by MIME', () => {
|
|
expect(getFileTypeCategory({ mime_type: '', original_name: 'budget.xlsx' })).toBe('xls');
|
|
expect(getFileTypeCategory({ mime_type: '', original_name: 'list.csv' })).toBe('xls');
|
|
expect(getFileTypeCategory({ mime_type: '', original_name: 'sheet.ods' })).toBe('xls');
|
|
expect(getFileTypeCategory({ mime_type: 'application/vnd.ms-excel', original_name: 'legacy' })).toBe('xls');
|
|
expect(getFileTypeCategory({ mime_type: 'text/csv', original_name: 'export' })).toBe('xls');
|
|
expect(getFileTypeCategory({ mime_type: 'application/vnd.oasis.opendocument.spreadsheet', original_name: 'x' })).toBe('xls');
|
|
});
|
|
|
|
it('FE-MOB-FILM-004: falls back to "other" for plain attachments', () => {
|
|
expect(getFileTypeCategory({ mime_type: 'application/zip', original_name: 'trip.zip' })).toBe('other');
|
|
expect(getFileTypeCategory({ mime_type: null, original_name: null })).toBe('other');
|
|
});
|
|
|
|
it('FE-MOB-FILM-005: getFileTypeMeta pairs the category with its tile icon, colour and caption', () => {
|
|
expect(getFileTypeMeta({ mime_type: 'application/pdf', original_name: 'a.pdf' })).toEqual({
|
|
category: 'pdf',
|
|
icon: FileText,
|
|
color: '#D6273B',
|
|
label: 'PDF',
|
|
});
|
|
expect(getFileTypeMeta({ mime_type: 'image/png', original_name: 'a.png' }).icon).toBe(ImageIcon);
|
|
expect(getFileTypeMeta({ mime_type: null, original_name: 'a.pkpass' }).icon).toBe(Ticket);
|
|
expect(getFileTypeMeta({ mime_type: 'application/zip', original_name: 'a.zip' }).label).toBe('FILE');
|
|
});
|
|
});
|
|
|
|
describe('filesModel — filters', () => {
|
|
const pdf = buildTripFile({ id: 1, mime_type: 'application/pdf', original_name: 'invoice.pdf' });
|
|
const image = buildTripFile({ id: 2, mime_type: 'image/jpeg', original_name: 'beach.jpg' });
|
|
const sheet = buildTripFile({ id: 3, mime_type: 'text/csv', original_name: 'budget.csv' });
|
|
const pass = buildTripFile({ id: 4, mime_type: 'application/vnd.apple.pkpass', original_name: 'bp.pkpass' });
|
|
const zip = buildTripFile({ id: 5, mime_type: 'application/zip', original_name: 'stuff.zip' });
|
|
const starred = buildTripFile({ id: 6, mime_type: 'application/pdf', original_name: 's.pdf', starred: 1 });
|
|
const collab = buildTripFile({ id: 7, mime_type: 'application/pdf', original_name: 'c.pdf', note_id: 42 });
|
|
const all = [pdf, image, sheet, pass, zip, starred, collab];
|
|
|
|
const idsFor = (filter: FileFilterId) => all.filter(f => matchesFileFilter(f, filter)).map(f => f.id);
|
|
|
|
it('FE-MOB-FILM-006: the filter grid is defined in display order', () => {
|
|
expect(FILE_FILTERS.map(f => f.id)).toEqual(['all', 'pdf', 'image', 'doc', 'starred', 'collab']);
|
|
expect(FILE_FILTERS.every(f => f.labelKey.startsWith('files.filter'))).toBe(true);
|
|
});
|
|
|
|
it('FE-MOB-FILM-007: "all" keeps everything, "pdf" and "image" keep their type', () => {
|
|
expect(idsFor('all')).toEqual([1, 2, 3, 4, 5, 6, 7]);
|
|
expect(idsFor('pdf')).toEqual([1, 6, 7]);
|
|
expect(idsFor('image')).toEqual([2]);
|
|
});
|
|
|
|
it('FE-MOB-FILM-008: "doc" is the catch-all for xls, passes and generic files', () => {
|
|
expect(idsFor('doc')).toEqual([3, 4, 5]);
|
|
});
|
|
|
|
it('FE-MOB-FILM-009: "starred" and "collab" read their flags', () => {
|
|
expect(idsFor('starred')).toEqual([6]);
|
|
expect(idsFor('collab')).toEqual([7]);
|
|
expect(matchesFileFilter(buildTripFile({ starred: 0 }), 'starred')).toBe(false);
|
|
});
|
|
|
|
it('FE-MOB-FILM-010: the filter tiles fully partition the list', () => {
|
|
const covered = new Set([...idsFor('pdf'), ...idsFor('image'), ...idsFor('doc')]);
|
|
expect(covered.size).toBe(all.length);
|
|
});
|
|
|
|
it('FE-MOB-FILM-011: an unknown filter id keeps every file', () => {
|
|
expect(matchesFileFilter(pdf, 'bogus' as FileFilterId)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('filesModel — sorting and dates', () => {
|
|
it('FE-MOB-FILM-012: sortFilesStarredFirst hoists starred files and keeps the rest stable', () => {
|
|
const files = [
|
|
buildTripFile({ id: 1 }),
|
|
buildTripFile({ id: 2, starred: 1 }),
|
|
buildTripFile({ id: 3 }),
|
|
buildTripFile({ id: 4, starred: 1 }),
|
|
];
|
|
const sorted = sortFilesStarredFirst(files);
|
|
|
|
expect(sorted.map(f => f.id)).toEqual([2, 4, 1, 3]);
|
|
expect(files.map(f => f.id)).toEqual([1, 2, 3, 4]);
|
|
});
|
|
|
|
it('FE-MOB-FILM-013: formatFileDate renders a compact day/month label', () => {
|
|
expect(formatFileDate('2026-07-16T10:00:00.000Z', 'en-US')).toMatch(/Jul\s16|16\sJul/);
|
|
expect(formatFileDate('2026-07-16T10:00:00.000Z', 'de-DE')).toMatch(/16\.\s?Juli/);
|
|
});
|
|
|
|
it('FE-MOB-FILM-014: formatFileDate returns an empty string for missing or broken dates', () => {
|
|
expect(formatFileDate(null, 'en-US')).toBe('');
|
|
expect(formatFileDate(undefined, 'en-US')).toBe('');
|
|
expect(formatFileDate('', 'en-US')).toBe('');
|
|
expect(formatFileDate('not-a-date', 'en-US')).toBe('');
|
|
});
|
|
});
|
|
|
|
describe('filesModel — link labels', () => {
|
|
const places = [buildPlace({ id: 11, name: 'Louvre' }), buildPlace({ id: 12, name: 'Eiffel Tower' })];
|
|
const reservations = [
|
|
buildReservation({ id: 21, type: 'flight', title: 'LH123' }),
|
|
buildReservation({ id: 22, type: 'hotel', title: 'Hotel Ritz' }),
|
|
buildReservation({ id: 23, type: 'hotel', title: '' }),
|
|
];
|
|
const transportTypes = new Set(['flight', 'train']);
|
|
|
|
function file(overrides: Partial<TripFile>): TripFile {
|
|
return buildTripFile({ id: 1, ...overrides });
|
|
}
|
|
|
|
it('FE-MOB-FILM-015: merges the single and multi link columns and skips unknown ids', () => {
|
|
const labels = buildFileLinkLabels(
|
|
file({ place_id: 11, linked_place_ids: [11, 12, null, 99] }),
|
|
places,
|
|
reservations,
|
|
transportTypes,
|
|
t,
|
|
);
|
|
// 11 appears in both columns but is listed once; 99 does not exist any more
|
|
expect(labels).toEqual([
|
|
{ kind: 'files.sourcePlan', name: 'Louvre' },
|
|
{ kind: 'files.sourcePlan', name: 'Eiffel Tower' },
|
|
]);
|
|
});
|
|
|
|
it('FE-MOB-FILM-016: splits reservations into transport and booking and appends the collab note', () => {
|
|
const labels = buildFileLinkLabels(
|
|
file({
|
|
reservation_id: 21,
|
|
linked_reservation_ids: [22, 23, null, 98],
|
|
note_id: 5,
|
|
}),
|
|
places,
|
|
reservations,
|
|
transportTypes,
|
|
t,
|
|
);
|
|
|
|
expect(labels).toEqual([
|
|
{ kind: 'files.sourceTransport', name: 'LH123' },
|
|
{ kind: 'files.sourceBooking', name: 'Hotel Ritz' },
|
|
// an untitled reservation is named by its kind alone
|
|
{ kind: 'files.sourceBooking', name: undefined },
|
|
{ kind: 'files.sourceCollab' },
|
|
]);
|
|
});
|
|
|
|
it('FE-MOB-FILM-017: an unlinked file has no labels', () => {
|
|
expect(buildFileLinkLabels(file({}), places, reservations, transportTypes, t)).toEqual([]);
|
|
});
|
|
});
|