fix(ci): parse errors, two leaky tests and the lint ratchets after the parity round

This commit is contained in:
Maurice
2026-10-22 12:44:07 +02:00
parent 8d4d3423ff
commit 76c7323e23
22 changed files with 128 additions and 87 deletions
+4 -4
View File
@@ -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'),
};
}
+20 -11
View File
@@ -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))
})
+19 -8
View File
@@ -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])
+7 -10
View File
@@ -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 });
+3 -3
View File
@@ -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',
+2 -2
View File
@@ -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);
});
});
+4 -4
View File
@@ -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>;
+1 -1
View File
@@ -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);