Files
immich-folio/app/admin/__tests__/gallery-yaml.test.ts
Ralksta ad8b09700b release: v0.19.0
Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of
[CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md)
for the full list.

Highlights: client proofing links (#683), custom content pages (#738),
asset picker search (#740), concurrent-edit detection (#742), env-locked
settings (#739), an accessibility baseline (#741), three security fixes
with advisories (#737, #755, #759), and a pre-release quality pass
(#780–#797).

---------

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>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
2026-09-29 22:50:30 +02:00

189 lines
6.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import { parseGalleryYaml, serializeGallery } from '../components/page-builder/galleryYaml';
import type { GalleryYaml } from '@/lib/config/schema';
/**
* #608: gallery.yaml ⇄ the page builder's model. This is what decides what the
* builder writes on save, and until it left PageBuilder.tsx nothing tested it.
* Album entries themselves are covered in album-entries-round-trip.test.ts.
*/
const A = '11111111-1111-1111-1111-111111111111';
const B = '22222222-2222-2222-2222-222222222222';
/** Load and save once, the way the builder does without any edit in between. */
const roundTrip = (yaml: Record<string, unknown>) => serializeGallery(parseGalleryYaml(yaml));
describe('gallery.yaml round trip', () => {
it('keeps hero photos, standalone albums and a plain subpage', () => {
const yaml = {
hero: ['h1', 'h2'],
albums: [A],
subpages: [{ name: 'Japan', albums: [B] }],
};
expect(roundTrip(yaml)).toEqual(yaml);
});
it('turns a single hero string into a list', () => {
expect(roundTrip({ hero: 'h1' })).toEqual({ hero: ['h1'] });
});
it('leaves empty groups out instead of writing empty lists', () => {
expect(roundTrip({ hero: [], albums: [], subpages: [] })).toEqual({});
});
it('keeps every subpage field the builder knows', () => {
const sp = {
name: 'Clients',
title: 'For clients',
subtitle: 'Handover',
password: 'scrypt:aa:bb',
enabled: false,
hidden: true,
essayText: 'Text',
essayFile: 'story',
grid: { columns: 4, layout: 'justified' },
coverGrid: { columns: 2, gap: 8 },
location: 'city',
albums: [A],
};
expect(roundTrip({ subpages: [sp] })).toEqual({ subpages: [sp] });
});
it('writes enabled and hidden only when they differ from the default', () => {
const out = roundTrip({ subpages: [{ name: 'P', enabled: true, hidden: false, albums: [A] }] });
expect(out).toEqual({ subpages: [{ name: 'P', albums: [A] }] });
});
it('keeps sections, and page-level albums next to them only when there are any', () => {
const withSections = {
subpages: [
{
name: 'Trips',
sections: [
{ title: 'North', description: 'Cold', albums: [A] },
{ title: 'South', albums: [B] },
],
},
],
};
expect(roundTrip(withSections)).toEqual(withSections);
const both = {
subpages: [{ name: 'Trips', albums: [B], sections: [{ title: 'North', albums: [A] }] }],
};
expect(roundTrip(both)).toEqual({
subpages: [{ name: 'Trips', sections: [{ title: 'North', albums: [A] }], albums: [B] }],
});
});
it('converts the older name → albums map into the list the builder writes', () => {
expect(
roundTrip({ subpages: { Japan: [A], Korea: { title: 'South Korea', albums: [B] } } }),
).toEqual({
subpages: [
{ name: 'Japan', albums: [A] },
{ name: 'Korea', title: 'South Korea', albums: [B] },
],
});
});
/*
* #523 split `grid` into photo grid and cover grid. A pre-#523 page keeps
* its cover columns and gap by being seeded from `grid` on load, so the
* first save writes the split out explicitly.
*/
it('seeds the cover grid from a pre-#523 grid', () => {
const out = roundTrip({
subpages: [{ name: 'Old', grid: { columns: 3, gap: 10, layout: 'masonry' }, albums: [A] }],
});
expect(out).toEqual({
subpages: [
{
name: 'Old',
grid: { columns: 3, gap: 10, layout: 'masonry' },
coverGrid: { columns: 3, gap: 10 },
albums: [A],
},
],
});
});
it('is stable: a second round trip changes nothing', () => {
const once = roundTrip({
hero: 'h1',
subpages: { Japan: { grid: { columns: 2 }, albums: [A] } },
});
expect(roundTrip(once)).toEqual(once);
});
});
/**
* The builder rewrites gallery.yaml wholesale on every save, so a subpage key
* it does not carry is deleted, not ignored. `proofing` was lost this way: a
* page switched off for client proofing by hand came back on after any
* unrelated save in the admin. Same lesson as #614 for album entries.
*
* The type annotation is the enforcement: every key the schema allows on a
* subpage has to be listed here, so the next one added and forgotten in
* galleryYaml.ts fails this test instead of vanishing from someone's file.
*/
type SubpageYaml = Extract<NonNullable<GalleryYaml['subpages']>, unknown[]>[number];
const everySubpageKey: Required<SubpageYaml> = {
name: 'Clients',
title: 'For clients',
subtitle: 'Handover',
albums: [A],
sections: [{ title: 'North', description: 'Cold', albums: [B] }],
password: 'scrypt:aa:bb',
proofing: false,
essayFile: 'story',
essayText: 'Text',
enabled: false,
hidden: true,
location: 'city',
grid: { columns: 4, gap: 6, aspectRatio: '3/2', layout: 'justified' },
coverGrid: { columns: 2, gap: 8, aspectRatio: '1', layout: 'masonry' },
};
describe('subpage keys', () => {
it('survives a round trip with every key the schema allows', () => {
expect(roundTrip({ subpages: [everySubpageKey] })).toEqual({ subpages: [everySubpageKey] });
});
it('keeps proofing in either direction, and writes nothing when unset', () => {
const on = { name: 'P', proofing: true, albums: [A] };
expect(roundTrip({ subpages: [on] })).toEqual({ subpages: [on] });
expect(roundTrip({ subpages: [{ name: 'P', albums: [A] }] })).toEqual({
subpages: [{ name: 'P', albums: [A] }],
});
});
});
describe('content page references (#722)', () => {
it('keeps `- page:` entries where they were among the subpages', () => {
const yaml = {
subpages: [
{ page: 'pricing' },
{ name: 'Japan', albums: [A] },
{ page: 'faq' },
{ name: 'Korea', albums: [B] },
{ page: 'contact-me' },
],
};
expect(roundTrip(yaml)).toEqual(yaml);
});
it('parses page references apart from the subpages', () => {
const g = parseGalleryYaml({ subpages: [{ name: 'Japan', albums: [A] }, { page: 'faq' }] });
expect(g.subpages.map((s) => s.name)).toEqual(['Japan']);
expect(g.pageRefs).toEqual([{ slug: 'faq', position: 1 }]);
});
it('writes a menu of pages only', () => {
const yaml = { subpages: [{ page: 'pricing' }] };
expect(roundTrip(yaml)).toEqual(yaml);
});
});