import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { placesApi } from '../../../../src/api/client' import { tourRepo } from '../../../../src/repo/tourRepo' import { useAddonStore } from '../../../../src/store/addonStore' import MImportSheet from '../../../../src/mobile/screens/trip/sheets/MImportSheet' import { buildPlanner } from '../../../helpers/mobileTrip' import { resetAllStores, seedStore } from '../../../helpers/store' import { fireEvent, render, screen, waitFor } from '../../../helpers/render' // The Tours preview draws its course on a map, which jsdom cannot. vi.mock('../../../../src/components/Map/MapViewAuto', () => ({ MapViewAuto: () =>
})) // FE-MOB-IMPS-001 to FE-MOB-IMPS-013 // // planner.t echoes its key — assertions go against keys, not English copy. function renderSheet(open = true) { const planner = buildPlanner() const onClose = vi.fn() const view = render() return { ...view, planner, onClose } } const menuRow = (title: string) => screen.getByRole('button', { name: new RegExp(`^${title}`) }) describe('MImportSheet', () => { beforeEach(() => { resetAllStores() }) afterEach(() => { vi.restoreAllMocks() }) it('FE-MOB-IMPS-001: renders nothing while closed', () => { renderSheet(false) expect(screen.queryByRole('dialog')).not.toBeInTheDocument() }) it('FE-MOB-IMPS-002: opens on the menu with both import options', () => { renderSheet() const dialog = screen.getByRole('dialog', { name: 'mobileTrip.importPlaces' }) expect(dialog).toBeInTheDocument() expect(menuRow('places.importFile')).toHaveTextContent('GPX · KML · KMZ') expect(menuRow('places.importList')).toHaveTextContent('mobileTrip.importListSub') expect(screen.queryByRole('button', { name: 'common.back' })).not.toBeInTheDocument() }) it('FE-MOB-IMPS-003: the close button reports the sheet closed', () => { const { onClose } = renderSheet() fireEvent.click(screen.getByRole('button', { name: 'common.close' })) expect(onClose).toHaveBeenCalledTimes(1) }) it('FE-MOB-IMPS-004: the file option opens the file step under its own title', () => { renderSheet() fireEvent.click(menuRow('places.importFile')) expect(screen.getByText('places.importFileHint')).toBeInTheDocument() expect(screen.getByText('places.importFileDropHere')).toBeInTheDocument() expect(screen.queryByText('mobileTrip.importListSub')).not.toBeInTheDocument() }) it('FE-MOB-IMPS-005: the list option opens the shared-list step', () => { renderSheet() fireEvent.click(menuRow('places.importList')) expect(screen.getByText('places.googleListHint')).toBeInTheDocument() expect(screen.getByRole('textbox')).toBeInTheDocument() }) it('FE-MOB-IMPS-006: the header back button returns to the menu', () => { renderSheet() fireEvent.click(menuRow('places.importList')) fireEvent.click(screen.getByRole('button', { name: 'common.back' })) expect(menuRow('places.importFile')).toBeInTheDocument() expect(screen.queryByRole('textbox')).not.toBeInTheDocument() }) it('FE-MOB-IMPS-007: the footer cancel of either step returns to the menu', () => { renderSheet() fireEvent.click(menuRow('places.importFile')) fireEvent.click(screen.getByRole('button', { name: 'common.cancel' })) expect(menuRow('places.importList')).toBeInTheDocument() fireEvent.click(menuRow('places.importList')) fireEvent.click(screen.getByRole('button', { name: 'common.cancel' })) expect(menuRow('places.importFile')).toBeInTheDocument() }) it('FE-MOB-IMPS-008: reopening the sheet resets it to the menu', () => { const { rerender, planner, onClose } = renderSheet() fireEvent.click(menuRow('places.importList')) expect(screen.getByRole('textbox')).toBeInTheDocument() rerender() rerender() expect(screen.queryByRole('textbox')).not.toBeInTheDocument() expect(menuRow('places.importFile')).toBeInTheDocument() }) it('FE-MOB-IMPS-009: a finished list import closes the whole sheet', async () => { vi.spyOn(placesApi, 'importGoogleList').mockResolvedValue({ count: 2, skipped: 0, listName: 'L', places: [] }) const { onClose } = renderSheet() fireEvent.click(menuRow('places.importList')) fireEvent.change(screen.getByRole('textbox'), { target: { value: 'https://maps.app.goo.gl/abc' } }) fireEvent.click(screen.getByRole('button', { name: 'common.import' })) await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)) }) it('FE-MOB-IMPS-010: a finished file import closes the whole sheet', async () => { vi.spyOn(placesApi, 'importGpx').mockResolvedValue({ count: 1, skipped: 0, places: [] }) const { onClose } = renderSheet() fireEvent.click(menuRow('places.importFile')) const input = document.querySelector('input[type="file"]') as HTMLInputElement fireEvent.change(input, { target: { files: [new File([''], 'route.gpx')] } }) fireEvent.click(screen.getByRole('button', { name: 'common.import' })) await waitFor(() => expect(onClose).toHaveBeenCalledTimes(1)) }) describe('with the Tours addon on', () => { const picked: HTMLInputElement[] = [] beforeEach(() => { picked.length = 0 seedStore(useAddonStore, { addons: [{ id: 'tours', enabled: true }], loaded: true }) vi.spyOn(HTMLInputElement.prototype, 'click').mockImplementation(function (this: HTMLInputElement) { picked.push(this) }) }) const pickFiles = (files: File[]) => { const input = picked[0] Object.defineProperty(input, 'files', { value: files }) fireEvent(input, new Event('change')) } it('FE-MOB-IMPS-011: the file option stays and opens the Tours file picker instead of the file step', () => { renderSheet() fireEvent.click(menuRow('places.importFile')) expect(picked.map(input => input.accept)).toEqual(['.gpx,.kml,.kmz']) expect(screen.queryByText('places.importFileHint')).not.toBeInTheDocument() }) it('FE-MOB-IMPS-012: a picked route lands in the Tours preview', async () => { const preview = vi.spyOn(tourRepo, 'previewGpx').mockResolvedValue({ tracks: [{ name: 'Ridge', geometry: [[47.1, 11.2]], point_count: 1, distance_km: 1.2, elevation_gain_m: null, elevation_loss_m: null, duplicate: false, }], }) renderSheet() fireEvent.click(menuRow('places.importFile')) const route = new File([''], 'ridge.gpx') pickFiles([route]) await waitFor(() => expect(screen.getByTestId('gpx-preview-import')).toBeInTheDocument()) expect(preview).toHaveBeenCalledWith(1, route) }) it('FE-MOB-IMPS-013: a picked file of points only opens the file step with it chosen', async () => { vi.spyOn(tourRepo, 'previewGpx').mockRejectedValue({ response: { data: { code: 'no_track' } } }) renderSheet() fireEvent.click(menuRow('places.importFile')) pickFiles([new File([''], 'cafes.kml')]) await waitFor(() => expect(screen.getByText('cafes.kml')).toBeInTheDocument()) expect(screen.getByText('places.importFileHint')).toBeInTheDocument() fireEvent.click(screen.getByRole('button', { name: 'common.back' })) expect(menuRow('places.importList')).toBeInTheDocument() expect(screen.queryByText('cafes.kml')).not.toBeInTheDocument() }) }) })