release: v0.17.0 (#677)
See CHANGELOG.md, section [0.17.0] — 2026-09-23.
This commit is contained in:
251
lib/__tests__/journal-map.test.ts
Normal file
251
lib/__tests__/journal-map.test.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
parseJournalMarkdown,
|
||||
serializeJournalMarkdown,
|
||||
collectAssetIds,
|
||||
mapBlockAssetIds,
|
||||
} from '../journal';
|
||||
import type { JournalBlock } from '../journal';
|
||||
import { pinsForEntry } from '../journalMap';
|
||||
import type { LocationPrecision } from '../mapPrecision';
|
||||
|
||||
const mapBlock = (md: string) => parseJournalMarkdown(md).blocks[0];
|
||||
|
||||
describe('map block syntax', () => {
|
||||
it('parses ::map with and without a caption, empty by default', () => {
|
||||
expect(mapBlock('::map')).toEqual({ type: 'map', caption: undefined, line: true, items: [] });
|
||||
expect(mapBlock('::map Busan → Seoul')).toMatchObject({ caption: 'Busan → Seoul' });
|
||||
});
|
||||
|
||||
it('does not mistake ::mapping for a map', () => {
|
||||
expect(mapBlock('::mapping things').type).toBe('paragraph');
|
||||
});
|
||||
|
||||
it('reads every line form, in order', () => {
|
||||
const block = mapBlock(
|
||||
[
|
||||
'::map The trip',
|
||||
'Busan harbour: 35.098, 129.036',
|
||||
'photo: aaa, bbb',
|
||||
'photos: all',
|
||||
'37.566, 126.978',
|
||||
'Photo: ccc',
|
||||
'line: off',
|
||||
].join('\n'),
|
||||
);
|
||||
expect(block).toEqual({
|
||||
type: 'map',
|
||||
caption: 'The trip',
|
||||
line: false,
|
||||
items: [
|
||||
{ kind: 'point', label: 'Busan harbour', lat: 35.098, lng: 129.036 },
|
||||
{ kind: 'photo', assetId: 'aaa' },
|
||||
{ kind: 'photo', assetId: 'bbb' },
|
||||
{ kind: 'all-photos' },
|
||||
{ kind: 'point', lat: 37.566, lng: 126.978 },
|
||||
{ kind: 'photo', assetId: 'ccc' },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores malformed and out-of-range coordinates and unknown lines', () => {
|
||||
const block = mapBlock(
|
||||
[
|
||||
'::map',
|
||||
'Nowhere: 95, 10',
|
||||
'Also: 10, 200',
|
||||
'Text without numbers',
|
||||
'1.5',
|
||||
'Ok: -33.9, 151.2',
|
||||
].join('\n'),
|
||||
);
|
||||
expect(block).toMatchObject({
|
||||
items: [{ kind: 'point', label: 'Ok', lat: -33.9, lng: 151.2 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('round-trips byte-stable and never writes pins', () => {
|
||||
const block: JournalBlock = {
|
||||
type: 'map',
|
||||
caption: 'A <strong>trip</strong>',
|
||||
line: false,
|
||||
items: [
|
||||
{ kind: 'point', label: 'Start', lat: 52.51, lng: 13.47 },
|
||||
{ kind: 'photo', assetId: 'abc' },
|
||||
{ kind: 'all-photos' },
|
||||
{ kind: 'point', lat: 52.4, lng: 13.04 },
|
||||
],
|
||||
pins: [{ lat: 1, lng: 2, label: 'never' }],
|
||||
};
|
||||
const md = serializeJournalMarkdown({
|
||||
frontmatter: {},
|
||||
blocks: [block],
|
||||
referencedAssetIds: [],
|
||||
});
|
||||
expect(md).toBe(
|
||||
[
|
||||
'::map A **trip**',
|
||||
'Start: 52.51, 13.47',
|
||||
'photo: abc',
|
||||
'photos: all',
|
||||
'52.4, 13.04',
|
||||
'line: off',
|
||||
].join('\n'),
|
||||
);
|
||||
const reparsed = parseJournalMarkdown(md);
|
||||
const { pins: _pins, ...withoutPins } = block;
|
||||
expect(reparsed.blocks).toEqual([{ ...withoutPins, caption: 'A <strong>trip</strong>' }]);
|
||||
expect(serializeJournalMarkdown(reparsed)).toBe(md);
|
||||
});
|
||||
|
||||
it('drops invalid points and empty photo placeholders on save', () => {
|
||||
const block: JournalBlock = {
|
||||
type: 'map',
|
||||
line: true,
|
||||
items: [
|
||||
{ kind: 'point', lat: Number.NaN, lng: Number.NaN },
|
||||
{ kind: 'photo', assetId: '' },
|
||||
{ kind: 'point', label: 'Kept', lat: 1, lng: 2 },
|
||||
],
|
||||
};
|
||||
expect(
|
||||
serializeJournalMarkdown({ frontmatter: {}, blocks: [block], referencedAssetIds: [] }),
|
||||
).toBe('::map\nKept: 1, 2');
|
||||
});
|
||||
|
||||
it('collects and maps the ids of photo pins', () => {
|
||||
const block: JournalBlock = {
|
||||
type: 'map',
|
||||
line: true,
|
||||
items: [
|
||||
{ kind: 'photo', assetId: 'abc' },
|
||||
{ kind: 'all-photos' },
|
||||
{ kind: 'photo', assetId: '' },
|
||||
],
|
||||
};
|
||||
expect(collectAssetIds([block])).toEqual(['abc']);
|
||||
expect(mapBlockAssetIds(block, (id) => `t:${id}`)).toMatchObject({
|
||||
items: [
|
||||
{ kind: 'photo', assetId: 't:abc' },
|
||||
{ kind: 'all-photos' },
|
||||
{ kind: 'photo', assetId: 't:' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const asset = (
|
||||
id: string,
|
||||
lat: number | null,
|
||||
lng: number | null,
|
||||
city = 'Busan',
|
||||
country = 'South Korea',
|
||||
) => ({
|
||||
id,
|
||||
exifInfo: {
|
||||
latitude: lat,
|
||||
longitude: lng,
|
||||
city,
|
||||
country,
|
||||
} as unknown as NonNullable<import('../immich').ImmichAsset['exifInfo']>,
|
||||
});
|
||||
|
||||
describe('pinsForEntry', () => {
|
||||
const exact = (): LocationPrecision => 'exact';
|
||||
|
||||
it('publishes typed points as typed, no snapping, no label invented', () => {
|
||||
const pins = pinsForEntry(
|
||||
[
|
||||
{ kind: 'point', label: 'Harbour', lat: 35.098123, lng: 129.036456 },
|
||||
{ kind: 'point', lat: 37.5, lng: 127 },
|
||||
],
|
||||
[],
|
||||
[],
|
||||
exact,
|
||||
);
|
||||
expect(pins).toEqual([
|
||||
{ lat: 35.098123, lng: 129.036456, label: 'Harbour' },
|
||||
{ lat: 37.5, lng: 127 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('places a photo pin on the 1 km grid with the city label', () => {
|
||||
const pins = pinsForEntry(
|
||||
[{ kind: 'photo', assetId: 'a' }],
|
||||
[asset('a', 52.510044, 13.467244, 'Berlin', 'Germany')],
|
||||
[],
|
||||
exact,
|
||||
);
|
||||
expect(pins).toEqual([{ lat: 52.51, lng: 13.47, label: 'Berlin, Germany' }]);
|
||||
});
|
||||
|
||||
it('expands photos: all in entry order, skipping ids listed explicitly', () => {
|
||||
const assets = [asset('a', 1, 1), asset('b', 2, 2), asset('c', 3, 3)];
|
||||
const pins = pinsForEntry(
|
||||
[{ kind: 'photo', assetId: 'c' }, { kind: 'all-photos' }],
|
||||
assets,
|
||||
['a', 'b', 'c'],
|
||||
exact,
|
||||
);
|
||||
expect(pins.map((p) => [p.lat, p.lng])).toEqual([
|
||||
[3, 3],
|
||||
[1, 1],
|
||||
[2, 2],
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps the drawing order across mixed items', () => {
|
||||
const pins = pinsForEntry(
|
||||
[
|
||||
{ kind: 'point', label: 'Start', lat: 0.5, lng: 0.5 },
|
||||
{ kind: 'photo', assetId: 'a' },
|
||||
{ kind: 'point', label: 'End', lat: 9, lng: 9 },
|
||||
],
|
||||
[asset('a', 1, 1)],
|
||||
['a'],
|
||||
exact,
|
||||
);
|
||||
expect(pins.map((p) => p.label)).toEqual(['Start', 'Busan, South Korea', 'End']);
|
||||
});
|
||||
|
||||
it('applies precision to photo pins only: city snaps, country and hidden vanish', () => {
|
||||
const levels: Record<string, LocationPrecision> = { a: 'city', b: 'country', c: 'hidden' };
|
||||
const pins = pinsForEntry(
|
||||
[{ kind: 'all-photos' }, { kind: 'point', lat: 52.5163, lng: 13.3777 }],
|
||||
[asset('a', 52.5163, 13.3777, 'Berlin', 'Germany'), asset('b', 2, 2), asset('c', 3, 3)],
|
||||
['a', 'b', 'c'],
|
||||
(id) => levels[id],
|
||||
);
|
||||
expect(pins).toEqual([
|
||||
{ lat: 52.5, lng: 13.4, label: 'Berlin, Germany' },
|
||||
{ lat: 52.5163, lng: 13.3777 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('skips photos without coordinates but keeps the equator (#635)', () => {
|
||||
const pins = pinsForEntry(
|
||||
[{ kind: 'all-photos' }],
|
||||
[asset('a', null, null), asset('b', 0, 0, '', '')],
|
||||
['a', 'b'],
|
||||
exact,
|
||||
);
|
||||
expect(pins).toEqual([{ lat: 0, lng: 0 }]);
|
||||
});
|
||||
|
||||
it('merges items at one position into one pin', () => {
|
||||
const pins = pinsForEntry(
|
||||
[
|
||||
{ kind: 'point', lat: 52.51, lng: 13.47 },
|
||||
{ kind: 'photo', assetId: 'a' },
|
||||
],
|
||||
[asset('a', 52.510044, 13.467244)],
|
||||
[],
|
||||
exact,
|
||||
);
|
||||
expect(pins).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('ignores photo items the page did not fetch', () => {
|
||||
expect(pinsForEntry([{ kind: 'photo', assetId: 'missing' }], [], [], exact)).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user