diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1e72dc5cc..92731cb95c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -717,8 +717,8 @@ importers: specifier: file:../open-api/typescript-sdk version: link:../open-api/typescript-sdk '@immich/ui': - specifier: ^0.50.1 - version: 0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1) + specifier: ^0.52.0 + version: 0.52.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1) '@mapbox/mapbox-gl-rtl-text': specifier: 0.2.3 version: 0.2.3(mapbox-gl@1.13.3) @@ -3054,8 +3054,8 @@ packages: peerDependencies: svelte: ^5.0.0 - '@immich/ui@0.50.1': - resolution: {integrity: sha512-fNlQGh75ZFa/UZAgJaYk9/ItHOXHNNzN4CunjCmE7WocVVkUZbUxopN9Ku3F5GULSqD/zJ5gNO6PQAZ1ZoSaaQ==} + '@immich/ui@0.52.0': + resolution: {integrity: sha512-ECQIE5qYNpe7Q5+hifIGUDaRQXBkPOp9dvZaHELWWzAGIhbwG+mUYwMpUgU2TO7fV5u8XU6nHyBuC055zApiWQ==} peerDependencies: svelte: ^5.0.0 @@ -10677,6 +10677,10 @@ packages: resolution: {integrity: sha512-i/w5Ie4tENfGYbdCo2iJ+oies0vOFd8QXWHopKOUzudfLCvnmeheF2PpHp89Z2azpc+c2su3lMiWO/SpP+429A==} engines: {node: '>=0.12.18'} + simple-icons@16.4.0: + resolution: {integrity: sha512-8CKtCvx1Zq3L0CBsR4RR1MjGCXkXbzdspwl2yCxs8oWkstbzj2+DatRKDee/tuj3Ffd/2CDzwEky9RgG2yggew==} + engines: {node: '>=0.12.18'} + sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -15026,14 +15030,14 @@ snapshots: dependencies: svelte: 5.46.1 - '@immich/ui@0.50.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)': + '@immich/ui@0.52.0(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)': dependencies: '@immich/svelte-markdown-preprocess': 0.1.0(svelte@5.46.1) '@internationalized/date': 3.10.0 '@mdi/js': 7.4.47 bits-ui: 2.14.4(@internationalized/date@3.10.0)(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1)(vite@7.3.0(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(sass@1.97.1)(terser@5.44.1)(yaml@2.8.2)))(svelte@5.46.1) luxon: 3.7.2 - simple-icons: 15.22.0 + simple-icons: 16.4.0 svelte: 5.46.1 svelte-highlight: 7.9.0 tailwind-merge: 3.4.0 @@ -24017,6 +24021,8 @@ snapshots: simple-icons@15.22.0: {} + simple-icons@16.4.0: {} + sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.29 diff --git a/web/package.json b/web/package.json index 55f7248375..50a9a7d91d 100644 --- a/web/package.json +++ b/web/package.json @@ -19,7 +19,7 @@ "format": "prettier --check .", "format:fix": "prettier --write . && pnpm run format:i18n", "format:i18n": "pnpm dlx sort-json ../i18n/*.json", - "test": "vitest --run", + "test": "vitest", "test:cov": "vitest --coverage", "test:watch": "vitest dev", "prepare": "svelte-kit sync" @@ -28,7 +28,7 @@ "@formatjs/icu-messageformat-parser": "^2.9.8", "@immich/justified-layout-wasm": "^0.4.3", "@immich/sdk": "file:../open-api/typescript-sdk", - "@immich/ui": "^0.50.1", + "@immich/ui": "^0.52.0", "@mapbox/mapbox-gl-rtl-text": "0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.14.0", diff --git a/web/src/lib/components/TestWrapper.svelte b/web/src/lib/components/TestWrapper.svelte new file mode 100644 index 0000000000..918053cfc3 --- /dev/null +++ b/web/src/lib/components/TestWrapper.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 9e396bec3e..67ae2daf2e 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -1,4 +1,5 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; +import { renderWithTooltips } from '$tests/helpers'; import { albumFactory } from '@test-data/factories/album-factory'; import '@testing-library/jest-dom'; import { render, waitFor, type RenderResult } from '@testing-library/svelte'; @@ -88,7 +89,7 @@ describe('AlbumCard component', () => { const album = Object.freeze(albumFactory.build({ albumThumbnailAssetId: null })); beforeEach(async () => { - sut = render(AlbumCard, { album, onShowContextMenu }); + sut = renderWithTooltips(AlbumCard, { album, onShowContextMenu }); const albumImgElement = sut.getByTestId('album-image'); await waitFor(() => expect(albumImgElement).toHaveAttribute('src')); diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts index 98d99d8d77..126beead9c 100644 --- a/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts +++ b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts @@ -1,7 +1,7 @@ +import { renderWithTooltips } from '$tests/helpers'; import type { AssetResponseDto } from '@immich/sdk'; import { assetFactory } from '@test-data/factories/asset-factory'; import '@testing-library/jest-dom'; -import { render } from '@testing-library/svelte'; import DeleteAction from './delete-action.svelte'; let asset: AssetResponseDto; @@ -13,8 +13,12 @@ describe('DeleteAction component', () => { }); it('displays a button to move the asset to the trash bin', () => { - const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn(), preAction: vi.fn() }); - expect(getByTitle('delete')).toBeInTheDocument(); + const { getByLabelText, queryByTitle } = renderWithTooltips(DeleteAction, { + asset, + onAction: vi.fn(), + preAction: vi.fn(), + }); + expect(getByLabelText('delete')).toBeInTheDocument(); expect(queryByTitle('deletePermanently')).toBeNull(); }); }); @@ -25,8 +29,12 @@ describe('DeleteAction component', () => { }); it('displays a button to permanently delete the asset', () => { - const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn(), preAction: vi.fn() }); - expect(getByTitle('permanently_delete')).toBeInTheDocument(); + const { getByLabelText, queryByTitle } = renderWithTooltips(DeleteAction, { + asset, + onAction: vi.fn(), + preAction: vi.fn(), + }); + expect(getByLabelText('permanently_delete')).toBeInTheDocument(); expect(queryByTitle('delete')).toBeNull(); }); }); diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index 55231c11ae..48c72221e5 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -1,9 +1,9 @@ import { preferences as preferencesStore, resetSavedUser, user as userStore } from '$lib/stores/user.store'; +import { renderWithTooltips } from '$tests/helpers'; import { assetFactory } from '@test-data/factories/asset-factory'; import { preferencesFactory } from '@test-data/factories/preferences-factory'; import { userAdminFactory } from '@test-data/factories/user-factory'; import '@testing-library/jest-dom'; -import { render } from '@testing-library/svelte'; import AssetViewerNavBar from './asset-viewer-nav-bar.svelte'; describe('AssetViewerNavBar component', () => { @@ -16,12 +16,13 @@ describe('AssetViewerNavBar component', () => { showShareButton: false, preAction: () => {}, onZoomImage: () => {}, - onCopyImage: () => {}, onAction: () => {}, onRunJob: () => {}, onPlaySlideshow: () => {}, onShowDetail: () => {}, onClose: () => {}, + playOriginalVideo: false, + setPlayOriginalVideo: () => Promise.resolve(), }; beforeAll(() => { @@ -51,8 +52,8 @@ describe('AssetViewerNavBar component', () => { preferencesStore.set(prefs); const asset = assetFactory.build({ isTrashed: false }); - const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps }); - expect(getByTitle('go_back')).toBeInTheDocument(); + const { getByLabelText } = renderWithTooltips(AssetViewerNavBar, { asset, ...additionalProps }); + expect(getByLabelText('go_back')).toBeInTheDocument(); }); describe('if the current user owns the asset', () => { @@ -65,8 +66,8 @@ describe('AssetViewerNavBar component', () => { const prefs = preferencesFactory.build({ cast: { gCastEnabled: false } }); preferencesStore.set(prefs); - const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps }); - expect(getByTitle('delete')).toBeInTheDocument(); + const { getByLabelText } = renderWithTooltips(AssetViewerNavBar, { asset, ...additionalProps }); + expect(getByLabelText('delete')).toBeInTheDocument(); }); }); }); diff --git a/web/src/lib/components/faces-page/manage-people-visibility.spec.ts b/web/src/lib/components/faces-page/manage-people-visibility.spec.ts deleted file mode 100644 index 6762b31416..0000000000 --- a/web/src/lib/components/faces-page/manage-people-visibility.spec.ts +++ /dev/null @@ -1,119 +0,0 @@ -import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock'; -import { sdkMock } from '$lib/__mocks__/sdk.mock'; -import ManagePeopleVisibility from '$lib/components/faces-page/manage-people-visibility.svelte'; -import type { PersonResponseDto } from '@immich/sdk'; -import { personFactory } from '@test-data/factories/person-factory'; -import { render } from '@testing-library/svelte'; -import { tick } from 'svelte'; - -describe('ManagePeopleVisibility Component', () => { - let personVisible: PersonResponseDto; - let personHidden: PersonResponseDto; - let personWithoutName: PersonResponseDto; - - beforeAll(() => { - // Prevents errors from `img.decode()` in ImageThumbnail - Object.defineProperty(HTMLImageElement.prototype, 'decode', { - value: vi.fn(), - }); - }); - - beforeEach(() => { - vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock()); - personVisible = personFactory.build({ isHidden: false }); - personHidden = personFactory.build({ isHidden: true }); - personWithoutName = personFactory.build({ isHidden: false, name: undefined }); - sdkMock.updatePeople.mockResolvedValue([]); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('does not update people when no changes are made', () => { - const { getByText } = render(ManagePeopleVisibility, { - props: { - people: [personVisible, personHidden, personWithoutName], - totalPeopleCount: 3, - onClose: vi.fn(), - loadNextPage: vi.fn(), - }, - }); - - const saveButton = getByText('done'); - saveButton.click(); - expect(sdkMock.updatePeople).not.toHaveBeenCalled(); - }); - - // svelte animations require a real browser - it.skip('hides unnamed people on first button press', () => { - const { getByText, getByTitle } = render(ManagePeopleVisibility, { - props: { - people: [personVisible, personHidden, personWithoutName], - totalPeopleCount: 3, - onClose: vi.fn(), - loadNextPage: vi.fn(), - }, - }); - - getByTitle('hide_unnamed_people').click(); - getByText('done').click(); - - expect(sdkMock.updatePeople).toHaveBeenCalledWith({ - peopleUpdateDto: { - people: [{ id: personWithoutName.id, isHidden: true }], - }, - }); - }); - - // svelte animations require a real browser - it.skip('hides all people on second button press', async () => { - const { getByText, getByTitle } = render(ManagePeopleVisibility, { - props: { - people: [personVisible, personHidden, personWithoutName], - totalPeopleCount: 3, - onClose: vi.fn(), - loadNextPage: vi.fn(), - }, - }); - - getByTitle('hide_unnamed_people').click(); - await tick(); - getByTitle('hide_all_people').click(); - getByText('done').click(); - - expect(sdkMock.updatePeople).toHaveBeenCalledWith({ - peopleUpdateDto: { - people: expect.arrayContaining([ - { id: personVisible.id, isHidden: true }, - { id: personWithoutName.id, isHidden: true }, - ]), - }, - }); - }); - - // svelte animations require a real browser - it.skip('shows all people on third button press', async () => { - const { getByText, getByTitle } = render(ManagePeopleVisibility, { - props: { - people: [personVisible, personHidden, personWithoutName], - totalPeopleCount: 3, - onClose: vi.fn(), - loadNextPage: vi.fn(), - }, - }); - - getByTitle('hide_unnamed_people').click(); - await tick(); - getByTitle('hide_all_people').click(); - await tick(); - getByTitle('show_all_people').click(); - getByText('done').click(); - - expect(sdkMock.updatePeople).toHaveBeenCalledWith({ - peopleUpdateDto: { - people: [{ id: personHidden.id, isHidden: false }], - }, - }); - }); -}); diff --git a/web/src/lib/components/shared-components/__test__/combobox.spec.ts b/web/src/lib/components/shared-components/__test__/combobox.spec.ts deleted file mode 100644 index e1518809b4..0000000000 --- a/web/src/lib/components/shared-components/__test__/combobox.spec.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { getIntersectionObserverMock } from '$lib/__mocks__/intersection-observer.mock'; -import { getVisualViewportMock } from '$lib/__mocks__/visual-viewport.mock'; -import Combobox from '$lib/components/shared-components/combobox.svelte'; -import { render, screen } from '@testing-library/svelte'; - -describe('Combobox component', () => { - beforeAll(() => { - vi.stubGlobal('IntersectionObserver', getIntersectionObserverMock()); - vi.stubGlobal('visualViewport', getVisualViewportMock()); - }); - - it('shows selected option', () => { - render(Combobox, { - label: 'test', - selectedOption: { label: 'option-1', value: 'option-1' }, - }); - - expect(screen.getByRole('combobox')).toHaveValue('option-1'); - }); - - it('clears the selected option when set to undefined', async () => { - const { rerender } = render(Combobox, { - label: 'test', - selectedOption: { label: 'option-1', value: 'option-1' }, - }); - - await rerender({ selectedOption: undefined }); - expect(screen.getByRole('combobox')).toHaveValue(''); - }); -}); diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 3a1d4f49f9..d921392512 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -21,7 +21,14 @@ import { copyToClipboard, getReleaseType, semverToName } from '$lib/utils'; import { maintenanceShouldRedirect } from '$lib/utils/maintenance'; import { isAssetViewerRoute } from '$lib/utils/navigation'; - import { CommandPaletteContext, modalManager, setTranslations, toastManager, type ActionItem } from '@immich/ui'; + import { + CommandPaletteDefaultProvider, + TooltipProvider, + modalManager, + setTranslations, + toastManager, + type ActionItem, + } from '@immich/ui'; import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiThemeLightDark } from '@mdi/js'; import { onMount, type Snippet } from 'svelte'; import { t } from 'svelte-i18n'; @@ -180,7 +187,7 @@ - + {page.data.meta?.title || 'Web'} - Immich @@ -228,15 +235,17 @@ }} /> -{#if page.data.error} - -{:else} - {@render children?.()} -{/if} + + {#if page.data.error} + + {:else} + {@render children?.()} + {/if} -{#if showNavigationLoadingBar} - -{/if} + {#if showNavigationLoadingBar} + + {/if} - - + + + diff --git a/web/src/routes/admin/library-management/(list)/+layout.svelte b/web/src/routes/admin/library-management/(list)/+layout.svelte index e741dbd610..3f9374283f 100644 --- a/web/src/routes/admin/library-management/(list)/+layout.svelte +++ b/web/src/routes/admin/library-management/(list)/+layout.svelte @@ -8,7 +8,7 @@ import { locale } from '$lib/stores/preferences.store'; import { getBytesWithUnit } from '$lib/utils/byte-units'; import { getLibrary, getLibraryStatistics, type LibraryResponseDto } from '@immich/sdk'; - import { Button, CommandPaletteContext } from '@immich/ui'; + import { Button, CommandPaletteDefaultProvider } from '@immich/ui'; import type { Snippet } from 'svelte'; import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; @@ -51,7 +51,7 @@ - +
diff --git a/web/src/routes/admin/library-management/[id]/+layout.svelte b/web/src/routes/admin/library-management/[id]/+layout.svelte index 62b4891459..762d3ea073 100644 --- a/web/src/routes/admin/library-management/[id]/+layout.svelte +++ b/web/src/routes/admin/library-management/[id]/+layout.svelte @@ -16,7 +16,7 @@ } from '$lib/services/library.service'; import { getBytesWithUnit } from '$lib/utils/byte-units'; import type { LibraryResponseDto } from '@immich/sdk'; - import { Code, CommandPaletteContext, Container, Heading, modalManager } from '@immich/ui'; + import { Code, CommandPaletteDefaultProvider, Container, Heading, modalManager } from '@immich/ui'; import { mdiCameraIris, mdiChartPie, mdiFilterMinusOutline, mdiFolderOutline, mdiPlayCircle } from '@mdi/js'; import type { Snippet } from 'svelte'; import { t } from 'svelte-i18n'; @@ -51,7 +51,7 @@ - + - + diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index 4a8987708e..f531c8337b 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -26,7 +26,7 @@ import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte'; import { systemConfigManager } from '$lib/managers/system-config-manager.svelte'; import { getSystemConfigActions } from '$lib/services/system-config.service'; - import { Alert, CommandPaletteContext, Container } from '@immich/ui'; + import { Alert, CommandPaletteDefaultProvider, Container } from '@immich/ui'; import { mdiAccountOutline, mdiBackupRestore, @@ -214,7 +214,7 @@ ); - + diff --git a/web/src/routes/admin/users/(list)/+layout.svelte b/web/src/routes/admin/users/(list)/+layout.svelte index a8c281690d..1979054014 100644 --- a/web/src/routes/admin/users/(list)/+layout.svelte +++ b/web/src/routes/admin/users/(list)/+layout.svelte @@ -5,7 +5,7 @@ import { locale } from '$lib/stores/preferences.store'; import { getByteUnitString } from '$lib/utils/byte-units'; import { searchUsersAdmin, type UserAdminResponseDto } from '@immich/sdk'; - import { Button, CommandPaletteContext, Container, Icon } from '@immich/ui'; + import { Button, CommandPaletteDefaultProvider, Container, Icon } from '@immich/ui'; import { mdiInfinity } from '@mdi/js'; import type { Snippet } from 'svelte'; import { t } from 'svelte-i18n'; @@ -44,7 +44,7 @@ {onUserAdminDeleted} /> - + diff --git a/web/src/routes/admin/users/[id]/+layout.svelte b/web/src/routes/admin/users/[id]/+layout.svelte index d3fe5fdcaf..c58693fe7f 100644 --- a/web/src/routes/admin/users/[id]/+layout.svelte +++ b/web/src/routes/admin/users/[id]/+layout.svelte @@ -17,7 +17,7 @@ Alert, Badge, Code, - CommandPaletteContext, + CommandPaletteDefaultProvider, Container, getByteUnitString, Heading, @@ -99,7 +99,7 @@ {onUserAdminDeleted} /> - + , K extends Component>( + component: K, + props: T, +) => { + return render(TestWrapper as Component<{ component: K; componentProps: T }>, { + component, + componentProps: props, + }) as unknown as RenderResult; +};