Files
immich-folio/lib/__tests__/pages.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

156 lines
5.3 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
RESERVED_PAGE_SLUGS,
describeCollision,
menuPageSlugs,
pageSlugCollision,
resolveMenu,
type NavEntry,
type PageSummary,
} from '../pages';
import { deriveGallery, type GalleryYaml } from '../config';
import { rewritePageRefs } from '../admin/pageRefs';
const A1 = '11111111-1111-4111-8111-111111111111';
const A2 = '22222222-2222-4222-8222-222222222222';
const none = { subpages: [], albums: [], journal: [] };
describe('pageSlugCollision (#722)', () => {
it('accepts a free slug', () => {
expect(pageSlugCollision('pricing', none)).toBeNull();
});
it.each(RESERVED_PAGE_SLUGS)('refuses the built-in route /%s', (slug) => {
expect(pageSlugCollision(slug, none)).toEqual({ kind: 'reserved' });
});
it('refuses a slug taken by a subpage, an album or a journal entry', () => {
const taken = { subpages: ['travel'], albums: ['iceland'], journal: ['trip'] };
expect(pageSlugCollision('travel', taken)).toEqual({ kind: 'subpage' });
expect(pageSlugCollision('iceland', taken)).toEqual({ kind: 'album' });
expect(pageSlugCollision('trip', taken)).toEqual({ kind: 'journal' });
});
it('compares without regard to case', () => {
expect(pageSlugCollision('Travel', { ...none, subpages: ['travel'] })).toEqual({
kind: 'subpage',
});
expect(pageSlugCollision('ADMIN', none)).toEqual({ kind: 'reserved' });
});
it('refuses what is not a slug at all', () => {
expect(pageSlugCollision('a/b', none)).toEqual({ kind: 'invalid' });
expect(pageSlugCollision('', none)).toEqual({ kind: 'invalid' });
});
it('describes each collision in a sentence naming the slug', () => {
for (const kind of ['invalid', 'reserved', 'subpage', 'album', 'journal'] as const) {
expect(describeCollision('x', { kind })).toContain('x');
}
});
});
describe('deriveGallery: `- page:` references (#722)', () => {
const gallery = (subpages: NonNullable<GalleryYaml['subpages']>): GalleryYaml => ({ subpages });
it('keeps pages and subpages in one menu order', () => {
const { nav, subpages } = deriveGallery(
gallery([
{ page: 'pricing' },
{ name: 'Travel', albums: [A1] },
{ page: 'faq' },
{ name: 'Weddings', albums: [A2] },
]),
);
expect(subpages.map((s) => s.slug)).toEqual(['travel', 'weddings']);
expect(nav).toEqual([
{ type: 'page', slug: 'pricing' },
{ type: 'subpage', slug: 'travel' },
{ type: 'page', slug: 'faq' },
{ type: 'subpage', slug: 'weddings' },
]);
expect(menuPageSlugs(nav)).toEqual(['pricing', 'faq']);
});
it('allows a gallery whose menu holds only pages', () => {
expect(deriveGallery(gallery([{ page: 'pricing' }])).nav).toEqual([
{ type: 'page', slug: 'pricing' },
]);
});
it('rejects an invalid page slug and a page listed twice', () => {
expect(() => deriveGallery(gallery([{ page: '../x' }]))).toThrow(/not a valid page slug/);
expect(() => deriveGallery(gallery([{ page: 'a' }, { page: 'a' }]))).toThrow(/more than once/);
});
it('builds the menu from the map form of subpages too', () => {
const { nav } = deriveGallery({ subpages: { Travel: [A1] } });
expect(nav).toEqual([{ type: 'subpage', slug: 'travel' }]);
});
});
describe('resolveMenu (#722)', () => {
const nav: NavEntry[] = [
{ type: 'subpage', slug: 'travel' },
{ type: 'page', slug: 'pricing' },
{ type: 'subpage', slug: 'hidden' },
{ type: 'page', slug: 'draft' },
{ type: 'page', slug: 'missing' },
{ type: 'page', slug: 'locked' },
];
const pages: PageSummary[] = [
{ slug: 'pricing', frontmatter: { title: 'Pricing' } },
{ slug: 'draft', frontmatter: { title: 'Soon', draft: true } },
{ slug: 'locked', frontmatter: { password: 'scrypt:a:b' } },
];
it('interleaves visible subpages and published pages in gallery.yaml order', () => {
const menu = resolveMenu(nav, [{ slug: 'travel', name: 'Travel' }], pages);
expect(menu.map((m) => [m.href, m.label])).toEqual([
['/travel', 'Travel'],
['/pricing', 'Pricing'],
// A protected page stays listed; its title falls back to the slug.
['/locked', 'locked'],
]);
});
// `name` is the gallery.yaml key the slug is built from ("south-korea");
// `title` is what the page itself shows ("South Korea"). The menu showed
// the key.
it('labels a subpage with its title, falling back to its name', () => {
const menu = resolveMenu(
[
{ type: 'subpage', slug: 'south-korea' },
{ type: 'subpage', slug: 'travel' },
],
[
{ slug: 'south-korea', name: 'south-korea', title: 'South Korea' },
{ slug: 'travel', name: 'Travel' },
],
[],
);
expect(menu.map((m) => m.label)).toEqual(['South Korea', 'Travel']);
});
});
describe('rewritePageRefs (#722)', () => {
const list = [{ name: 'Travel' }, { page: 'pricing' }, { page: 'faq' }];
it('renames a reference in place', () => {
expect(rewritePageRefs(list, 'pricing', 'rates')).toEqual([
{ name: 'Travel' },
{ page: 'rates' },
{ page: 'faq' },
]);
});
it('removes a reference', () => {
expect(rewritePageRefs(list, 'faq', null)).toEqual([{ name: 'Travel' }, { page: 'pricing' }]);
});
it('returns the same list when nothing matches', () => {
expect(rewritePageRefs(list, 'nothing', null)).toBe(list);
});
});