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>
74 lines
3.1 KiB
TypeScript
74 lines
3.1 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { getImgProps } from 'next/dist/shared/lib/get-img-props';
|
|
import { imageConfigDefault } from 'next/dist/shared/lib/image-config';
|
|
import immichLoader from '../immichLoader';
|
|
import { canonicalImageUrl, resolveImageSize } from '../imageSize';
|
|
|
|
/**
|
|
* The proxy answers every width inside an Immich tier with the same bytes, but
|
|
* each distinct URL is its own `immutable` cache entry. next/image used to
|
|
* write a different `?w=` into every srcset candidate and the lightbox asked
|
|
* for the bare URL, so a grid tile and the same photo in the lightbox — or a
|
|
* tile before and after a rotation — downloaded the identical preview twice.
|
|
*/
|
|
|
|
const PREVIEW = '/api/image/v2:tok?size=preview';
|
|
const THUMB = '/api/image/v2:tok?size=thumbnail';
|
|
|
|
const ALL_WIDTHS = [...imageConfigDefault.deviceSizes, ...imageConfigDefault.imageSizes];
|
|
|
|
function srcSetUrls(src: string, sizes: string): string[] {
|
|
const { props } = getImgProps(
|
|
{ src, alt: '', fill: true, sizes },
|
|
{
|
|
defaultLoader: immichLoader,
|
|
imgConf: { ...imageConfigDefault, loader: 'custom', loaderFile: './lib/immichLoader.ts' },
|
|
showAltText: false,
|
|
blurComplete: false,
|
|
},
|
|
);
|
|
return (props.srcSet ?? '').split(', ').map((candidate) => candidate.split(' ')[0]);
|
|
}
|
|
|
|
const tierOf = (url: string) => {
|
|
const params = new URLSearchParams(url.slice(url.indexOf('?') + 1));
|
|
return resolveImageSize(params.get('size'), params.get('w'));
|
|
};
|
|
|
|
describe('one image URL per asset and tier', () => {
|
|
it.each([
|
|
['PhotoGrid', '(max-width: 600px) 50vw, (max-width: 1000px) 33vw, 25vw'],
|
|
['SubpageGridView', '(max-width: 600px) 100vw, (max-width: 1000px) 50vw, 33vw'],
|
|
['album hero', '100vw'],
|
|
['home hero', '(max-width: 640px) 100vw, 50vw'],
|
|
['essay', '(max-width: 1100px) 100vw, 1100px'],
|
|
])('every %s srcset candidate is the URL the lightbox opens', (_name, sizes) => {
|
|
const urls = srcSetUrls(PREVIEW, sizes);
|
|
expect(urls.length).toBeGreaterThan(3);
|
|
expect(new Set(urls)).toEqual(new Set([canonicalImageUrl(PREVIEW)]));
|
|
});
|
|
|
|
it('collapses a thumbnail-ceiling image to one URL as well', () => {
|
|
expect(new Set(srcSetUrls(THUMB, '25vw'))).toEqual(new Set([canonicalImageUrl(THUMB)]));
|
|
});
|
|
|
|
it('asks the proxy for the same rendition the raw width did', () => {
|
|
for (const size of ['thumbnail', 'preview', 'original', null]) {
|
|
const src = size ? `/api/image/v2:tok?size=${size}` : '/api/image/v2:tok';
|
|
for (const w of [...ALL_WIDTHS, 1, 250, 251, 1440, 1441, 99999]) {
|
|
expect(tierOf(canonicalImageUrl(src, w))).toBe(resolveImageSize(size, String(w)));
|
|
}
|
|
}
|
|
});
|
|
|
|
it('keeps the cache buster and writes a width next/image accepts', () => {
|
|
expect(canonicalImageUrl('/api/image/v2:tok?size=preview&v=7', 640)).toBe(
|
|
'/api/image/v2:tok?size=preview&v=7&w=1440',
|
|
);
|
|
expect(canonicalImageUrl('https://cdn.example.com/api/image/v2:tok?size=thumbnail', 96)).toBe(
|
|
'https://cdn.example.com/api/image/v2:tok?size=thumbnail&w=250',
|
|
);
|
|
expect(immichLoader({ src: PREVIEW, width: 400 })).not.toBe(PREVIEW);
|
|
});
|
|
});
|