fix: the last red checks after the tour, home and place picture merges

A GPX file that mixes routes and tracks gives each its own text: the pieces
are matched to the reader's rows by kind, routes first as the reader gives
them, where they were paired by index and a route got a track's points. The
place pictures reach the network through a small repo instead of the api
module, the wanderer parity case and the tool catalog test allow wanderer,
and the knip and theme baselines follow the lower counts.
This commit is contained in:
Maurice
2026-10-23 22:40:14 +02:00
parent 770fb2a2b3
commit 31dba65906
12 changed files with 151 additions and 46 deletions
@@ -6,12 +6,12 @@ import type { PlacePhotoCandidate } from '@trek/shared';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { buildTripFile } from '../../../tests/helpers/factories';
import { fetchImageAsBlob } from '../../api/authUrl';
import { placePictureRepo } from '../../repo/placePictureRepo';
import { normalizeImageFile } from '../../utils/convertHeic';
import PlaceImagePicker, { PhotoCredit, type PlaceImageOwn } from './PlaceImagePicker';
import type { ChosenImage } from './placeImageChoice';
vi.mock('../../api/authUrl', () => ({ fetchImageAsBlob: vi.fn() }));
vi.mock('../../repo/placePictureRepo', () => ({ placePictureRepo: { attachedImage: vi.fn() } }));
vi.mock('../../utils/convertHeic', () => ({
normalizeImageFile: vi.fn(async (file: File) => new File([file], `normalized-${file.name}`, { type: 'image/jpeg' })),
}));
@@ -81,8 +81,8 @@ function renderPicker(
const fileInput = () => document.querySelector('input[type="file"]') as HTMLInputElement;
beforeEach(() => {
vi.mocked(fetchImageAsBlob).mockReset();
vi.mocked(fetchImageAsBlob).mockResolvedValue('blob:attached');
vi.mocked(placePictureRepo.attachedImage).mockReset();
vi.mocked(placePictureRepo.attachedImage).mockResolvedValue('blob:attached');
vi.mocked(normalizeImageFile).mockClear();
});
@@ -168,19 +168,19 @@ describe('PlaceImagePicker', () => {
renderPicker({ own: mine });
const tile = screen.getByRole('button', { name: 'facade.png' });
await waitFor(() => expect(tile.querySelector('img')).toHaveAttribute('src', 'blob:attached'));
expect(fetchImageAsBlob).toHaveBeenCalledWith('/api/files/41');
expect(placePictureRepo.attachedImage).toHaveBeenCalledWith('/api/files/41');
fireEvent.click(tile);
expect(mine.onPickAttached).toHaveBeenCalledWith(ATTACHED);
});
it('FE-PLANNER-PLIMGPICK-009: a chosen attached image is ticked and named under the grid; one that fails to load stays blank', async () => {
vi.mocked(fetchImageAsBlob).mockRejectedValue(new Error('gone'));
vi.mocked(placePictureRepo.attachedImage).mockRejectedValue(new Error('gone'));
renderPicker({ own: own({ attached: [ATTACHED] }), chosen: { kind: 'attached', fileId: 41 } });
const tile = screen.getByRole('button', { name: 'facade.png' });
expect(tile).toHaveAttribute('aria-pressed', 'true');
expect(screen.getByText('facade.png')).toBeInTheDocument();
await waitFor(() => expect(fetchImageAsBlob).toHaveBeenCalled());
await waitFor(() => expect(placePictureRepo.attachedImage).toHaveBeenCalled());
expect(tile.querySelector('img')).toBeNull();
// Focusing it names it as well, and the name stays once it is left.
@@ -1,7 +1,7 @@
import type { PlacePhotoCandidate } from '@trek/shared';
import { Check, ImageOff, ImagePlus, Loader2 } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
import { fetchImageAsBlob } from '../../api/authUrl';
import { placePictureRepo } from '../../repo/placePictureRepo';
import type { TranslationFn, TripFile } from '../../types';
import { normalizeImageFile } from '../../utils/convertHeic';
import { Tooltip } from '../shared/Tooltip';
@@ -308,7 +308,8 @@ function AttachedTile({
const [src, setSrc] = useState<string | null>(null);
useEffect(() => {
let current = true;
fetchImageAsBlob(file.url)
placePictureRepo
.attachedImage(file.url)
.then((url) => {
if (current) setSrc(url);
})
@@ -13,10 +13,8 @@ import { __clearEnrichmentCacheForTests, readCachedEnrichment, storeEnrichment }
import { usePlaceGallery } from './usePlaceGallery';
const placeEnrichment = vi.fn();
// Partial: the store registry behind resetAllStores needs the real default client.
vi.mock('../../api/client', async (importOriginal) => ({
...(await importOriginal<typeof import('../../api/client')>()),
mapsApi: { placeEnrichment: (...a: unknown[]) => placeEnrichment(...a) },
vi.mock('../../repo/placePictureRepo', () => ({
placePictureRepo: { enrichment: (...a: unknown[]) => placeEnrichment(...a) },
}));
// One toast for every render, the way the real hook memoises it.
const toast = vi.hoisted(() => ({ success: vi.fn(), error: vi.fn(), warning: vi.fn(), info: vi.fn() }));
@@ -1,7 +1,7 @@
import type { PlacePhotoCandidate } from '@trek/shared';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { mapsApi } from '../../api/client';
import { translateApiError, useTranslation } from '../../i18n';
import { placePictureRepo } from '../../repo/placePictureRepo';
import { useAuthStore } from '../../store/authStore';
import type { Place } from '../../types';
import { useToast } from '../shared/Toast';
@@ -117,8 +117,8 @@ export function usePlaceGallery(place: Place, language: string): UsePlaceGallery
// An answer for a place the user has left is kept for that place, but opens nothing and says nothing.
const current = () => shownRef.current === placeId;
setLoadingFor(placeId);
mapsApi
.placeEnrichment({ ...selection, lang: language })
placePictureRepo
.enrichment({ ...selection, lang: language })
.then((result) => {
storeEnrichment(selection, language, result);
if (current()) apply(result.photos);