mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
fix(ci): parse errors, two leaky tests and the lint ratchets after the parity round
This commit is contained in:
@@ -3,15 +3,15 @@
|
||||
"@typescript-eslint/no-non-null-assertion": 404,
|
||||
"@typescript-eslint/no-misused-promises": 870,
|
||||
"no-console": 88,
|
||||
"complexity": 230,
|
||||
"complexity": 229,
|
||||
"@typescript-eslint/no-explicit-any": 147,
|
||||
"trek/no-swallowed-catch": 132,
|
||||
"trek/no-swallowed-catch": 131,
|
||||
"max-params": 35,
|
||||
"trek/store-without-selector": 69,
|
||||
"react-hooks/refs": 557,
|
||||
"react-hooks/set-state-in-effect": 196,
|
||||
"@typescript-eslint/no-floating-promises": 166,
|
||||
"react-hooks/exhaustive-deps": 119,
|
||||
"react-hooks/exhaustive-deps": 118,
|
||||
"@typescript-eslint/no-unused-vars": 127,
|
||||
"react-hooks/immutability": 28,
|
||||
"max-lines-per-function": 133,
|
||||
@@ -35,7 +35,7 @@
|
||||
},
|
||||
"tests": {
|
||||
"@typescript-eslint/no-explicit-any": 491,
|
||||
"@typescript-eslint/no-non-null-assertion": 1945,
|
||||
"@typescript-eslint/no-non-null-assertion": 1935,
|
||||
"@typescript-eslint/no-misused-promises": 22,
|
||||
"@typescript-eslint/no-unused-vars": 51,
|
||||
"@typescript-eslint/no-floating-promises": 33,
|
||||
|
||||
@@ -11,7 +11,7 @@ import type { AiStatusResponse } from '@trek/shared';
|
||||
import { useLocation } from 'react-router';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { act, fireEvent, render, screen, waitFor } from '../../../tests/helpers/render';
|
||||
import { act, cleanup, fireEvent, render, screen, waitFor } from '../../../tests/helpers/render';
|
||||
import { tripRepo } from '../../repo/tripRepo';
|
||||
import { chatOf, useAssistantStore } from '../../store/assistantStore';
|
||||
import type { Trip } from '../../types';
|
||||
@@ -104,6 +104,9 @@ beforeEach(() => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Unmounted before the store gets its real actions back: a window still on screen would ask the real
|
||||
// loadStatus, whose failed request lands in a later case and hides the assistant there (status null).
|
||||
cleanup();
|
||||
useAssistantStore.setState(INITIAL, true);
|
||||
vi.restoreAllMocks();
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -200,6 +200,6 @@ export function useAiConnections({ phone = false }: { phone?: boolean } = {}) {
|
||||
canOpen: status?.available === true,
|
||||
openAssistant,
|
||||
isAdmin,
|
||||
openAdmin: () => navigate('/admin?tab=addons'),
|
||||
openAdmin: () => void navigate('/admin?tab=addons'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -31,6 +31,25 @@ function saveDismissedSuggestions(userId: number | undefined, dismissed: Set<num
|
||||
window.localStorage.setItem(key, JSON.stringify([...dismissed]))
|
||||
}
|
||||
|
||||
/** The journey events after which the list reads itself again. */
|
||||
const JOURNEY_LIST_EVENTS = ['journey:updated', 'journey:deleted', 'journey:contributor:changed'] as const
|
||||
|
||||
/**
|
||||
* A journey renamed, given other trips or deleted in another tab or through
|
||||
* MCP, or this user taken off one as a contributor: the list reads itself
|
||||
* again, once per burst. The spinner only shows over an empty list, so a
|
||||
* reread of a full one does not flash.
|
||||
*/
|
||||
function useJourneyListReread(loadJourneys: () => Promise<void>): void {
|
||||
useServerPing(JOURNEY_LIST_EVENTS, true, async () => {
|
||||
try {
|
||||
await loadJourneys()
|
||||
} catch {
|
||||
// A failed reread keeps the list on screen; the next event or visit reads it again.
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Journey list page logic — owns the journey store load, the create-journey
|
||||
* modal (title, available trips, selection), the search box, the trip
|
||||
@@ -63,17 +82,7 @@ export function useJourney() {
|
||||
useEffect(() => {
|
||||
loadJourneys()
|
||||
}, [])
|
||||
// A journey renamed, given other trips or deleted in another tab or through
|
||||
// MCP, or this user taken off one as a contributor: the list reads itself
|
||||
// again, once per burst. The spinner only shows over an empty list, so a
|
||||
// reread of a full one does not flash.
|
||||
useServerPing(['journey:updated', 'journey:deleted', 'journey:contributor:changed'], true, async () => {
|
||||
try {
|
||||
await loadJourneys()
|
||||
} catch {
|
||||
// A failed reread keeps the list on screen; the next event or visit reads it again.
|
||||
}
|
||||
})
|
||||
useJourneyListReread(loadJourneys)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
|
||||
@@ -279,7 +279,7 @@ describe('useTripPlanner — bootstrap', () => {
|
||||
await waitFor(() => expect(tripsApi.getMembers).toHaveBeenCalledTimes(1))
|
||||
|
||||
env.forcedOffline = true
|
||||
act(() => { result.current.refreshMembers() })
|
||||
await act(async () => { await result.current.refreshMembers() })
|
||||
|
||||
expect(tripsApi.getMembers).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
@@ -2845,7 +2845,7 @@ describe('useTripPlanner — misc state', () => {
|
||||
owner: { user_id: 1, username: 'owner' },
|
||||
members: [{ user_id: 2 }, { user_id: 3 }],
|
||||
})
|
||||
act(() => { result.current.refreshMembers() })
|
||||
await act(async () => { await result.current.refreshMembers() })
|
||||
|
||||
await waitFor(() => expect(result.current.tripMembers).toHaveLength(3))
|
||||
})
|
||||
|
||||
@@ -67,6 +67,21 @@ import { usePlannerTripView } from './usePlannerTripView'
|
||||
import { useIsPhone } from '../../mobile/useIsPhone'
|
||||
import { useRoadtripVias } from '../../components/Roadtrip/useRoadtripVias'
|
||||
|
||||
/**
|
||||
* The trip's roster from the server: the owner and the members. Null offline
|
||||
* or when it could not be read; the roster on screen then stays, and the next
|
||||
* member event or reload reads it again.
|
||||
*/
|
||||
async function fetchRoster(tripId: number): Promise<TripMember[] | null> {
|
||||
if (!tripId || isEffectivelyOffline()) return null
|
||||
try {
|
||||
const d = await tripsApi.getMembers(tripId)
|
||||
return [d.owner, ...(d.members || [])].filter(Boolean)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trip planner page logic, the big one. Owns the trip store wiring, addon
|
||||
* gating, accommodations/members loading, the tab + resizable-panel + selection
|
||||
@@ -178,13 +193,8 @@ export function useTripPlanner() {
|
||||
// Re-fetch the trip roster so consumers (Costs participants, Collab, …) pick up a
|
||||
// just-added guest or member without a full page reload.
|
||||
const refreshMembers = useCallback(async () => {
|
||||
if (!tripId || isEffectivelyOffline()) return
|
||||
try {
|
||||
const d = await tripsApi.getMembers(tripId)
|
||||
setTripMembers([d.owner, ...(d.members || [])].filter(Boolean))
|
||||
} catch {
|
||||
// The roster on screen stays; the next member event or reload reads it again.
|
||||
}
|
||||
const roster = await fetchRoster(tripId)
|
||||
if (roster) setTripMembers(roster)
|
||||
}, [tripId])
|
||||
// A member added, removed or renamed elsewhere, and the trip going away under us.
|
||||
useTripRosterSync(tripId, refreshMembers)
|
||||
@@ -420,7 +430,8 @@ export function useTripPlanner() {
|
||||
.then(rows => setTripMembers(rows))
|
||||
.catch(() => {})
|
||||
} else {
|
||||
void refreshMembers()
|
||||
// The roster lands from the read's callback, never synchronously inside the effect.
|
||||
void fetchRoster(tripId).then(roster => { if (roster) setTripMembers(roster) })
|
||||
}
|
||||
}
|
||||
}, [tripId])
|
||||
|
||||
@@ -65,6 +65,11 @@ interface NotificationState {
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
|
||||
/** What marking everything read leaves, here or in another tab: every notification read, none to count. */
|
||||
function allRead(state: NotificationState): Partial<NotificationState> {
|
||||
return { notifications: state.notifications.map(n => n.is_read ? n : { ...n, is_read: true }), unreadCount: 0 }
|
||||
}
|
||||
|
||||
export const useInAppNotificationStore = create<NotificationState>((set, get) => ({
|
||||
notifications: [],
|
||||
unreadCount: 0,
|
||||
@@ -130,10 +135,7 @@ export const useInAppNotificationStore = create<NotificationState>((set, get) =>
|
||||
markAllRead: async () => {
|
||||
try {
|
||||
await inAppNotificationsApi.markAllRead()
|
||||
set(state => ({
|
||||
notifications: state.notifications.map(n => ({ ...n, is_read: true })),
|
||||
unreadCount: 0,
|
||||
}))
|
||||
set(allRead)
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
@@ -206,10 +208,5 @@ export const useInAppNotificationStore = create<NotificationState>((set, get) =>
|
||||
},
|
||||
|
||||
// Everything marked read at once elsewhere: the same end state markAllRead leaves.
|
||||
handleAllRead: () => {
|
||||
set(state => ({
|
||||
notifications: state.notifications.map(n => n.is_read ? n : { ...n, is_read: true }),
|
||||
unreadCount: 0,
|
||||
}))
|
||||
},
|
||||
handleAllRead: () => set(allRead),
|
||||
}))
|
||||
|
||||
@@ -148,7 +148,7 @@ describe('remoteEventHandler > broadcast parity', () => {
|
||||
expect(putBudget.mock.calls[0][0]).toEqual([state.budgetItems[0]]);
|
||||
});
|
||||
|
||||
it('FE-BCAST-026: only this user's removal drops the open trip from the offline cache', async () => {
|
||||
it("FE-BCAST-026: only this user's removal drops the open trip from the offline cache", async () => {
|
||||
const drop = vi.spyOn(offlineDb.trips, 'delete');
|
||||
useTripStore.setState({ trip: buildTrip({ id: 5 }) });
|
||||
send({ type: 'member:removed', tripId: 5, userId: 99 });
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"src": {
|
||||
"@typescript-eslint/no-explicit-any": 89,
|
||||
"@typescript-eslint/no-non-null-assertion": 264,
|
||||
"@typescript-eslint/no-explicit-any": 86,
|
||||
"@typescript-eslint/no-non-null-assertion": 263,
|
||||
"@typescript-eslint/no-require-imports": 18,
|
||||
"@typescript-eslint/no-unsafe-function-type": 1,
|
||||
"@typescript-eslint/no-unused-vars": 36,
|
||||
"no-await-in-loop": 563,
|
||||
"no-console": 236,
|
||||
"no-console": 235,
|
||||
"no-empty": 6
|
||||
},
|
||||
"suppressed": {
|
||||
|
||||
@@ -39,14 +39,13 @@ export function memoisedReader(session: ReadSession): (tool: string, args?: Tool
|
||||
const memo = new Map<string, Promise<unknown>>();
|
||||
return (tool, args = {}) => {
|
||||
const key = readKey(tool, args);
|
||||
let read = memo.get(key);
|
||||
if (!read) {
|
||||
read = session.read(tool, args).then(
|
||||
(run) => (run.ok ? run.value : null),
|
||||
() => null,
|
||||
);
|
||||
memo.set(key, read);
|
||||
}
|
||||
const kept = memo.get(key);
|
||||
if (kept !== undefined) return kept;
|
||||
const read = session.read(tool, args).then(
|
||||
(run) => (run.ok ? run.value : null),
|
||||
() => null,
|
||||
);
|
||||
memo.set(key, read);
|
||||
return read;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ import type {
|
||||
CollectionUpdateRequest,
|
||||
CollectionSavePlaceRequest,
|
||||
CollectionSaveResult,
|
||||
CollectionCopyToTripRequest,
|
||||
CollectionCopyToTripRequest, CollectionCopyToTripResponse,
|
||||
CollectionPlaceUpdateRequest,
|
||||
CollectionStatus,
|
||||
CollectionLabel,
|
||||
@@ -1128,7 +1128,7 @@ export class CollectionsService {
|
||||
restTripEvents((this.realtime.broadcast as TripBroadcast).bind(this.realtime), tripId, socketId);
|
||||
|
||||
/** Copy saved places into a trip; after the commit each copy goes out as `place:created`, as in the planner. */
|
||||
async copyToTrip(userId: number, body: CollectionCopyToTripRequest, events: TripEventSink): Promise<{ copied: number; skipped: { id: number; name: string }[] }> {
|
||||
async copyToTrip(userId: number, body: CollectionCopyToTripRequest, events: TripEventSink): Promise<CollectionCopyToTripResponse> {
|
||||
// R6's opposite order from `savePlace`: trip access (AP6) first, then `place_edit`; each keeps its legacy order.
|
||||
const trip = await this.trips.findAccessible(body.trip_id, userId);
|
||||
if (!trip) httpError(404, 'Trip not found');
|
||||
@@ -1158,7 +1158,7 @@ export class CollectionsService {
|
||||
else if (r.lat != null && r.lng != null) dedup.coords.push({ lat: r.lat, lng: r.lng });
|
||||
}
|
||||
|
||||
const copiedIds: number[] = [];
|
||||
const created: CollectionCopyToTripResponse['created'] = []; // each new trip place by the saved place it came from
|
||||
const skipped: { id: number; name: string }[] = [];
|
||||
// The whole copy is one logical write — atomic since the post-fold quirk pass.
|
||||
await this.uow.transactional(async () => {
|
||||
@@ -1183,15 +1183,15 @@ export class CollectionsService {
|
||||
const votes = await this.collectionPlaceRatings.listForPlace(s.id);
|
||||
for (const v of votes) if (tripMemberIds.has(v.user_id)) await this.tripPlaceRatings.insertIgnore(newPlaceId, v.user_id, v.rating);
|
||||
trackInsertedInDedupSet(s, dedup);
|
||||
copiedIds.push(newPlaceId);
|
||||
created.push({ id: newPlaceId, source_id: s.id });
|
||||
}
|
||||
});
|
||||
for (const placeId of copiedIds) {
|
||||
const place = await this.tripPlaces.findWithTagsAndRatings(placeId); // as the place routes send it
|
||||
if (place) events.emit('place:created', { place });
|
||||
await this.places.onCreated(String(body.trip_id), placeId); // a no-op on no day, kept for parity
|
||||
const rows = await Promise.all(created.map(({ id }) => this.tripPlaces.findWithTagsAndRatings(id))); // reads only
|
||||
for (const [i, place] of rows.entries()) {
|
||||
if (place) events.emit('place:created', { place }); // as the place routes send it
|
||||
await this.places.onCreated(String(body.trip_id), created[i].id); // a no-op on no day, kept for parity
|
||||
}
|
||||
return { copied: copiedIds.length, skipped };
|
||||
return { copied: created.length, skipped, created };
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
@@ -1219,7 +1219,7 @@ export class CollectionsService {
|
||||
const role = await this.roleOf(userId, r.collection_id);
|
||||
lists.push({
|
||||
collection_id: r.collection_id,
|
||||
name: r.name,
|
||||
name: r.name, color: r.color,
|
||||
place_id: r.place_id,
|
||||
status: (r.status as CollectionStatus) ?? 'idea',
|
||||
can_edit: role !== null && role !== 'viewer',
|
||||
|
||||
@@ -998,7 +998,7 @@ describe('Link changes reach every tab', () => {
|
||||
/** The one file:updated a link change sent, and the file it carried. */
|
||||
function sentFile() {
|
||||
expect(broadcastMock.mock.calls.map((call) => call[1])).toEqual(['file:updated']);
|
||||
return broadcastMock.mock.calls[0]![2].file as {
|
||||
return broadcastMock.mock.calls[0][2].file as {
|
||||
id: number;
|
||||
linked_place_ids: number[];
|
||||
linked_reservation_ids: number[];
|
||||
@@ -1033,7 +1033,7 @@ describe('Link changes reach every tab', () => {
|
||||
const file = await insertFile(trip.id);
|
||||
await linkFile(orm, file.id, { reservation: reservation.id });
|
||||
await linkFile(orm, file.id, { place: place.id });
|
||||
const doomed = (await linkRows(file.id))[0]!.id;
|
||||
const doomed = (await linkRows(file.id))[0].id;
|
||||
await withHarness(user.id, async (h) => {
|
||||
await h.client.callTool({
|
||||
name: 'unlink_trip_file',
|
||||
|
||||
@@ -333,6 +333,6 @@ describe('Read state reaches every tab of the user', () => {
|
||||
expect(unread.isError).toBe(true);
|
||||
});
|
||||
expect(sent).not.toHaveBeenCalled();
|
||||
expect((await findRow(orm, Notifications, { id: notif.id }))!.is_read).toBe(0);
|
||||
expect((await findRow(orm, Notifications, { id: notif.id }))?.is_read).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1521,7 +1521,7 @@ describe('journey hooks on the MCP create and update paths', () => {
|
||||
});
|
||||
expect(result.isError).toBeFalsy();
|
||||
});
|
||||
expect((await skeletonFor(journey.id, place.id))!.title).toBe("Musée d'Orsay");
|
||||
expect((await skeletonFor(journey.id, place.id))?.title).toBe("Musée d'Orsay");
|
||||
});
|
||||
|
||||
it('MCP-PLACES-JRN-002: bulk_update_places carries a new time into the journey entry of every place', async () => {
|
||||
@@ -1536,8 +1536,8 @@ describe('journey hooks on the MCP create and update paths', () => {
|
||||
});
|
||||
expect((parseToolResult(result) as { count: number }).count).toBe(2);
|
||||
});
|
||||
expect((await skeletonFor(journey.id, place.id))!.entry_time).toBe('10:30');
|
||||
expect((await skeletonFor(journey.id, second.id))!.entry_time).toBe('10:30');
|
||||
expect((await skeletonFor(journey.id, place.id))?.entry_time).toBe('10:30');
|
||||
expect((await skeletonFor(journey.id, second.id))?.entry_time).toBe('10:30');
|
||||
});
|
||||
|
||||
it('MCP-PLACES-JRN-003: set_place_image_from_file runs the journey hook after the new picture', async () => {
|
||||
@@ -1559,7 +1559,7 @@ describe('journey hooks on the MCP create and update paths', () => {
|
||||
expect(result.isError).toBeFalsy();
|
||||
});
|
||||
expect(await imageOf(place.id)).toMatch(/^\/uploads\/places\/[0-9a-f-]+\.jpg$/);
|
||||
expect((await skeletonFor(journey.id, place.id))!.title).toBe('Lighthouse');
|
||||
expect((await skeletonFor(journey.id, place.id))?.title).toBe('Lighthouse');
|
||||
});
|
||||
|
||||
it('MCP-PLACES-JRN-004: create_place hands the new place to the journey hook, as POST /places does', async () => {
|
||||
|
||||
@@ -17,6 +17,7 @@ import { countRows, findRow, findRows, insertRow, updateRows } from '../../helpe
|
||||
import { makeDay } from '../../helpers/factories/trips';
|
||||
import { FakeRealtimeService } from '../../helpers/fake-realtime';
|
||||
import { createMcpHarness, parseToolResult, type McpHarness } from '../../helpers/mcp-harness';
|
||||
import { oneByOne } from '../../helpers/one-by-one';
|
||||
import { resetTestDb } from '../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
|
||||
|
||||
@@ -652,12 +653,12 @@ describe('Transport tools: the booking notice and the AirTrail push', () => {
|
||||
linked = await create('AY1502');
|
||||
const local = await create('LX638');
|
||||
await updateRows(orm, Reservations, { id: linked }, { external_source: 'airtrail', external_id: '501' });
|
||||
for (const reservationId of [linked, local]) {
|
||||
await h.client.callTool({
|
||||
await oneByOne([linked, local], (reservationId) =>
|
||||
h.client.callTool({
|
||||
name: 'update_transport',
|
||||
arguments: { tripId: trip.id, reservationId, status: 'confirmed' },
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(push.mock.calls).toEqual([[linked, trip.id]]);
|
||||
} finally {
|
||||
|
||||
@@ -1040,7 +1040,7 @@ describe('Roster writes, as REST makes them', () => {
|
||||
broadcastMock.mockClear();
|
||||
await h.client.callTool({ name: 'delete_trip_guest', arguments: { tripId: trip.id, guestId } });
|
||||
expect(broadcastMock.mock.calls.map((call) => call[1])).toEqual(['member:removed', 'budget:updated']);
|
||||
const [, , payload] = broadcastMock.mock.calls[1]!;
|
||||
const [, , payload] = broadcastMock.mock.calls[1];
|
||||
const item = payload.item as { id: number; members: { user_id: number }[] };
|
||||
expect(item.id).toBe(shared.id);
|
||||
expect(item.members.map((m) => m.user_id)).toEqual([owner.id]);
|
||||
@@ -1057,7 +1057,7 @@ describe('Roster writes, as REST makes them', () => {
|
||||
broadcastMock.mockClear();
|
||||
await h.client.callTool({ name: 'delete_trip_guest', arguments: { tripId: trip.id, guestId } });
|
||||
expect(broadcastMock.mock.calls.map((call) => call[1])).toEqual(['member:removed', 'budget:updated']);
|
||||
const item = broadcastMock.mock.calls[1]![2].item as { id: number; paid_by_user_id: number | null };
|
||||
const item = broadcastMock.mock.calls[1][2].item as { id: number; paid_by_user_id: number | null };
|
||||
expect(item.id).toBe(legacy.id);
|
||||
expect(item.paid_by_user_id).toBeNull();
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
memoisedReader,
|
||||
} from '../../../../../src/nest/assistant/cards/card-context';
|
||||
import type { ToolCards } from '../../../../../src/nest/assistant/cards/card-types';
|
||||
import { oneByOne } from '../../../../helpers/one-by-one';
|
||||
import type { AssistantMessagePart, TrekCard } from '@trek/shared';
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
@@ -76,7 +77,7 @@ describe('loadFor', () => {
|
||||
const cards: Record<string, ToolCards> = {
|
||||
plan_days: {
|
||||
load: async (_args, read) => {
|
||||
for (const year of [2021, 2022, 2023, 2024, 2025]) seen.push(await read('get_vacay_plan', { year }));
|
||||
seen.push(...(await oneByOne([2021, 2022, 2023, 2024, 2025], (year) => read('get_vacay_plan', { year }))));
|
||||
return seen.length;
|
||||
},
|
||||
},
|
||||
|
||||
@@ -450,7 +450,7 @@ describe('postNote / postMessage', () => {
|
||||
const { service, realtime, send } = await postingSvc();
|
||||
const long = 'x'.repeat(100);
|
||||
const result = await service.postMessage(trip.id, { text: ` ${long} ` }, mcpWriter(service, trip.id, user1.id));
|
||||
expect(result.message!.text).toBe(long);
|
||||
expect(result.message?.text).toBe(long);
|
||||
expect(realtime.broadcastMock).toHaveBeenCalledWith(
|
||||
trip.id,
|
||||
'collab:message:created',
|
||||
|
||||
@@ -50,6 +50,13 @@ const placeCreated = () =>
|
||||
.filter(([, event]) => event === 'place:created')
|
||||
.map(([room, , payload, sid]) => [room, payload, sid]);
|
||||
|
||||
/** The place a save to a collection kept; a save that answered with a warning instead fails the case. */
|
||||
async function saved(userId: number, body: Parameters<CollectionsService['savePlace']>[1]) {
|
||||
const { place } = await svc.savePlace(userId, body);
|
||||
if (!place) throw new Error(`expected ${body.name} to be saved`);
|
||||
return place;
|
||||
}
|
||||
|
||||
describe('CollectionsService.copyToTrip: the trip hears its new places', () => {
|
||||
it('COLLECTIONS-SVC-215: each copied place goes out as place:created with its tags, then runs the journey hook; a skipped one sends nothing', async () => {
|
||||
const { user } = await makeUser(orm);
|
||||
@@ -57,9 +64,9 @@ describe('CollectionsService.copyToTrip: the trip hears its new places', () => {
|
||||
const trip = await makeTrip(orm, user.id);
|
||||
const tag = await makeTag(orm, user.id, { name: 'rooftop' });
|
||||
const col = await svc.createCollection(user.id, { name: 'Evenings' });
|
||||
const bar = (await svc.savePlace(user.id, { collection_id: col.id, name: 'Sky Bar', tag_ids: [tag.id] })).place!;
|
||||
const bar = await saved(user.id, { collection_id: col.id, name: 'Sky Bar', tag_ids: [tag.id] });
|
||||
await makePlace(orm, trip.id, { name: 'Pantheon' });
|
||||
const dup = (await svc.savePlace(user.id, { collection_id: col.id, name: 'Pantheon' })).place!;
|
||||
const dup = await saved(user.id, { collection_id: col.id, name: 'Pantheon' });
|
||||
t.realtime.broadcastMock.mockClear();
|
||||
|
||||
const res = await svc.copyToTrip(
|
||||
@@ -68,8 +75,14 @@ describe('CollectionsService.copyToTrip: the trip hears its new places', () => {
|
||||
svc.tripEvents(trip.id, 'sock-copy'),
|
||||
);
|
||||
|
||||
expect(res).toEqual({ copied: 1, skipped: [{ id: dup.id, name: 'Pantheon' }] });
|
||||
const copy = (await findRow(orm, Places, { trip: trip.id, name: 'Sky Bar' }))!;
|
||||
const copy = await findRow(orm, Places, { trip: trip.id, name: 'Sky Bar' });
|
||||
if (!copy) throw new Error('expected the copy in the trip');
|
||||
// Each new trip place is named with the saved place it came from, for the assistant's "Show on map".
|
||||
expect(res).toEqual({
|
||||
copied: 1,
|
||||
skipped: [{ id: dup.id, name: 'Pantheon' }],
|
||||
created: [{ id: copy.id, source_id: bar.id }],
|
||||
});
|
||||
const sent = placeCreated();
|
||||
expect(sent).toHaveLength(1);
|
||||
// The trip room, the REST socket left out, the place as the place routes send it.
|
||||
@@ -87,7 +100,7 @@ describe('CollectionsService.copyToTrip: the trip hears its new places', () => {
|
||||
const { user: stranger } = await makeUser(orm);
|
||||
await makeCategory(orm);
|
||||
const col = await svc.createCollection(owner.id, { name: 'Mine' });
|
||||
const gem = (await svc.savePlace(owner.id, { collection_id: col.id, name: 'Hidden gem' })).place!;
|
||||
const gem = await saved(owner.id, { collection_id: col.id, name: 'Hidden gem' });
|
||||
const theirs = await makeTrip(orm, stranger.id);
|
||||
t.realtime.broadcastMock.mockClear();
|
||||
|
||||
@@ -103,7 +116,7 @@ describe('CollectionsService.copyToTrip: the trip hears its new places', () => {
|
||||
await makeCategory(orm);
|
||||
const trip = await makeTrip(orm, user.id);
|
||||
const col = await svc.createCollection(user.id, { name: 'Plugin' });
|
||||
const spot = (await svc.savePlace(user.id, { collection_id: col.id, name: 'Viewpoint' })).place!;
|
||||
const spot = await saved(user.id, { collection_id: col.id, name: 'Viewpoint' });
|
||||
t.realtime.broadcastMock.mockClear();
|
||||
|
||||
await svc.copyToTrip(user.id, { trip_id: trip.id, place_ids: [spot.id] }, svc.tripEvents(trip.id, undefined));
|
||||
|
||||
@@ -557,8 +557,13 @@ describe('HikeRoutingService: the hike created from a proposal', () => {
|
||||
const savedLine = async (placeId: number) =>
|
||||
JSON.parse(String((await findRow(orm, Places, { id: placeId }))?.route_geometry)) as number[][];
|
||||
|
||||
// The router would now plan another way, and its own caches (five minutes) have let the first go.
|
||||
valhalla({ route: () => json(valhallaRoute([{ line: stepsOf(0, 20, 11.01), km: 0.222, seconds: 330 }])) });
|
||||
// The router would now plan another way, about 76 m east (within the snap limit, one leg per pair of points as
|
||||
// Valhalla answers), and its own caches (five minutes) have let the first go.
|
||||
const otherWay = valhallaRoute([
|
||||
{ line: stepsOf(0, 10, 11.001), km: 0.111, seconds: 150 },
|
||||
{ line: stepsOf(10, 20, 11.001), km: 0.111, seconds: 180 },
|
||||
]);
|
||||
valhalla({ route: () => json(otherWay) });
|
||||
vi.advanceTimersByTime(6 * 60_000);
|
||||
const created = await settled(hiking.create(user.id, tripRow.id, request));
|
||||
expect(asked('/route')).toHaveLength(0);
|
||||
@@ -571,6 +576,6 @@ describe('HikeRoutingService: the hike created from a proposal', () => {
|
||||
vi.advanceTimersByTime(10 * 60_000);
|
||||
const later = await settled(hiking.create(user.id, tripRow.id, { ...request, name: 'Later' }));
|
||||
expect(asked('/route')).toHaveLength(1);
|
||||
expect((await savedLine(later.tour.place_id)).every(([, lng]) => lng === 11.01)).toBe(true);
|
||||
expect((await savedLine(later.tour.place_id)).every(([, lng]) => lng === 11.001)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -16,15 +16,17 @@ const user = { id: 5 } as User;
|
||||
* sends is held by tools-tags-maps-weather.test.ts and the broadcast parity suite.
|
||||
*/
|
||||
function writesFor(svc: Partial<TagsService>) {
|
||||
// As the controller gets it: a case hands in the methods its route reaches.
|
||||
const tags = svc as TagsService;
|
||||
return {
|
||||
create: vi.fn((userId: number, name: string, color: string | undefined, _socketId: string | undefined) =>
|
||||
svc.create!(userId, name, color),
|
||||
tags.create(userId, name, color),
|
||||
),
|
||||
update: vi.fn(
|
||||
(_userId: number, id: string, changes: { name?: string; color?: string }, _socketId: string | undefined) =>
|
||||
svc.update!(id, changes.name, changes.color),
|
||||
tags.update(id, changes.name, changes.color),
|
||||
),
|
||||
remove: vi.fn((_userId: number, id: string, _socketId: string | undefined) => svc.remove!(id)),
|
||||
remove: vi.fn((_userId: number, id: string, _socketId: string | undefined) => tags.remove(id)),
|
||||
};
|
||||
}
|
||||
let writes: ReturnType<typeof writesFor>;
|
||||
|
||||
@@ -68,7 +68,7 @@ describe('the blank book and the fixed pages', () => {
|
||||
expect(doc.title.length).toBeLessThan(500);
|
||||
});
|
||||
|
||||
it('BOOKPG-022: lays a new book down in the format it is asked for, at that format's size', () => {
|
||||
it("BOOKPG-022: lays a new book down in the format it is asked for, at that format's size", () => {
|
||||
const doc = blankBook({ title: 'Iceland', preset: 'a4-landscape', newId });
|
||||
expect(doc.page).toMatchObject({ preset: 'a4-landscape', pageWidth: 297, pageHeight: 210, bleed: 3, safe: 5 });
|
||||
expect(doc.page.pageNumbers.startAt).toBe(1);
|
||||
|
||||
Reference in New Issue
Block a user