mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
Merge pull request #2376 from liketrek/fix/atlas-visit-dates
fix(atlas): date a country's first and last trip from the trips that count
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import {
|
||||
A2_TO_A3,
|
||||
bucketTooltipHeight,
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
bucketTooltipPlacement,
|
||||
bucketTooltipWidth,
|
||||
countryStatus,
|
||||
visitMonth,
|
||||
findBucketDuplicate,
|
||||
isBucketDuplicateError,
|
||||
isCountryVisible,
|
||||
@@ -71,6 +72,31 @@ describe('countryStatus', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('visitMonth (#1535)', () => {
|
||||
// West of Greenwich, where new Date('2024-06-01') is still the last evening of May.
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('TZ', 'America/New_York');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('reads the year and month as a local date, so the 1st stays in its own month', () => {
|
||||
const june = visitMonth('2024-06-01')!;
|
||||
expect([june.getFullYear(), june.getMonth(), june.getDate()]).toEqual([2024, 5, 1]);
|
||||
const january = visitMonth('2025-01-01')!;
|
||||
expect([january.getFullYear(), january.getMonth()]).toEqual([2025, 0]);
|
||||
});
|
||||
|
||||
it('gives nothing for a missing or malformed date', () => {
|
||||
expect(visitMonth(null)).toBeNull();
|
||||
expect(visitMonth(undefined)).toBeNull();
|
||||
expect(visitMonth('')).toBeNull();
|
||||
expect(visitMonth('June 2024')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCountryVisible', () => {
|
||||
it('always shows visited countries', () => {
|
||||
expect(isCountryVisible({ status: 'visited' }, false)).toBe(true);
|
||||
@@ -121,7 +147,7 @@ describe('withCountryMarkedVisited', () => {
|
||||
const prev = base({
|
||||
countries: [
|
||||
{ code: 'FR', tripCount: 2, placeCount: 5, status: 'visited' },
|
||||
{ code: 'JP', tripCount: 1, placeCount: 0, status: 'planned' },
|
||||
{ code: 'JP', tripCount: 1, placeCount: 0, firstVisit: '2099-05-01', lastVisit: '2099-05-08', status: 'planned' },
|
||||
],
|
||||
stats: { totalTrips: 3, totalPlaces: 10, totalCountries: 1, totalDays: 14, totalCountriesPlanned: 1 },
|
||||
continents: { Europe: 1 },
|
||||
@@ -131,7 +157,8 @@ describe('withCountryMarkedVisited', () => {
|
||||
const next = withCountryMarkedVisited(prev, 'JP');
|
||||
|
||||
expect(next.countries).toHaveLength(2);
|
||||
expect(next.countries.find((c) => c.code === 'JP')?.status).toBe('visited');
|
||||
// The planned trip's dates are not the dates of a visit, as the server agrees (#1535).
|
||||
expect(next.countries.find((c) => c.code === 'JP')).toMatchObject({ status: 'visited', firstVisit: null, lastVisit: null });
|
||||
expect(next.stats.totalCountries).toBe(2);
|
||||
expect(next.stats.totalCountriesPlanned).toBe(0);
|
||||
expect(next.continents).toEqual({ Europe: 1, Asia: 1 });
|
||||
|
||||
@@ -61,6 +61,15 @@ export function isCountryVisible(c: Pick<AtlasCountry, 'status'>, showPlanned: b
|
||||
return countryStatus(c) === 'visited' || showPlanned
|
||||
}
|
||||
|
||||
/**
|
||||
* The month a 'YYYY-MM-DD' visit date falls in, as a local date. `new Date('2025-06-01')`
|
||||
* reads the string as UTC midnight, which is still May anywhere west of Greenwich (#1535).
|
||||
*/
|
||||
export function visitMonth(date: string | null | undefined): Date | null {
|
||||
const match = date ? /^(\d{4})-(\d{2})/.exec(date) : null
|
||||
return match ? new Date(Number(match[1]), Number(match[2]) - 1, 1) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* Fold a manual "I have been here" mark into the loaded data without refetching — the
|
||||
* map redraws from `data`, so a reload would flash the whole globe. A country that was
|
||||
@@ -75,7 +84,8 @@ export function withCountryMarkedVisited(prev: AtlasData, code: string): AtlasDa
|
||||
return {
|
||||
...prev,
|
||||
countries: existing
|
||||
? prev.countries.map(c => (c.code === code ? { ...c, status: 'visited' as const } : c))
|
||||
// A country that was not visited yet has no visited trip to take dates from (#1535).
|
||||
? prev.countries.map(c => (c.code === code ? { ...c, status: 'visited' as const, firstVisit: null, lastVisit: null } : c))
|
||||
: [...prev.countries, { code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null, status: 'visited' as const }],
|
||||
stats: {
|
||||
...prev.stats,
|
||||
|
||||
@@ -312,6 +312,7 @@ beforeEach(() => {
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.unstubAllEnvs();
|
||||
delete window.__addToast;
|
||||
});
|
||||
|
||||
@@ -754,6 +755,24 @@ describe('useAtlas', () => {
|
||||
expect(atlas.confirmAction?.name).not.toBe('GERMANY_EN_ONLY');
|
||||
});
|
||||
|
||||
it('FE-HOOK-ATLAS-062: the country tooltip names both dates and shows their months (#1535)', async () => {
|
||||
// FR's dates fall on the 1st, which a UTC parse moves into the month before over here.
|
||||
vi.stubEnv('TZ', 'America/New_York');
|
||||
await mountAtlas({ geo: geoCountries });
|
||||
await waitFor(() => expect(lf.geoJson.length).toBeGreaterThan(0));
|
||||
|
||||
const countryLayer = lf.geoJson[0] as MockGeoJson;
|
||||
const fr = countryLayer.entries.find((e) => (e.feature.properties as Record<string, string>).ISO_A2 === 'FR');
|
||||
const html = fr!.layer.bindTooltip.mock.calls[0][0] as string;
|
||||
|
||||
expect(html).toContain('First trip');
|
||||
expect(html).toContain('Last trip');
|
||||
expect(html).not.toContain('atlas.lastVisitLabel');
|
||||
const month = (y: number, m: number) => new Date(y, m, 1).toLocaleDateString('en-US', { month: 'short', year: 'numeric' });
|
||||
expect(html).toContain(month(2023, 0));
|
||||
expect(html).toContain(month(2024, 5));
|
||||
});
|
||||
|
||||
it('FE-HOOK-ATLAS-026: plugin tint layers are drawn in their own pane and redrawn on theme change', async () => {
|
||||
await mountAtlas({
|
||||
geo: geoCountries,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { isVectorStyle } from '../../utils/tileUrl'
|
||||
import apiClient, { mapsApi, pluginsApi, type PluginAtlasLayer } from '../../api/client'
|
||||
import L from 'leaflet'
|
||||
import type { GeoJsonFeatureCollection } from '../../types'
|
||||
import { A2_TO_A3, countryStatus, findBucketDuplicate, isBucketDuplicateError, isCountryVisible, normalizeRegionName, regionCacheEvictions, withCountryMarkedVisited, wishlistA3Codes, countryColor, REGION_CACHE_MAX, bucketTooltipWidth, bucketTooltipPlacement, bucketTooltipNeedsScroll, type AtlasData, type AtlasPlaceHit, type CountryDetail, type BucketItem } from './atlasModel'
|
||||
import { A2_TO_A3, countryStatus, visitMonth, findBucketDuplicate, isBucketDuplicateError, isCountryVisible, normalizeRegionName, regionCacheEvictions, withCountryMarkedVisited, wishlistA3Codes, countryColor, REGION_CACHE_MAX, bucketTooltipWidth, bucketTooltipPlacement, bucketTooltipNeedsScroll, type AtlasData, type AtlasPlaceHit, type CountryDetail, type BucketItem } from './atlasModel'
|
||||
import { continentForCountry, escapeHtml, type VisitStatus } from '@trek/shared'
|
||||
import { useGlassGlare } from '../../components/Atlas/useGlassGlare'
|
||||
import { dawarichApi } from '../../api/dawarich'
|
||||
@@ -560,7 +560,7 @@ export function useAtlas() {
|
||||
if (c) {
|
||||
country_layer_by_a2_ref.current[c.code] = layer
|
||||
const name = resolveName(c.code)
|
||||
const formatDate = (d) => { if (!d) return '—'; const dt = new Date(d); return dt.toLocaleDateString(getLocaleForLanguage(language), { month: 'short', year: 'numeric' }) }
|
||||
const formatDate = (d) => { const month = visitMonth(d); return month ? month.toLocaleDateString(getLocaleForLanguage(language), { month: 'short', year: 'numeric' }) : '—' }
|
||||
// "First trip / Last trip" is simply wrong for a country you haven't reached yet —
|
||||
// a planned one gets a single departure date instead.
|
||||
const planned = countryStatus(c) !== 'visited'
|
||||
|
||||
@@ -16,6 +16,7 @@ import { KNOWN_COUNTRIES } from './known-countries';
|
||||
import { cityFromAddress } from './city-from-address';
|
||||
import { transferEndpointIds } from './transfer-endpoints';
|
||||
import type { FlightEndpointRow } from './transfer-endpoints';
|
||||
import { countryVisitDates } from './visit-dates';
|
||||
import { haversineKm } from '../common/geo';
|
||||
|
||||
/**
|
||||
@@ -262,21 +263,16 @@ export class AtlasService {
|
||||
}
|
||||
}
|
||||
|
||||
const countries = [...countrySet.values()].map((c) => {
|
||||
const countryTrips = trips.filter((t) => c.tripIds.has(t.id));
|
||||
const dates = countryTrips
|
||||
.map((t) => t.start_date)
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
|
||||
return {
|
||||
code: c.code,
|
||||
placeCount: c.places.length,
|
||||
tripCount: c.tripIds.size,
|
||||
firstVisit: dates[0] || null,
|
||||
lastVisit: dates[dates.length - 1] || null,
|
||||
status: c.status,
|
||||
};
|
||||
});
|
||||
// The dates are filled in at the end, once a manual mark or a booking has had its
|
||||
// say on the status: they come only from the trips that match that final status.
|
||||
const countries = [...countrySet.values()].map((c) => ({
|
||||
code: c.code,
|
||||
placeCount: c.places.length,
|
||||
tripCount: c.tripIds.size,
|
||||
firstVisit: null as string | null,
|
||||
lastVisit: null as string | null,
|
||||
status: c.status,
|
||||
}));
|
||||
|
||||
const citySet = new Set<string>();
|
||||
for (const place of places) {
|
||||
@@ -367,14 +363,17 @@ export class AtlasService {
|
||||
|
||||
// Collapse to one entry per coordinate before resolving countries —
|
||||
// getCountryFromCoords is a point-in-polygon scan and used to run once per point.
|
||||
const endpointStatus = new Map<string, { lat: number; lng: number; status: VisitStatus }>();
|
||||
const endpointStatus = new Map<string, { lat: number; lng: number; status: VisitStatus; tripIds: Set<number> }>();
|
||||
for (const e of endpoints) {
|
||||
const status = tripStatus.get(e.trip_id) ?? 'idea';
|
||||
const key = `${e.lat},${e.lng}`;
|
||||
const seen = endpointStatus.get(key);
|
||||
if (seen) seen.status = strongerVisitStatus(seen.status, status);
|
||||
else endpointStatus.set(key, { lat: e.lat, lng: e.lng, status });
|
||||
if (seen) {
|
||||
seen.status = strongerVisitStatus(seen.status, status);
|
||||
seen.tripIds.add(e.trip_id);
|
||||
} else endpointStatus.set(key, { lat: e.lat, lng: e.lng, status, tripIds: new Set([e.trip_id]) });
|
||||
}
|
||||
const bookingTripIds = new Map<string, Set<number>>();
|
||||
for (const e of endpointStatus.values()) {
|
||||
const code = getCountryFromCoords(e.lat, e.lng);
|
||||
if (!code || hidden.has(code)) continue;
|
||||
@@ -382,6 +381,20 @@ export class AtlasService {
|
||||
if (existing) existing.status = strongerVisitStatus(existing.status, e.status);
|
||||
else
|
||||
countries.push({ code, placeCount: 0, tripCount: 0, firstVisit: null, lastVisit: null, status: e.status });
|
||||
const ids = bookingTripIds.get(code) ?? new Set<number>();
|
||||
for (const id of e.tripIds) ids.add(id);
|
||||
bookingTripIds.set(code, ids);
|
||||
}
|
||||
|
||||
// A booking can be what makes a country with places visited, so its trip dates the
|
||||
// country as well. A country reached by bookings alone has no trip in its tooltip
|
||||
// and stays without dates.
|
||||
for (const c of countries) {
|
||||
const fromPlaces = countrySet.get(c.code);
|
||||
if (!fromPlaces) continue;
|
||||
const fromBookings = bookingTripIds.get(c.code);
|
||||
const dated = trips.filter((t) => fromPlaces.tripIds.has(t.id) || fromBookings?.has(t.id));
|
||||
Object.assign(c, countryVisitDates(dated, c.status, now));
|
||||
}
|
||||
|
||||
// Everything below counts actual visits only. countries[] still carries planned and
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
/**
|
||||
* The "First trip / Last trip" dates Atlas shows for a country (#1535).
|
||||
*
|
||||
* Both used to be the earliest and the latest start_date of every trip with a
|
||||
* place in the country. A single trip therefore showed its departure month twice,
|
||||
* the month you came home never appeared, and a trip you had only booked could
|
||||
* become the last visit of a country you had already been to.
|
||||
*
|
||||
* Now only the trips that give the country its status count: the visited ones for
|
||||
* a visited country, the planned ones for a planned country. firstVisit is where
|
||||
* the earliest of them starts and lastVisit where the latest ends. A trip you are
|
||||
* still on ends today, not on the day you plan to fly back.
|
||||
*
|
||||
* Dates stay 'YYYY-MM-DD' strings and are compared as such, like tripVisitStatus
|
||||
* does, so no timezone can shift them on the way.
|
||||
*/
|
||||
import { tripVisitStatus, type VisitStatus } from '@trek/shared';
|
||||
|
||||
export interface DatedTrip {
|
||||
start_date?: string | null;
|
||||
end_date?: string | null;
|
||||
}
|
||||
|
||||
export function countryVisitDates(
|
||||
trips: DatedTrip[],
|
||||
status: VisitStatus,
|
||||
today: string,
|
||||
): { firstVisit: string | null; lastVisit: string | null } {
|
||||
let firstVisit: string | null = null;
|
||||
let lastVisit: string | null = null;
|
||||
for (const trip of trips) {
|
||||
const start = trip.start_date || trip.end_date;
|
||||
if (!start || tripVisitStatus(trip.start_date, trip.end_date, today) !== status) continue;
|
||||
let end = trip.end_date && trip.end_date > start ? trip.end_date : start;
|
||||
if (status === 'visited' && end > today) end = today;
|
||||
if (!firstVisit || start < firstVisit) firstVisit = start;
|
||||
if (!lastVisit || end > lastVisit) lastVisit = end;
|
||||
}
|
||||
return { firstVisit, lastVisit };
|
||||
}
|
||||
@@ -171,8 +171,9 @@ describe('Atlas e2e (real auth guard + real service + temp SQLite)', () => {
|
||||
expect(res.status).toBe(200);
|
||||
|
||||
const byCode = Object.fromEntries((res.body.countries as { code: string }[]).map((c) => [c.code, c]));
|
||||
expect(byCode['IT']).toMatchObject({ status: 'visited' });
|
||||
expect(byCode['JP']).toMatchObject({ status: 'planned' });
|
||||
// #1535: the dates run from the start to the end of the trip, not start to start.
|
||||
expect(byCode['IT']).toMatchObject({ status: 'visited', firstVisit: iso(-40), lastVisit: iso(-30) });
|
||||
expect(byCode['JP']).toMatchObject({ status: 'planned', firstVisit: iso(30), lastVisit: iso(40) });
|
||||
expect(res.body.stats.totalCountries).toBe(1);
|
||||
expect(res.body.stats.totalCountriesPlanned).toBe(1);
|
||||
expect(res.body.countries.length).toBeGreaterThan(res.body.stats.totalCountries);
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* The First trip / Last trip dates behind the Atlas tooltip (#1535). Pure, so no DB:
|
||||
* every case is a set of trips and the status the country ended up with.
|
||||
*/
|
||||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import { countryVisitDates } from '../../../src/nest/atlas/visit-dates';
|
||||
|
||||
const TODAY = '2026-09-15';
|
||||
|
||||
describe('countryVisitDates', () => {
|
||||
it('ATLAS-DATES-001: a trip over two months keeps both months', () => {
|
||||
expect(countryVisitDates([{ start_date: '2025-03-28', end_date: '2025-04-12' }], 'visited', TODAY)).toEqual({
|
||||
firstVisit: '2025-03-28',
|
||||
lastVisit: '2025-04-12',
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-DATES-002: several visited trips span from the earliest start to the latest end', () => {
|
||||
const trips = [
|
||||
{ start_date: '2024-06-01', end_date: '2024-06-10' },
|
||||
{ start_date: '2022-01-05', end_date: '2022-01-20' },
|
||||
{ start_date: '2023-07-01', end_date: '2023-07-03' },
|
||||
];
|
||||
expect(countryVisitDates(trips, 'visited', TODAY)).toEqual({ firstVisit: '2022-01-05', lastVisit: '2024-06-10' });
|
||||
});
|
||||
|
||||
it('ATLAS-DATES-003: a visited country ignores its planned and dateless trips', () => {
|
||||
const trips = [
|
||||
{ start_date: '2025-05-01', end_date: '2025-05-04' },
|
||||
{ start_date: '2027-01-01', end_date: '2027-01-09' },
|
||||
{ start_date: null, end_date: null },
|
||||
];
|
||||
expect(countryVisitDates(trips, 'visited', TODAY)).toEqual({ firstVisit: '2025-05-01', lastVisit: '2025-05-04' });
|
||||
});
|
||||
|
||||
it('ATLAS-DATES-004: a trip still under way ends today', () => {
|
||||
expect(countryVisitDates([{ start_date: '2026-09-10', end_date: '2026-09-20' }], 'visited', TODAY)).toEqual({
|
||||
firstVisit: '2026-09-10',
|
||||
lastVisit: TODAY,
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-DATES-005: a planned country keeps its future dates', () => {
|
||||
const trips = [
|
||||
{ start_date: '2027-03-01', end_date: '2027-03-08' },
|
||||
{ start_date: '2026-12-20', end_date: '2026-12-27' },
|
||||
];
|
||||
expect(countryVisitDates(trips, 'planned', TODAY)).toEqual({ firstVisit: '2026-12-20', lastVisit: '2027-03-08' });
|
||||
});
|
||||
|
||||
it('ATLAS-DATES-006: a trip with one date only starts and ends on it', () => {
|
||||
expect(countryVisitDates([{ start_date: '2025-02-02' }], 'visited', TODAY)).toEqual({
|
||||
firstVisit: '2025-02-02',
|
||||
lastVisit: '2025-02-02',
|
||||
});
|
||||
expect(countryVisitDates([{ start_date: null, end_date: '2025-02-09' }], 'visited', TODAY)).toEqual({
|
||||
firstVisit: '2025-02-09',
|
||||
lastVisit: '2025-02-09',
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-DATES-007: an end typed before the start does not end the trip before it began', () => {
|
||||
expect(countryVisitDates([{ start_date: '2025-02-10', end_date: '2025-02-01' }], 'visited', TODAY)).toEqual({
|
||||
firstVisit: '2025-02-10',
|
||||
lastVisit: '2025-02-10',
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-DATES-008: no matching trip gives no dates', () => {
|
||||
expect(countryVisitDates([], 'visited', TODAY)).toEqual({ firstVisit: null, lastVisit: null });
|
||||
expect(countryVisitDates([{ start_date: '2027-01-01', end_date: '2027-01-02' }], 'visited', TODAY)).toEqual({
|
||||
firstVisit: null,
|
||||
lastVisit: null,
|
||||
});
|
||||
expect(countryVisitDates([{ start_date: null, end_date: null }], 'idea', TODAY)).toEqual({
|
||||
firstVisit: null,
|
||||
lastVisit: null,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1653,6 +1653,110 @@ describe('getStats — visited vs planned vs idea (#1048)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats first and last visit (#1535)', () => {
|
||||
it('ATLAS-UNIT-054: one trip runs from its start to its end, not from its start to its start', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Paris', start_date: PAST_START, end_date: PAST_END });
|
||||
insertPlace(testDb, trip.id, 'Louvre', '75001 Paris, France');
|
||||
|
||||
const stats = await atlas.stats(user.id);
|
||||
|
||||
expect(stats.countries.find((c) => c.code === 'FR')).toMatchObject({ firstVisit: PAST_START, lastVisit: PAST_END });
|
||||
});
|
||||
|
||||
it('ATLAS-UNIT-055: a trip only booked does not become the last visit of a country already visited', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const past = createTrip(testDb, user.id, { title: 'Munich 2023', start_date: PAST_START, end_date: PAST_END });
|
||||
const future = createTrip(testDb, user.id, { title: 'Munich again', start_date: FUTURE_START, end_date: FUTURE_END });
|
||||
insertPlace(testDb, past.id, 'Marienplatz', 'Marienplatz, Munich, Germany');
|
||||
insertPlace(testDb, future.id, 'Englischer Garten', 'Englischer Garten, Munich, Germany');
|
||||
|
||||
const stats = await atlas.stats(user.id);
|
||||
|
||||
expect(stats.countries.find((c) => c.code === 'DE')).toMatchObject({
|
||||
status: 'visited',
|
||||
firstVisit: PAST_START,
|
||||
lastVisit: PAST_END,
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-UNIT-056: a trip still under way ends today', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Rome now', start_date: isoOffsetDays(-2), end_date: isoOffsetDays(5) });
|
||||
insertPlace(testDb, trip.id, 'Colosseum', 'Piazza del Colosseo, Rome, Italy');
|
||||
|
||||
const stats = await atlas.stats(user.id);
|
||||
|
||||
expect(stats.countries.find((c) => c.code === 'IT')).toMatchObject({
|
||||
firstVisit: isoOffsetDays(-2),
|
||||
lastVisit: isoOffsetDays(0),
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-UNIT-057: a planned country keeps the dates of its planned trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Tokyo soon', start_date: FUTURE_START, end_date: FUTURE_END });
|
||||
insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan');
|
||||
|
||||
const stats = await atlas.stats(user.id);
|
||||
|
||||
expect(stats.countries.find((c) => c.code === 'JP')).toMatchObject({
|
||||
status: 'planned',
|
||||
firstVisit: FUTURE_START,
|
||||
lastVisit: FUTURE_END,
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-UNIT-058: a country marked by hand takes no dates from a trip that has not happened yet', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Japan, next month', start_date: FUTURE_START, end_date: FUTURE_END });
|
||||
insertPlace(testDb, trip.id, 'Senso-ji', 'Asakusa, Tokyo, Japan');
|
||||
testDb.prepare('INSERT INTO visited_countries (user_id, country_code) VALUES (?, ?)').run(user.id, 'JP');
|
||||
|
||||
const stats = await atlas.stats(user.id);
|
||||
|
||||
expect(stats.countries.find((c) => c.code === 'JP')).toMatchObject({
|
||||
status: 'visited',
|
||||
firstVisit: null,
|
||||
lastVisit: null,
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-UNIT-060: a flight that makes a country with places visited dates it by its own trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const flown = createTrip(testDb, user.id, { title: 'Tokyo 2023', start_date: PAST_START, end_date: PAST_END });
|
||||
const reservation = createReservation(testDb, flown.id, { type: 'flight' });
|
||||
insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo
|
||||
const planned = createTrip(testDb, user.id, { title: 'Japan again', start_date: FUTURE_START, end_date: FUTURE_END });
|
||||
insertPlace(testDb, planned.id, 'Senso-ji', 'Asakusa, Tokyo, Japan');
|
||||
|
||||
const stats = await atlas.stats(user.id);
|
||||
|
||||
expect(stats.countries.find((c) => c.code === 'JP')).toMatchObject({
|
||||
status: 'visited',
|
||||
tripCount: 1,
|
||||
firstVisit: PAST_START,
|
||||
lastVisit: PAST_END,
|
||||
});
|
||||
});
|
||||
|
||||
it('ATLAS-UNIT-059: a country reached only by a flight and a dateless idea stay without dates', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id, { title: 'Tokyo', start_date: PAST_START, end_date: PAST_END });
|
||||
const reservation = createReservation(testDb, trip.id, { type: 'flight' });
|
||||
insertReservationEndpoint(testDb, reservation.id, 'from', 0, 50.9014, 4.4844); // Brussels
|
||||
insertReservationEndpoint(testDb, reservation.id, 'to', 1, 35.6762, 139.6503); // Tokyo
|
||||
const someday = createTrip(testDb, user.id, { title: 'Someday: Paris' });
|
||||
insertPlace(testDb, someday.id, 'Louvre', '75001 Paris, France');
|
||||
|
||||
const stats = await atlas.stats(user.id);
|
||||
|
||||
expect(stats.countries.find((c) => c.code === 'JP')).toMatchObject({ tripCount: 0, firstVisit: null, lastVisit: null });
|
||||
expect(stats.countries.find((c) => c.code === 'FR')).toMatchObject({ status: 'idea', firstVisit: null, lastVisit: null });
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCountryPlaces — status (#1048)', () => {
|
||||
it('ATLAS-UNIT-040: the country sheet reports the same status the map paints', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'أوقيانوسيا',
|
||||
'atlas.other': 'أخرى',
|
||||
'atlas.firstVisit': 'أول رحلة',
|
||||
'atlas.lastVisitLabel': 'آخر رحلة',
|
||||
'atlas.tripSingular': 'رحلة',
|
||||
'atlas.tripPlural': 'رحلات',
|
||||
'atlas.placeVisited': 'مكان تمت زيارته',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceania',
|
||||
'atlas.other': 'Outro',
|
||||
'atlas.firstVisit': 'Primeira viagem',
|
||||
'atlas.lastVisitLabel': 'Última viagem',
|
||||
'atlas.tripSingular': 'Viagem',
|
||||
'atlas.tripPlural': 'Viagens',
|
||||
'atlas.placeVisited': 'Lugar visitado',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceania',
|
||||
'atlas.other': 'Altres',
|
||||
'atlas.firstVisit': 'Primer viatge',
|
||||
'atlas.lastVisitLabel': 'Darrer viatge',
|
||||
'atlas.tripSingular': 'Viatge',
|
||||
'atlas.tripPlural': 'Viatges',
|
||||
'atlas.placeVisited': 'Lloc visitat',
|
||||
|
||||
@@ -50,6 +50,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceánie',
|
||||
'atlas.other': 'Ostatní',
|
||||
'atlas.firstVisit': 'První cesta',
|
||||
'atlas.lastVisitLabel': 'Poslední cesta',
|
||||
'atlas.tripSingular': 'Cesta',
|
||||
'atlas.tripPlural': 'Cesty',
|
||||
'atlas.placeVisited': 'Navštívené místo',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Ozeanien',
|
||||
'atlas.other': 'Andere',
|
||||
'atlas.firstVisit': 'Erste Reise',
|
||||
'atlas.lastVisitLabel': 'Letzte Reise',
|
||||
'atlas.tripSingular': 'Reise',
|
||||
'atlas.tripPlural': 'Reisen',
|
||||
'atlas.placeVisited': 'Ort besucht',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceania',
|
||||
'atlas.other': 'Other',
|
||||
'atlas.firstVisit': 'First trip',
|
||||
'atlas.lastVisitLabel': 'Last trip',
|
||||
'atlas.tripSingular': 'Trip',
|
||||
'atlas.tripPlural': 'Trips',
|
||||
'atlas.placeVisited': 'Place visited',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceanía',
|
||||
'atlas.other': 'Otros',
|
||||
'atlas.firstVisit': 'Primer viaje',
|
||||
'atlas.lastVisitLabel': 'Último viaje',
|
||||
'atlas.tripSingular': 'Viaje',
|
||||
'atlas.tripPlural': 'Viajes',
|
||||
'atlas.placeVisited': 'Lugar visitado',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Océanie',
|
||||
'atlas.other': 'Autre',
|
||||
'atlas.firstVisit': 'Premier voyage',
|
||||
'atlas.lastVisitLabel': 'Dernier voyage',
|
||||
'atlas.tripSingular': 'Voyage',
|
||||
'atlas.tripPlural': 'Voyages',
|
||||
'atlas.placeVisited': 'Lieu visité',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Ωκεανία',
|
||||
'atlas.other': 'Άλλο',
|
||||
'atlas.firstVisit': 'Πρώτο ταξίδι',
|
||||
'atlas.lastVisitLabel': 'Τελευταίο ταξίδι',
|
||||
'atlas.tripSingular': 'Ταξίδι',
|
||||
'atlas.tripPlural': 'Ταξίδια',
|
||||
'atlas.placeVisited': 'Μέρος που επισκεφθήκατε',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Óceánia',
|
||||
'atlas.other': 'Egyéb',
|
||||
'atlas.firstVisit': 'Első utazás',
|
||||
'atlas.lastVisitLabel': 'Utolsó utazás',
|
||||
'atlas.tripSingular': 'Utazás',
|
||||
'atlas.tripPlural': 'Utazások',
|
||||
'atlas.placeVisited': 'Meglátogatott hely',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oseania',
|
||||
'atlas.other': 'Lainnya',
|
||||
'atlas.firstVisit': 'Perjalanan pertama',
|
||||
'atlas.lastVisitLabel': 'Perjalanan terakhir',
|
||||
'atlas.tripSingular': 'Perjalanan',
|
||||
'atlas.tripPlural': 'Perjalanan',
|
||||
'atlas.placeVisited': 'Tempat dikunjungi',
|
||||
|
||||
@@ -50,6 +50,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceania',
|
||||
'atlas.other': 'Altro',
|
||||
'atlas.firstVisit': 'Primo viaggio',
|
||||
'atlas.lastVisitLabel': 'Ultimo viaggio',
|
||||
'atlas.tripSingular': 'Viaggio',
|
||||
'atlas.tripPlural': 'Viaggi',
|
||||
'atlas.placeVisited': 'Luogo visitato',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'オセアニア',
|
||||
'atlas.other': 'その他',
|
||||
'atlas.firstVisit': '最初の旅行',
|
||||
'atlas.lastVisitLabel': '最後の旅行',
|
||||
'atlas.tripSingular': '旅行',
|
||||
'atlas.tripPlural': '旅行',
|
||||
'atlas.placeVisited': '訪問した場所',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': '오세아니아',
|
||||
'atlas.other': '기타',
|
||||
'atlas.firstVisit': '첫 번째 여행',
|
||||
'atlas.lastVisitLabel': '마지막 여행',
|
||||
'atlas.tripSingular': '여행',
|
||||
'atlas.tripPlural': '여행',
|
||||
'atlas.placeVisited': '방문한 장소',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceanië',
|
||||
'atlas.other': 'Overig',
|
||||
'atlas.firstVisit': 'Eerste reis',
|
||||
'atlas.lastVisitLabel': 'Laatste reis',
|
||||
'atlas.tripSingular': 'Reis',
|
||||
'atlas.tripPlural': 'Reizen',
|
||||
'atlas.placeVisited': 'Bezochte plaats',
|
||||
|
||||
@@ -50,6 +50,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceania',
|
||||
'atlas.other': 'Inne',
|
||||
'atlas.firstVisit': 'Pierwsza podróż',
|
||||
'atlas.lastVisitLabel': 'Ostatnia podróż',
|
||||
'atlas.tripSingular': 'Podróż',
|
||||
'atlas.tripPlural': 'Podróże',
|
||||
'atlas.placeVisited': 'Odwiedzone miejsce',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Океания',
|
||||
'atlas.other': 'Другое',
|
||||
'atlas.firstVisit': 'Первая поездка',
|
||||
'atlas.lastVisitLabel': 'Последняя поездка',
|
||||
'atlas.tripSingular': 'Поездка',
|
||||
'atlas.tripPlural': 'Поездки',
|
||||
'atlas.placeVisited': 'Посещённое место',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Oceanien',
|
||||
'atlas.other': 'Andra',
|
||||
'atlas.firstVisit': 'Första resan',
|
||||
'atlas.lastVisitLabel': 'Den senaste resan',
|
||||
'atlas.tripSingular': 'Resa',
|
||||
'atlas.tripPlural': 'Resor',
|
||||
'atlas.placeVisited': 'Plats besökt',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Okyanusya',
|
||||
'atlas.other': 'Diğer',
|
||||
'atlas.firstVisit': 'İlk yolculuk',
|
||||
'atlas.lastVisitLabel': 'Son yolculuk',
|
||||
'atlas.tripSingular': 'Seyahat',
|
||||
'atlas.tripPlural': 'Geziler',
|
||||
'atlas.placeVisited': 'Ziyaret edilen yer',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Океанія',
|
||||
'atlas.other': 'Інше',
|
||||
'atlas.firstVisit': 'Перша поїздка',
|
||||
'atlas.lastVisitLabel': 'Остання поїздка',
|
||||
'atlas.tripSingular': 'Поїздка',
|
||||
'atlas.tripPlural': 'Поїздки',
|
||||
'atlas.placeVisited': 'Відвідане місце',
|
||||
|
||||
@@ -52,6 +52,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': 'Châu Đại Dương',
|
||||
'atlas.other': 'Khác',
|
||||
'atlas.firstVisit': 'Chuyến đi đầu tiên',
|
||||
'atlas.lastVisitLabel': 'Chuyến đi cuối cùng',
|
||||
'atlas.tripSingular': 'Chuyến đi',
|
||||
'atlas.tripPlural': 'Chuyến đi',
|
||||
'atlas.placeVisited': 'Nơi đã ghé thăm',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': '大洋洲',
|
||||
'atlas.other': '其他',
|
||||
'atlas.firstVisit': '首次旅行',
|
||||
'atlas.lastVisitLabel': '最近旅行',
|
||||
'atlas.tripSingular': '次旅行',
|
||||
'atlas.tripPlural': '次旅行',
|
||||
'atlas.placeVisited': '個地點已訪問',
|
||||
|
||||
@@ -28,6 +28,7 @@ const atlas: TranslationStrings = {
|
||||
'atlas.oceania': '大洋洲',
|
||||
'atlas.other': '其他',
|
||||
'atlas.firstVisit': '首次旅行',
|
||||
'atlas.lastVisitLabel': '最近旅行',
|
||||
'atlas.tripSingular': '次旅行',
|
||||
'atlas.tripPlural': '次旅行',
|
||||
'atlas.placeVisited': '个地点已访问',
|
||||
|
||||
Reference in New Issue
Block a user