mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 05:26:47 +00:00
fix(journey): emptying the location field in the entry editor removes the location
The field showed the stored place as soon as the search query was empty, so deleting the last character snapped it back and the location could never be removed. Clearing the field now empties the place and its coordinates, closes the suggestions and drops any search answer that comes back afterwards. Typed text without a picked result keeps the stored location as before. The hook is shared, so the desktop dialog and the phone sheet both get the fix. Refs #2615
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
// FE-JRN-EDITOR-001 to FE-JRN-EDITOR-059
|
||||
// FE-JRN-EDITOR-001 to FE-JRN-EDITOR-060
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
||||
import { http, HttpResponse, delay } from 'msw'
|
||||
@@ -1072,4 +1072,19 @@ describe('EntryEditor', () => {
|
||||
expect(img.getAttribute('src')).toBe(`/api/photos/${id}/thumbnail`)
|
||||
}
|
||||
})
|
||||
|
||||
it('FE-JRN-EDITOR-060: emptying the location field keeps it empty and saves the entry without a place (#2615)', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onSave } = mountEditor(buildEntry({ id: 10, location_name: 'Rome', location_lat: 41.9, location_lng: 12.5 }))
|
||||
const input = screen.getByPlaceholderText('Search location...')
|
||||
expect(input).toHaveValue('Rome')
|
||||
|
||||
fireEvent.change(input, { target: { value: 'Rom' } })
|
||||
fireEvent.change(input, { target: { value: '' } })
|
||||
expect(input).toHaveValue('')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'Save' }))
|
||||
await waitFor(() => expect(onSave).toHaveBeenCalled())
|
||||
expect(onSave.mock.calls[0][0]).toMatchObject({ location_name: null, location_lat: null, location_lng: null })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// FE-JRN-ENTRYFORM-001 to FE-JRN-ENTRYFORM-021: the journey entry form logic behind both
|
||||
// FE-JRN-ENTRYFORM-001 to FE-JRN-ENTRYFORM-024: the journey entry form logic behind both
|
||||
// the desktop editor dialog and the phone entry sheet.
|
||||
import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import type React from 'react';
|
||||
@@ -388,4 +388,55 @@ describe('useJourneyEntryForm', () => {
|
||||
expect(onDone).not.toHaveBeenCalled();
|
||||
expect(sheet.result.current.saving).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-JRN-ENTRYFORM-022: emptying the location field removes the location and saves it as gone (#2615)', async () => {
|
||||
const { result, props } = setup();
|
||||
act(() => result.current.searchLocation('Rom'));
|
||||
act(() => result.current.searchLocation(''));
|
||||
expect(result.current.locationQuery).toBe('');
|
||||
expect(result.current.locationName).toBe('');
|
||||
expect(result.current.locationLat).toBeNull();
|
||||
expect(result.current.locationLng).toBeNull();
|
||||
expect(result.current.showLocationResults).toBe(false);
|
||||
expect(result.current.isDirty).toBe(true);
|
||||
|
||||
await act(() => result.current.handleSave());
|
||||
expect(props.onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ location_name: null, location_lat: null, location_lng: null }),
|
||||
4
|
||||
);
|
||||
});
|
||||
|
||||
it('FE-JRN-ENTRYFORM-023: a search still on its way when the field is emptied does not bring its results back', async () => {
|
||||
vi.useFakeTimers();
|
||||
let answer: (v: unknown) => void = () => {};
|
||||
vi.spyOn(mapsApi, 'search').mockReturnValue(new Promise((resolve) => (answer = resolve)) as never);
|
||||
const { result } = setup();
|
||||
act(() => result.current.searchLocation('Par'));
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(400);
|
||||
});
|
||||
expect(result.current.locationSearching).toBe(true);
|
||||
|
||||
act(() => result.current.searchLocation(''));
|
||||
expect(result.current.locationSearching).toBe(false);
|
||||
await act(async () => {
|
||||
answer({ places: [{ name: 'Paris', lat: '48.85', lng: '2.35' }] });
|
||||
});
|
||||
expect(result.current.locationResults).toEqual([]);
|
||||
expect(result.current.locationSearching).toBe(false);
|
||||
});
|
||||
|
||||
it('FE-JRN-ENTRYFORM-024: typed text without a picked result keeps the stored location', async () => {
|
||||
const { result, props } = setup();
|
||||
act(() => result.current.searchLocation('Mil'));
|
||||
expect(result.current.locationName).toBe('Rome');
|
||||
expect(result.current.locationLat).toBe(41.89);
|
||||
|
||||
await act(() => result.current.handleSave());
|
||||
expect(props.onSave).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ location_name: 'Rome', location_lat: 41.89, location_lng: 12.49 }),
|
||||
4
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -102,6 +102,8 @@ export function useJourneyEntryForm({
|
||||
const [locating, setLocating] = useState(false);
|
||||
const [locationError, setLocationError] = useState('');
|
||||
const locationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
// Counts location lookups, so an answer that comes back after the field moved on is dropped.
|
||||
const locationSearchSeqRef = useRef(0);
|
||||
const [mood, setMood] = useState(entry.mood || '');
|
||||
const [weather, setWeather] = useState(entry.weather || '');
|
||||
const [statsExcluded, setStatsExcluded] = useState(entry.stats_excluded ?? false);
|
||||
@@ -423,8 +425,19 @@ export function useJourneyEntryForm({
|
||||
|
||||
const searchLocation = (query: string) => {
|
||||
setLocationQuery(query);
|
||||
setShowLocationResults(true);
|
||||
if (locationTimerRef.current) clearTimeout(locationTimerRef.current);
|
||||
const seq = ++locationSearchSeqRef.current;
|
||||
// An emptied field removes the place instead of falling back to the stored one (#2615).
|
||||
if (!query.trim()) {
|
||||
setLocationName('');
|
||||
setLocationLat(null);
|
||||
setLocationLng(null);
|
||||
setLocationResults([]);
|
||||
setLocationSearching(false);
|
||||
setShowLocationResults(false);
|
||||
return;
|
||||
}
|
||||
setShowLocationResults(true);
|
||||
if (query.trim().length < 2) {
|
||||
setLocationResults([]);
|
||||
return;
|
||||
@@ -433,6 +446,7 @@ export function useJourneyEntryForm({
|
||||
setLocationSearching(true);
|
||||
try {
|
||||
const res = await mapsApi.search(query, placeLang);
|
||||
if (seq !== locationSearchSeqRef.current) return;
|
||||
setLocationResults(
|
||||
(res.places || [])
|
||||
.slice(0, 6)
|
||||
@@ -444,14 +458,16 @@ export function useJourneyEntryForm({
|
||||
}))
|
||||
);
|
||||
} catch {
|
||||
setLocationResults([]);
|
||||
if (seq === locationSearchSeqRef.current) setLocationResults([]);
|
||||
} finally {
|
||||
setLocationSearching(false);
|
||||
if (seq === locationSearchSeqRef.current) setLocationSearching(false);
|
||||
}
|
||||
}, 400);
|
||||
};
|
||||
|
||||
const pickLocation = (r: LocationResult) => {
|
||||
locationSearchSeqRef.current++;
|
||||
setLocationSearching(false);
|
||||
setLocationName(r.name);
|
||||
setLocationLat(r.lat);
|
||||
setLocationLng(r.lng);
|
||||
@@ -470,6 +486,7 @@ export function useJourneyEntryForm({
|
||||
// reverse geocode comes back.
|
||||
const fallbackName = `${pos.lat.toFixed(5)}, ${pos.lng.toFixed(5)}`;
|
||||
if (locationTimerRef.current) clearTimeout(locationTimerRef.current);
|
||||
locationSearchSeqRef.current++;
|
||||
setLocationSearching(false);
|
||||
setLocationLat(pos.lat);
|
||||
setLocationLng(pos.lng);
|
||||
|
||||
Reference in New Issue
Block a user