From 2dd785e3e2b9b038883b17420fd46b00da5318d2 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:01:15 +0100 Subject: [PATCH] fix(web): restore duplicate viewer arrow key navigation (#27176) --- e2e/src/specs/web/duplicates.e2e-spec.ts | 51 +++++++++++++++++++ .../[[assetId=id]]/+page.svelte | 22 +++----- 2 files changed, 57 insertions(+), 16 deletions(-) create mode 100644 e2e/src/specs/web/duplicates.e2e-spec.ts diff --git a/e2e/src/specs/web/duplicates.e2e-spec.ts b/e2e/src/specs/web/duplicates.e2e-spec.ts new file mode 100644 index 0000000000..34f11cdf78 --- /dev/null +++ b/e2e/src/specs/web/duplicates.e2e-spec.ts @@ -0,0 +1,51 @@ +import { AssetMediaResponseDto, LoginResponseDto, updateAssets } from '@immich/sdk'; +import { expect, test } from '@playwright/test'; +import crypto from 'node:crypto'; +import { asBearerAuth, utils } from 'src/utils'; + +test.describe('Duplicates Utility', () => { + let admin: LoginResponseDto; + let firstAsset: AssetMediaResponseDto; + let secondAsset: AssetMediaResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + }); + + test.beforeEach(async ({ context }) => { + [firstAsset, secondAsset] = await Promise.all([ + utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-a' }), + utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-b' }), + ]); + + await updateAssets( + { + assetBulkUpdateDto: { + ids: [firstAsset.id, secondAsset.id], + duplicateId: crypto.randomUUID(), + }, + }, + { headers: asBearerAuth(admin.accessToken) }, + ); + + await utils.setAuthCookies(context, admin.accessToken); + }); + + test('navigates with arrow keys between duplicate preview assets', async ({ page }) => { + await page.goto('/utilities/duplicates'); + await page.getByRole('button', { name: 'View' }).first().click(); + await page.waitForSelector('#immich-asset-viewer'); + + const getViewedAssetId = () => new URL(page.url()).pathname.split('/').at(-1) ?? ''; + const initialAssetId = getViewedAssetId(); + expect([firstAsset.id, secondAsset.id]).toContain(initialAssetId); + + await page.keyboard.press('ArrowRight'); + await expect.poll(getViewedAssetId).not.toBe(initialAssetId); + + await page.keyboard.press('ArrowLeft'); + await expect.poll(getViewedAssetId).toBe(initialAssetId); + }); +}); diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index f1b9ca7614..c7c0c146ad 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -178,19 +178,7 @@ const handleFirst = () => navigateToIndex(0); const handlePrevious = () => navigateToIndex(Math.max(duplicatesIndex - 1, 0)); - const handlePreviousShortcut = async () => { - if ($showAssetViewer) { - return; - } - await handlePrevious(); - }; const handleNext = async () => navigateToIndex(Math.min(duplicatesIndex + 1, duplicates.length - 1)); - const handleNextShortcut = async () => { - if ($showAssetViewer) { - return; - } - await handleNext(); - }; const handleLast = () => navigateToIndex(duplicates.length - 1); const navigateToIndex = async (index: number) => @@ -198,10 +186,12 @@