From cab742e641a453620b16160b2553e63bed84af65 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 15 Oct 2026 19:25:22 +0200 Subject: [PATCH] Tours: bring the pinned counts and test doubles up to the new tables and switch - tours locale key count, entity count and NOT NULL field count follow the wanderer tables and strings - the shared MSW admin handlers answer /api/admin/tours-wanderer - the mobile tour detail test checks the dialog, not the dropped heading - the refused-origin e2e sends one request per case instead of four at once --- .../screens/trip/sheets/MTripSheets.test.tsx | 5 ++-- client/tests/helpers/msw/handlers/admin.ts | 9 ++++++ server/tests/e2e/tours.e2e.test.ts | 30 +++++++++---------- server/tests/unit/db/entities-index.test.ts | 6 ++-- .../tests/unit/db/entity-class-fields.test.ts | 2 +- shared/src/i18n/tours-locales.spec.ts | 4 +-- 6 files changed, 33 insertions(+), 23 deletions(-) diff --git a/client/src/mobile/screens/trip/sheets/MTripSheets.test.tsx b/client/src/mobile/screens/trip/sheets/MTripSheets.test.tsx index da688d706..615288ea9 100644 --- a/client/src/mobile/screens/trip/sheets/MTripSheets.test.tsx +++ b/client/src/mobile/screens/trip/sheets/MTripSheets.test.tsx @@ -98,13 +98,14 @@ describe('MSelectedTourDetail', () => { it('renders the selected tour through the shared detail dialog and elevation profile', () => { render() - expect(screen.getAllByText('Track Stats')).toHaveLength(1) + expect(screen.getByRole('dialog')).toBeInTheDocument() expect(document.querySelector('#tour-elevation-42')).toBeInTheDocument() }) it('renders nothing for an ordinary selected place', () => { render() - expect(screen.queryByText('Track Stats')).not.toBeInTheDocument() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + expect(document.querySelector('#tour-elevation-42')).not.toBeInTheDocument() }) }) \ No newline at end of file diff --git a/client/tests/helpers/msw/handlers/admin.ts b/client/tests/helpers/msw/handlers/admin.ts index 35d245c67..e90eb1872 100644 --- a/client/tests/helpers/msw/handlers/admin.ts +++ b/client/tests/helpers/msw/handlers/admin.ts @@ -76,6 +76,15 @@ export const adminHandlers = [ return HttpResponse.json({ enabled: body.enabled }); }), + http.get('/api/admin/tours-wanderer', () => { + return HttpResponse.json({ enabled: false }); + }), + + http.put('/api/admin/tours-wanderer', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ enabled: body.enabled }); + }), + http.get('/api/admin/addons', () => { return HttpResponse.json({ addons: [] }); }), diff --git a/server/tests/e2e/tours.e2e.test.ts b/server/tests/e2e/tours.e2e.test.ts index 0ff0a7a0c..b29b48cba 100644 --- a/server/tests/e2e/tours.e2e.test.ts +++ b/server/tests/e2e/tours.e2e.test.ts @@ -536,21 +536,21 @@ describe('Tours e2e (real guards + temp SQLite)', () => { expect(await listed(derived)).toMatchObject({ derived_from_place_id: null }); }); - it('400 for an origin that is a planned tour, a tour of another trip or no tour at all', async () => { - const planned = await createViaApi(); - const foreign = await importedTour(6); - const plain = await plainPlace(5, 'Cafe'); - // One request per origin at once; each answers on its own. - const answers = await Promise.all( - [planned, foreign, plain, 999_999].map((id) => - request(server) - .post('/api/trips/5/tours') - .set('Cookie', sessionCookie(OWNER)) - .send(tourBody({ derived_from_place_id: id })), - ), - ); - expect(answers.map((res) => res.status)).toEqual([400, 400, 400, 400]); - expect(await countRows(orm, Tours, { place: { $nin: [planned, foreign] } })).toBe(0); + const refusedOrigins: Record Promise> = { + 'a planned tour': () => createViaApi(), + 'a tour of another trip': () => importedTour(6), + 'no tour at all': () => plainPlace(5, 'Cafe'), + 'a place that does not exist': async () => 999_999, + }; + it.each(Object.keys(refusedOrigins))('400 for an origin that is %s, and nothing is written', async (kind) => { + const origin = await refusedOrigins[kind](); + const before = await countRows(orm, Tours, {}); + const res = await request(server) + .post('/api/trips/5/tours') + .set('Cookie', sessionCookie(OWNER)) + .send(tourBody({ derived_from_place_id: origin })); + expect(res.status).toBe(400); + expect(await countRows(orm, Tours, {})).toBe(before); }); it('planning an import over in place keeps its recorded track, and restoring brings it back', async () => { diff --git a/server/tests/unit/db/entities-index.test.ts b/server/tests/unit/db/entities-index.test.ts index 94b83b671..2547bfd07 100644 --- a/server/tests/unit/db/entities-index.test.ts +++ b/server/tests/unit/db/entities-index.test.ts @@ -30,7 +30,7 @@ describe('ALL_ENTITIES', () => { }); /** - * Plan 2 Task 3: every one of the 133 entities the migrated schema + * Plan 2 Task 3: every one of the 135 entities the migrated schema * describes (120 rewritten + the 5 document-sync tables added this task, * + push_subscriptions and google_api_usage from legacy steps 245 and 254, * + tour_types, tours and tour_waypoints from the Tours migration, @@ -42,8 +42,8 @@ describe('ALL_ENTITIES', () => { * `entity-schema-parity.test.ts` can't see — it iterates the metadata map, * which de-duplicates by class name). */ - it('ENT-003: exactly 133 entities, every class name unique', () => { - expect(ALL_ENTITIES.length).toBe(133); + it('ENT-003: exactly 135 entities, every class name unique', () => { + expect(ALL_ENTITIES.length).toBe(135); const names = ALL_ENTITIES.map((s) => s.meta.className); expect(new Set(names).size).toBe(names.length); }); diff --git a/server/tests/unit/db/entity-class-fields.test.ts b/server/tests/unit/db/entity-class-fields.test.ts index 8a3dfc189..bb6c47156 100644 --- a/server/tests/unit/db/entity-class-fields.test.ts +++ b/server/tests/unit/db/entity-class-fields.test.ts @@ -26,7 +26,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest'; // + scheduler_leases: name, owner, expires_at (the scheduler always writes all three). // + user_sessions: id, user_id, expires_at. // + ferrata_tiles: tile_key, payload, fetched_at (a fetched tile always carries all three). -const KNOWN_UNINITIALISED_NOT_NULL_SCALARS = 449; +const KNOWN_UNINITIALISED_NOT_NULL_SCALARS = 455; describe('entity class fields never carry a NaN initialiser (task-4-review-shape.md, Important 1)', () => { it('CLASSFIELD-001: new X() has no own property whose value is NaN, across all 133 entities', () => { diff --git a/shared/src/i18n/tours-locales.spec.ts b/shared/src/i18n/tours-locales.spec.ts index 6be18367c..772950e7b 100644 --- a/shared/src/i18n/tours-locales.spec.ts +++ b/shared/src/i18n/tours-locales.spec.ts @@ -20,7 +20,7 @@ const tokens = (value: string) => [...value.matchAll(/\{[^{}]+\}/g)].map((match) // placeholder spec; here only the general keys are. const groups = pluralGroups(Object.keys(english)); const generalKeys = (keys: string[]) => keys.filter((key) => !pluralFormOf(key, groups)); -const GENERAL_KEY_COUNT = 154; +const GENERAL_KEY_COUNT = 196; function literalKeys(source: string): string[] { const tree = ts.createSourceFile('locale.ts', source, ts.ScriptTarget.Latest, true); @@ -34,7 +34,7 @@ function literalKeys(source: string): string[] { } describe('Tours locale contracts', () => { - it('covers all 27 supported locales with 154 general source keys', () => { + it('covers all 27 supported locales with 196 general source keys', () => { expect(Object.keys(domains)).toHaveLength(27); expect(Object.keys(domains).sort()).toEqual(SUPPORTED_LANGUAGE_CODES.map((code) => `./${code}/tours.ts`).sort()); expect(generalKeys(Object.keys(english))).toHaveLength(GENERAL_KEY_COUNT);