release: v0.20.0

This commit is contained in:
Ralksta
2026-10-02 18:14:23 +02:00
committed by GitHub
parent 81ce886b01
commit 5f831f719a
209 changed files with 13807 additions and 803 deletions

View File

@@ -251,6 +251,84 @@ describe('ImmichClient', () => {
await expect(immich.streamAsset('asset-1')).rejects.toBeInstanceOf(ImmichUnavailableError);
});
// #467: Immich answers a missing full-size rendition with a redirect — to
// the preview, or to the original for a web format. Neither may be passed
// off as the rendition, so the redirect is not followed.
it('streamFullsize does not follow Immich’s redirect, and reports no rendition', async () => {
const cancel = vi.fn(async () => {});
mockFetch.mockResolvedValueOnce({
ok: false,
status: 302,
body: { cancel },
headers: { get: () => '/api/assets/asset-1/thumbnail?size=preview' },
});
await expect(immich.streamFullsize('asset-1')).resolves.toBeNull();
const [url, init] = mockFetch.mock.calls.at(-1)!;
expect(url).toMatch(/\/assets\/asset-1\/thumbnail\?size=fullsize$/);
expect(init.redirect).toBe('manual');
expect(cancel).toHaveBeenCalled();
});
it('streamFullsize streams a rendition Immich has', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
body: new ReadableStream(),
headers: { get: (h: string) => (h === 'Content-Type' ? 'image/jpeg' : '42') },
});
const result = await immich.streamFullsize('asset-1');
expect(result?.contentType).toBe('image/jpeg');
});
// #831: Immich applies an edit made in its editor only when asked.
describe('edited photos (#831)', () => {
const ok = () => ({
ok: true,
status: 200,
body: new ReadableStream(),
headers: { get: () => 'image/jpeg' },
});
const lastUrl = () => mockFetch.mock.calls.at(-1)![0] as string;
it.each([
['thumbnail', '/assets/asset-1/thumbnail?size=thumbnail&edited=true'],
['preview', '/assets/asset-1/thumbnail?size=preview&edited=true'],
['original', '/assets/asset-1/original?edited=true'],
] as const)('asks for the edited %s', async (size, path) => {
mockFetch.mockResolvedValueOnce(ok());
await immich.streamAsset('asset-1', size, true);
expect(lastUrl()).toBe(`http://immich.test/api${path}`);
});
it.each([
['thumbnail', '/assets/asset-1/thumbnail?size=thumbnail'],
['preview', '/assets/asset-1/thumbnail?size=preview'],
['original', '/assets/asset-1/original'],
] as const)('requests a %s that is not edited exactly as before', async (size, path) => {
mockFetch.mockResolvedValueOnce(ok());
await immich.streamAsset('asset-1', size);
expect(lastUrl()).toBe(`http://immich.test/api${path}`);
mockFetch.mockResolvedValueOnce(ok());
await immich.streamAsset('asset-1', size, false);
expect(lastUrl()).toBe(`http://immich.test/api${path}`);
});
it('asks for the edited full-size rendition, still without following redirects', async () => {
mockFetch.mockResolvedValueOnce(ok());
await immich.streamFullsize('asset-1', true);
expect(lastUrl()).toBe(
'http://immich.test/api/assets/asset-1/thumbnail?size=fullsize&edited=true',
);
expect(mockFetch.mock.calls.at(-1)![1].redirect).toBe('manual');
});
});
it('streamAsset still follows redirects', async () => {
mockFetch.mockResolvedValueOnce(streamRes(404));
await immich.streamAsset('asset-1', 'original');
expect(mockFetch.mock.calls.at(-1)![1].redirect).toBe('follow');
});
it('streamVideo returns null for a genuinely missing video', async () => {
mockFetch.mockResolvedValueOnce(streamRes(404));
await expect(immich.streamVideo('asset-1')).resolves.toBeNull();