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:
135
lib/__tests__/lightboxShortcuts.test.ts
Normal file
135
lib/__tests__/lightboxShortcuts.test.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import {
|
||||
LIGHTBOX_SHORTCUTS,
|
||||
lightboxActionFor,
|
||||
shortcutDisplayKeys,
|
||||
shortcutKeyLabel,
|
||||
} from '@/lib/lightboxShortcuts';
|
||||
import { en } from '@/lib/i18n/locales/en';
|
||||
import { de } from '@/lib/i18n/locales/de';
|
||||
|
||||
describe('LIGHTBOX_SHORTCUTS', () => {
|
||||
it('is not empty', () => {
|
||||
expect(LIGHTBOX_SHORTCUTS.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
/**
|
||||
* The whole point of the shared catalogue: the `?` overlay and the admin
|
||||
* help render this list, so a label that does not resolve would leave one of
|
||||
* them blank.
|
||||
*/
|
||||
it.each(['en', 'de'])('every label resolves in %s', (locale) => {
|
||||
const dictionary = locale === 'en' ? en.lightbox : de.lightbox;
|
||||
for (const shortcut of LIGHTBOX_SHORTCUTS) {
|
||||
const label = dictionary[shortcut.labelKey];
|
||||
expect(typeof label, shortcut.labelKey).toBe('string');
|
||||
expect((label as string).length).toBeGreaterThan(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('gives every shortcut at least one key', () => {
|
||||
for (const shortcut of LIGHTBOX_SHORTCUTS) {
|
||||
expect(shortcut.bindings.length, shortcut.labelKey).toBeGreaterThan(0);
|
||||
for (const binding of shortcut.bindings) {
|
||||
expect(binding.eventKeys.length, binding.display).toBeGreaterThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('binds no key twice, which would make the list contradict itself', () => {
|
||||
const keys = LIGHTBOX_SHORTCUTS.flatMap((shortcut) =>
|
||||
shortcut.bindings.map((binding) => binding.display),
|
||||
);
|
||||
expect(new Set(keys).size).toBe(keys.length);
|
||||
});
|
||||
|
||||
/**
|
||||
* Two rows claiming the same physical key would make the viewer's behaviour
|
||||
* depend on the order of this list, and the help would describe one of them
|
||||
* wrongly.
|
||||
*/
|
||||
it('binds no KeyboardEvent.key to two actions', () => {
|
||||
const eventKeys = LIGHTBOX_SHORTCUTS.flatMap((shortcut) =>
|
||||
shortcut.bindings.flatMap((binding) => [...binding.eventKeys]),
|
||||
);
|
||||
expect(new Set(eventKeys).size).toBe(eventKeys.length);
|
||||
});
|
||||
|
||||
it('lists each label once', () => {
|
||||
const labels = LIGHTBOX_SHORTCUTS.map((shortcut) => shortcut.labelKey);
|
||||
expect(new Set(labels).size).toBe(labels.length);
|
||||
});
|
||||
|
||||
it('documents the conditional shortcuts, since the reader cannot see them fail', () => {
|
||||
for (const shortcut of LIGHTBOX_SHORTCUTS) {
|
||||
if (shortcut.availability !== 'always') {
|
||||
expect(shortcut.note, shortcut.labelKey).toBeTruthy();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* The catalogue is the only route from a keypress to an action (#473), so
|
||||
* these cover the join the viewer relies on.
|
||||
*/
|
||||
describe('lightboxActionFor', () => {
|
||||
it('resolves every declared key to its action', () => {
|
||||
for (const shortcut of LIGHTBOX_SHORTCUTS) {
|
||||
for (const binding of shortcut.bindings) {
|
||||
for (const key of binding.eventKeys) {
|
||||
expect(lightboxActionFor(key), key).toBe(binding.action);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves the keys a visitor actually presses', () => {
|
||||
expect(lightboxActionFor('ArrowLeft')).toBe('prev');
|
||||
expect(lightboxActionFor('ArrowRight')).toBe('next');
|
||||
expect(lightboxActionFor('Escape')).toBe('close');
|
||||
expect(lightboxActionFor('?')).toBe('shortcutList');
|
||||
expect(lightboxActionFor('h')).toBe('shortcutList');
|
||||
expect(lightboxActionFor('H')).toBe('shortcutList');
|
||||
});
|
||||
|
||||
it('takes both cases of every letter key, since Shift must not matter', () => {
|
||||
for (const [lower, upper] of [
|
||||
['i', 'I'],
|
||||
['f', 'F'],
|
||||
['s', 'S'],
|
||||
['c', 'C'],
|
||||
['d', 'D'],
|
||||
]) {
|
||||
expect(lightboxActionFor(lower), lower).not.toBeNull();
|
||||
expect(lightboxActionFor(upper), upper).toBe(lightboxActionFor(lower));
|
||||
}
|
||||
});
|
||||
|
||||
it('returns null for a key the viewer does not bind', () => {
|
||||
for (const key of ['a', 'Enter', 'Tab', ' ', 'ArrowUp', 'F5']) {
|
||||
expect(lightboxActionFor(key), key).toBeNull();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('shortcutKeyLabel', () => {
|
||||
it('renders the arrow names as arrows', () => {
|
||||
expect(shortcutKeyLabel('ARROW_LEFT')).toBe(String.fromCharCode(0x2190));
|
||||
expect(shortcutKeyLabel('ARROW_RIGHT')).toBe(String.fromCharCode(0x2192));
|
||||
});
|
||||
|
||||
it('passes ordinary keys through', () => {
|
||||
expect(shortcutKeyLabel('S')).toBe('S');
|
||||
expect(shortcutKeyLabel('Esc')).toBe('Esc');
|
||||
expect(shortcutKeyLabel('?')).toBe('?');
|
||||
});
|
||||
|
||||
it('leaves no placeholder name unrendered', () => {
|
||||
for (const shortcut of LIGHTBOX_SHORTCUTS) {
|
||||
for (const key of shortcutDisplayKeys(shortcut)) {
|
||||
expect(key).not.toMatch(/^ARROW_/);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user