// The Tours file preview's handling of a file without a line: named in a toast by
// default, handed on when the caller takes such files (the places column does).
// Without a translation provider `t` echoes its key.
import { act, renderHook } from '@testing-library/react';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { resetAllStores } from '../../../tests/helpers/store';
import { placesApi } from '../../api/client';
import { tourRepo } from '../../repo/tourRepo';
import { useTripStore } from '../../store/tripStore';
import { useTourGpxImport } from './useTourGpxImport';
// The toasts are caught at useToast, so no window global has to be set for them.
const { addToast, toast } = vi.hoisted(() => {
const addToast = vi.fn((_message: string, _type?: string, _duration?: number) => 0);
const show = (type: string) => (message: string, duration?: number) => {
addToast(message, type, duration);
};
return {
addToast,
toast: { success: show('success'), error: show('error'), warning: show('warning'), info: show('info') },
};
});
vi.mock('../shared/Toast', () => ({ useToast: () => toast, default: () => toast }));
const file = new File([''], 'cafes.kml');
beforeEach(() => {
resetAllStores();
addToast.mockClear();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('useTourGpxImport without a line in the file', () => {
it('names the file in a toast when nothing takes such files', async () => {
vi.spyOn(tourRepo, 'previewGpx').mockRejectedValue({ response: { data: { code: 'no_track' } } });
const { result } = renderHook(() => useTourGpxImport({ tripId: 2, canEdit: true }));
await act(() => result.current.importFiles([file]));
expect(addToast).toHaveBeenCalledWith('tours.import.noTrackIn', 'error', undefined);
expect(result.current.preview).toBeNull();
});
it('reads a GPX answer by its message as well', async () => {
const onNoTrack = vi.fn();
vi.spyOn(tourRepo, 'previewGpx').mockRejectedValue({
response: { data: { error: 'No track or route found in GPX file' } },
});
const { result } = renderHook(() => useTourGpxImport({ tripId: 2, canEdit: true, onNoTrack }));
await act(() => result.current.importFiles([file]));
expect(onNoTrack).toHaveBeenCalledWith([file]);
expect(addToast).not.toHaveBeenCalled();
});
it('hands such files on, once, and still names any other failure', async () => {
const onNoTrack = vi.fn();
const broken = new File(['?'], 'broken.gpx');
vi.spyOn(tourRepo, 'previewGpx').mockImplementation(async (_tripId, picked) => {
throw picked === broken
? { response: { data: { code: 'malformed' } } }
: { response: { data: { code: 'no_track' } } };
});
const { result } = renderHook(() => useTourGpxImport({ tripId: 2, canEdit: true, onNoTrack }));
await act(() => result.current.importFiles([file, broken]));
expect(onNoTrack).toHaveBeenCalledTimes(1);
expect(onNoTrack).toHaveBeenCalledWith([file]);
expect(addToast).toHaveBeenCalledTimes(1);
expect(addToast).toHaveBeenCalledWith('tours.import.unreadableIn', 'error', undefined);
});
});
describe('useTourGpxImport with points beside the lines (#2969)', () => {
const route = new File([''], 'sygic.gpx');
const pins = new File([''], 'pins.kml');
const line = {
name: 'Route',
geometry: [[47, 11] as [number, number], [47.01, 11] as [number, number]],
point_count: 2,
distance_km: 1,
elevation_gain_m: null,
elevation_loss_m: null,
duplicate: false,
};
beforeEach(() => {
useTripStore.setState({ refreshPlaces: vi.fn(async () => {}) } as never);
vi.spyOn(tourRepo, 'importGpx').mockResolvedValue({ tours: [], skipped: 0, caution: false } as never);
});
const previewed = async (preview: (picked: File) => object) => {
vi.spyOn(tourRepo, 'previewGpx').mockImplementation(async (_tripId, picked) => preview(picked as File) as never);
const hook = renderHook(() => useTourGpxImport({ tripId: 2, canEdit: true }));
await act(() => hook.result.current.importFiles([route, pins]));
return hook;
};
it('adds the points of each file as places through the places import, waypoints only', async () => {
const importGpx = vi.spyOn(placesApi, 'importGpx').mockResolvedValue({ count: 3 });
const importMap = vi.spyOn(placesApi, 'importMapFile').mockResolvedValue({ count: 2 });
const { result } = await previewed((picked) => ({
tracks: [line],
left_out: { points: picked === route ? 3 : 2, areas: 0, other: 0 },
}));
await act(() => result.current.confirm({ tour_type: 'hike' }, { points: true }));
expect(importGpx).toHaveBeenCalledWith(2, route, { waypoints: true, routes: false, tracks: false });
expect(importMap).toHaveBeenCalledWith(2, pins, { points: true, paths: false });
expect(tourRepo.importGpx).toHaveBeenCalledTimes(2);
expect(addToast).toHaveBeenCalledWith('tours.import.pointsImported', 'success', undefined);
});
it('leaves the points out when they are not asked for, and skips the Tours import for a file of known lines', async () => {
const importGpx = vi.spyOn(placesApi, 'importGpx').mockResolvedValue({ count: 3 });
const { result } = await previewed(() => ({
tracks: [{ ...line, duplicate: true }],
left_out: { points: 3, areas: 0, other: 0 },
}));
await act(() => result.current.confirm({ tour_type: 'hike' }));
expect(importGpx).not.toHaveBeenCalled();
expect(tourRepo.importGpx).not.toHaveBeenCalled();
});
it('names a file whose points could not be added, and still takes in its tour', async () => {
vi.spyOn(placesApi, 'importGpx').mockRejectedValue(new Error('offline'));
vi.spyOn(placesApi, 'importMapFile').mockResolvedValue({ count: 0 });
const { result } = await previewed((picked) => ({
tracks: [line],
...(picked === route ? { left_out: { points: 1, areas: 0, other: 0 } } : {}),
}));
await act(() => result.current.confirm({ tour_type: 'hike' }, { points: true }));
expect(addToast).toHaveBeenCalledWith('tours.import.pointsFailedIn', 'error', undefined);
expect(tourRepo.importGpx).toHaveBeenCalledTimes(2);
});
});