release: v0.13.0 (#535)
Twenty-nine commits since v0.12.0. See CHANGELOG.md for the full list and the upgrade notes. Ten additions — the SEO package (site URL, robots.txt, sitemap.xml, JSON-LD), slideshow mode, opt-in original downloads, copy-link, previous/next album navigation, alt text from the Immich description, per-album location precision, the update notice, a Help tab, and a subpage cover grid separate from its photo grid. Two fixes: CJK album slugs, and the lightbox control bar. Photo of the Day (#476) was built on `dev` and reverted as not planned; it never appears in a release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01DmsHVRL9yQvckUB1ZpVWre --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: ralksta <ralksta@users.noreply.github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
89
lib/__tests__/assetLocation.test.ts
Normal file
89
lib/__tests__/assetLocation.test.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import type { LocationPrecision } from '@/lib/mapPrecision';
|
||||
|
||||
/**
|
||||
* The per-album `location:` setting governed the map and nothing else, so an
|
||||
* album asking to be absent from it still had its city named in the lightbox
|
||||
* info panel. These pin the join: an asset inherits the setting of the albums
|
||||
* that carry it.
|
||||
*/
|
||||
const albumLocationPrecision: Record<string, LocationPrecision> = {};
|
||||
|
||||
vi.mock('@/lib/config', async () => {
|
||||
const actual = await vi.importActual<typeof import('@/lib/config')>('@/lib/config');
|
||||
return {
|
||||
...actual,
|
||||
getConfig: () => ({ albumLocationPrecision }),
|
||||
};
|
||||
});
|
||||
|
||||
const getAlbum = vi.fn();
|
||||
vi.mock('@/lib/immich', () => ({
|
||||
immich: { getAlbum: (id: string) => getAlbum(id) },
|
||||
}));
|
||||
|
||||
import { assetLocationPrecision } from '@/lib/assetLocation';
|
||||
|
||||
const ASSET = 'asset-1';
|
||||
const OTHER = 'asset-2';
|
||||
|
||||
/** An album whose assets are just ids. */
|
||||
const album = (id: string, assetIds: string[]) => ({
|
||||
id,
|
||||
assets: assetIds.map((a) => ({ id: a })),
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
for (const key of Object.keys(albumLocationPrecision)) delete albumLocationPrecision[key];
|
||||
getAlbum.mockReset();
|
||||
});
|
||||
|
||||
describe('assetLocationPrecision', () => {
|
||||
it('is exact when no album restricts anything, without asking Immich at all', async () => {
|
||||
await expect(assetLocationPrecision(ASSET)).resolves.toBe('exact');
|
||||
// The common case must not cost a single album fetch.
|
||||
expect(getAlbum).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('takes the setting of an album that carries the asset', async () => {
|
||||
albumLocationPrecision['album-a'] = 'country';
|
||||
getAlbum.mockResolvedValue(album('album-a', [ASSET]));
|
||||
await expect(assetLocationPrecision(ASSET)).resolves.toBe('country');
|
||||
});
|
||||
|
||||
it('ignores an album that restricts but does not carry the asset', async () => {
|
||||
albumLocationPrecision['album-a'] = 'hidden';
|
||||
getAlbum.mockResolvedValue(album('album-a', [OTHER]));
|
||||
await expect(assetLocationPrecision(ASSET)).resolves.toBe('exact');
|
||||
});
|
||||
|
||||
// Same rule as a merged map marker: the cautious setting governs.
|
||||
it('takes the strictest when several albums carry the asset', async () => {
|
||||
albumLocationPrecision['album-a'] = 'city';
|
||||
albumLocationPrecision['album-b'] = 'hidden';
|
||||
getAlbum.mockImplementation(async (id: string) => album(id, [ASSET]));
|
||||
await expect(assetLocationPrecision(ASSET)).resolves.toBe('hidden');
|
||||
});
|
||||
|
||||
it('only asks about albums that actually restrict something', async () => {
|
||||
albumLocationPrecision['album-a'] = 'exact';
|
||||
albumLocationPrecision['album-b'] = 'city';
|
||||
getAlbum.mockImplementation(async (id: string) => album(id, [ASSET]));
|
||||
await assetLocationPrecision(ASSET);
|
||||
expect(getAlbum).toHaveBeenCalledTimes(1);
|
||||
expect(getAlbum).toHaveBeenCalledWith('album-b');
|
||||
});
|
||||
|
||||
// Failing open would publish the position the setting exists to withhold.
|
||||
it('withholds the place when an album cannot be read', async () => {
|
||||
albumLocationPrecision['album-a'] = 'city';
|
||||
getAlbum.mockRejectedValue(new Error('Immich is down'));
|
||||
await expect(assetLocationPrecision(ASSET)).resolves.toBe('hidden');
|
||||
});
|
||||
|
||||
it('withholds it when the album is gone from the allowlist', async () => {
|
||||
albumLocationPrecision['album-a'] = 'city';
|
||||
getAlbum.mockResolvedValue(null);
|
||||
await expect(assetLocationPrecision(ASSET)).resolves.toBe('hidden');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user