* refactor: web tests * fix: e2e ratelimits * fix: prefer data-testid * chore: pr feedback
5.0 KiB
web/AGENTS.md
SvelteKit SPA (SSR off), Svelte 5 runes, Tailwind, @immich/ui, @tanstack/svelte-query. Talks to the server through the workspace @immich/sdk.
Rules
- Import with the
#lib/*alias and an explicit.tsextension:import { authManager } from '#lib/managers/auth-manager.svelte.ts'. Never$lib. Other aliases:#i18n/*,#test/*(thetest/directory). - Build URLs with the typed
Routeobject insrc/lib/route.ts.continue/previousRoutequery params must go throughRoute.continue(...)(lint-enforced). - Global state lives in
*.svelte.tssingleton managers insrc/lib/managers/.eventManageris the typed app-wide bus; subscribe witheventManager.on({...})orcomponents/OnEvents.svelte.src/lib/stores/is legacy. src/lib/services/*.service.tsonly return@immich/uiActionItems and open modals; data access belongs insrc/lib/queries/.- Unused variables must be named exactly
_.svelte-checkruns with--fail-on-warnings. @immich/sdkmust be built before the web builds or tests:mise //:sdk:install && mise //:sdk:build(mise //web:startdoes this).
Skills
-
Add a query or mutation: one
src/lib/queries/<controller>.query.tsper API controller. Export a query-key constant,getXQuery(...)factories returningqueryOptions({ queryKey, queryFn: ({ signal }) => sdkFn(..., { signal }), select }), andxMutationoptions built withasMutation(sdkFn, { meta: { success: { toast }, error: { toast } } }). Cache updates andeventManageremits happen in the mutation'sonSuccess. The globalMutationCacheinsrc/lib/queries/client.tsshows the toasts and callshandleError;meta.error.toast: falsesuppresses it. Outside components run mutations withexecuteMutation(optionsFn, variables, mutateOptions). -
Add a page: put it under
src/routes/(user)/for the authenticated app orsrc/routes/admin/. In+page.ts:await authenticate(url), preload withawait queryClient.query(getXQuery()),const $t = await getFormatter(), and return{ meta: { title: $t('...') } }. In the component read the data withcreatePreloadedQuery(() => getXQuery()). Route-specific components sit next to the route. -
Add a modal: create
src/lib/modals/XModal.sveltewithProps = { onClose: (result?) => void }built on@immich/uiModal/FormModal. Open it withconst result = await modalManager.show(XModal, props); the promise resolves with whatever is passed toonClose. -
Use a translation:
import { t } from 'svelte-i18n'and$t('key')in components;const $t = await getFormatter()from#lib/utils/i18n.tselsewhere. Keys are typed againsti18n/en.json, so add the key there first (root skill). Rich text goes throughsrc/lib/elements/FormatMessage.svelte. -
Unit tests: vitest runs every
src/**/*.spec.tsin headless Chromium (@vitest/browser-playwright, config in thetestblock ofvite.config.ts;mise //web:test). Pure-function and manager specs need nothing special. Rendered components userenderfromvitest-browser-svelte, locators fromvitest/browser(page.getByRole,userEvent) andawait expect.element(...); there is no testing-library. Specs that touch the API usetestfrom#test/unit/fixtures.tsand itsapifixture (api.on({ operationId: handler }),expect(api.operationId).toHaveBeenCalledWith(...)) instead of mocking@immich/sdk.test/unit/setup.tsfakes$app/navigationand$app/stateand sets thedevlocale, so$t('key')renders the key. Build data withfactoryfrom#test/unit/factories.ts. -
Page tests:
page.spec.tsnext to a route, usingtestfrom#test/unit/fixtures.ts; seesrc/routes/(user)/albums/page.spec.ts.router.goto(url, params)thenrender(Page, load);await settle()after an action that calls the API. -
Unit or medium? Both tiers share the mock API in
test/common/(MockData, handlers, spies). What differs:Unit / page (Chromium iframe, vite dev) Medium (Playwright, production build) API calls fetchstub in the pagePlaywright route interception <img>/<video>/ downloadsnever load (404); a failed test prints the URLs served by the mock Routing one route per test, gotoonly records the URLreal SvelteKit router, multi-page flows Layouts root and (user)layouts are not rendered; routes nested in other+layout.sveltefiles are mediumall Websocket, service worker none mocked -
Medium tests: Playwright specs in
test/medium/specs/run against the production build with every/apicall mocked (mise //web:test-medium). Fixtures:app(locators and gestures,test/medium/app.ts),data(seed state viaMockDatamethods),api(expect(api.updateAsset).toHaveBeenCalledWith({ params, body }); handlers live intest/common/handlers.ts) andlogger(browser errors). The UI uses thedevlocale, so locate elements by translation key. Unhandled requests, unasserted mutations and unasserted browser errors fail the test.