Write and run Vitest tests for the web app in src/frontend/web — colocated *.test.ts files, what is worth testing (pure helpers, route and tenant-scope rules, reducers, middleware, source-reading rules such as "nothing calls /tenants/switch", client/API mirrors, locale files) and how to structure cases. Use when changing frontend logic that has rules worth pinning down.
Install
npx skillscat add ma496/easy-for-net/frontend-tests Install via the SkillsCat registry.
Frontend tests
Vitest, run with npm run test (vitest run) from src/frontend/web. A single file:
npx vitest run lib/utils/tenant-routing.test.ts
npx vitest # watch mode while iteratingvitest.config.mts maps the @/ alias from tsconfig.json and collects **/*.test.ts only — a.test.tsx file is never run. There is no DOM environment and no setup file: the suite covers logic,
not rendered components. Keep it that way unless a task genuinely requires a browser, where addingjsdom, an environment and a .tsx include is a deliberate decision, not a side effect.
Vitest does not read tsconfig.json paths, and TypeScript erases an import used only as a type. So an
import of a value through @/ — import { setUserInfo } from '@/store/slices' — is resolved by the
config, while a type-only one needs nothing; a module whose own imports go through the alias loads in
a test only because of it.
Layout
Tests are colocated with the code they cover and named <file>.test.ts:
lib/utils/redirect.test.ts lib/utils/tenant-routing.test.ts
lib/utils/upload-limit.test.ts lib/utils/api-error-helpers.test.ts
lib/notifications/hub-reconnect.test.ts components/ui/data-table/card-layout.test.ts
store/slices/authSlice.test.ts store/middlewares/rtk-error-middleware.test.ts
store/tenant-cache.test.ts store/api/settings/settings/settings-mappers.test.ts
styles/tokens.test.ts i18n/locales.test.ts
i18n/tenant-screens.test.ts i18n/resolve-locale.test.ts
i18n/locale-guard.test.ts i18n/translate.test.ts
i18n/locale-routing-loop.test.ts
allow.test.ts auth-urls.test.ts feature-names.test.ts
app/[lang]/(auth)/select-tenant/_components/select-tenant-view.test.tsImport the unit under test by relative path (from './redirect'), everything else through the @/
alias.
Shape
import { describe, expect, it } from 'vitest'
import { isValidRedirectPath } from './redirect'
describe('isValidRedirectPath', () => {
it('accepts local application paths', () => {
expect(isValidRedirectPath('/users')).toBe(true)
})
it.each(['https://example.com', '//example.com', '/signin'])('rejects unsafe redirect %s', (path) => {
expect(isValidRedirectPath(path)).toBe(false)
})
})- One
describeper exported function (or unit), named after it. it('<behaviour in plain words>')— describe the rule, not the mechanics.it.each([...])for a family of inputs that share one expectation.- Build fixtures with a small local factory taking overrides, rather than repeating object literals.
The session shape carries tenant fields — leave none out:
const tenant = (id: string): GetUserInfoTenant => ({ id, name: `Tenant ${id}`, identifier: `t-${id}` })
const userInfo = (overrides: Partial<GetUserInfoResponse> = {}): GetUserInfoResponse => ({
id: 'user-1', username: 'someone', email: 'someone@example.com',
isPlatform: false, tenants: [], roles: [],
...overrides,
})
const stateWithPermissions = (...permissions: string[]): AuthState => ({
isAuthenticated: true, activeTenant: undefined, tenants: [],
user: userInfo({ roles: [{ id: 'role-id', name: 'Admin', permissions: permissions.map((name) => ({ id: name, name, displayName: name })) }] }),
})What to cover
Worth a test — anything with a rule that would be expensive to get wrong and is testable without a
browser:
- Security- and routing-shaped helpers: redirect validation,
isAllowed,getMatchedAuthUrl
(including the "two entries match the same path" error), the tenant-scope rules intenant-routing.ts(isPathAvailable,isWithoutUsableTenant, …) for a platform account in no
tenant, one inside a tenant, and an ordinary member. A new route that is not tenant-only
gets its case there. - Data shaping:
getApiErrorMessages, upload-limit arithmetic, export row mapping,shortName. - Reducers — call
slice.reducer(state, action)and assert on the state it returns. - Middleware — run an action through
middleware(api)(next)(action)with a recordingdispatch
and assert on what it dispatched, and on what it left alone. - Rules only the source can show:
tenant-cache.test.tsreads every web source file and fails
when one calls/tenants/switchor/tenants/exit, or when a tenant link is built from a
hard-coded domain rather thantenant.url. Make such a scan prove it read something (a minimum
file count, a file it must have seen), so a moved tree cannot pass by being absent. - Browser globals a helper touches: stub
windowwithvi.stubGlobalandvi.unstubAllGlobals()inafterEach. - Client/API mirrors:
allow.test.tsandfeature-names.test.tsrestate the backend constants and
fail whenallow.ts/feature-names.tsdrift — add the new constant there when you add a
permission or a feature. - Locales:
i18n/locales.test.tsholdsi18nConfig.localesequal to the backend's resource files;i18n/tenant-screens.test.tsreads those files — extend its key lists when a screen's strings must
never go missing (seelocalization). Key parity across locales is the backend's test, not this one. - Wiring that cannot be rendered here (a guard calling a helper, a screen using logical
utilities) may be checked by reading the source withreadFileSync. Such a test must first assert
that it found the files it scans, so a moved file cannot make it pass by being absent.
Not worth it here — component rendering, styling, and the request/response shape of RTK Query
endpoints. Endpoint behaviour is covered on the API side instead; see the backend-tests skill.
Rules
- Tests must be deterministic: no network, no real timers, no reliance on
localStorageunless the
test sets it up; helpers that guard withtypeof window === 'undefined'are testable as-is. - Assert on behaviour, not implementation details — call the exported function, check the result.
- When you fix a bug in a helper, add the failing input as a case first.
npm run test,npm run lintandnpx tsc --noEmitall need to pass before the change is done —
Vitest and ESLint both pass on code that fails the type check.