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);
+33
View File
@@ -0,0 +1,33 @@
// FE-PLACE-PICTURE-REPO-001 to FE-PLACE-PICTURE-REPO-002: the pictures a place can be given,
// asked of the maps route and the attached files behind the repository.
import type { MapsPlaceEnrichmentResult } from '@trek/shared';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { fetchImageAsBlob } from '../api/authUrl';
import { mapsApi } from '../api/client';
import { placePictureRepo } from './placePictureRepo';
vi.mock('../api/client', () => ({ mapsApi: { placeEnrichment: vi.fn() } }));
vi.mock('../api/authUrl', () => ({ fetchImageAsBlob: vi.fn() }));
beforeEach(() => {
vi.clearAllMocks();
});
describe('place pictures', () => {
it('FE-PLACE-PICTURE-REPO-001: asks the maps route for the enrichment with the query and the signal', async () => {
const answer = { photos: [] } as unknown as MapsPlaceEnrichmentResult;
vi.mocked(mapsApi.placeEnrichment).mockResolvedValue(answer);
const signal = new AbortController().signal;
const query = { placeId: 'way:2', lat: 48.1, lng: 11.5, name: 'Rathaus', lang: 'de' };
await expect(placePictureRepo.enrichment(query, signal)).resolves.toBe(answer);
expect(mapsApi.placeEnrichment).toHaveBeenCalledWith(query, signal);
});
it('FE-PLACE-PICTURE-REPO-002: reads an attached image as an object URL', async () => {
vi.mocked(fetchImageAsBlob).mockResolvedValue('blob:attached');
await expect(placePictureRepo.attachedImage('/api/files/41')).resolves.toBe('blob:attached');
expect(fetchImageAsBlob).toHaveBeenCalledWith('/api/files/41');
});
});
+30
View File
@@ -0,0 +1,30 @@
import type { MapsPlaceEnrichmentResult } from '@trek/shared';
import { fetchImageAsBlob } from '../api/authUrl';
import { mapsApi } from '../api/client';
/** What the sources are asked for one place: where it is, what it is called, in which language. */
interface PlaceEnrichmentQuery {
placeId?: string;
lat: number;
lng: number;
name: string;
lang?: string;
details?: Record<string, unknown>;
}
/**
* The pictures a place can be given: the sources' photos and the images
* attached to it. Online only by nature, and nothing here is cached on the
* device; the session cache for enrichment lives with the planner view.
*/
export const placePictureRepo = {
/** The sources' pictures and description for a place, as the maps route answers them. */
enrichment(query: PlaceEnrichmentQuery, signal?: AbortSignal): Promise<MapsPlaceEnrichmentResult> {
return mapsApi.placeEnrichment(query, signal);
},
/** An attached image as an object URL the view can show; empty when it could not be read. */
attachedImage(url: string): Promise<string> {
return fetchImageAsBlob(url);
},
};