Use when creating or updating Vitest unit tests for the Reaparr frontend (Nuxt/Vue/Pinia/RxJS stores), especially for store setup, actions, getters, and RxJS observable flows that must follow the project's boilerplate, path alias, mock data, and naming conventions.
Use this skill to write frontend unit tests that match Reaparr conventions exactly.
Working directory: The frontend lives at src/AppHost/ClientApp/ from the repo root. All paths in this skill are relative to that directory (e.g. tests/nuxt/ means src/AppHost/ClientApp/tests/nuxt/).
Tests live under tests/nuxt/ and run in the nuxt Vitest environment with a global auth setup file that pre-mocks common API endpoints. Always use baseSetup, baseVars, getAxiosMock, and subscribeSpyTo from @services-test-base. Store methods return RxJS Observables — always use subscribeSpyTo to interact with them.
Use this skill when:
tests/nuxt/.axios-mock-adapter.Do not use this skill for backend C# tests (tests/UnitTests/) or Cypress E2E tests.
tests/nuxt/stores/<store-name>/<method-or-behavior>.test.ts
setup.test.ts tests only the setup flow. Additional behaviors get their own file (e.g., get-servers.test.ts, filter-media.test.ts).src/store/ (e.g., serverStore → server-store/).Every test file must follow this exact shape:
import { describe, beforeAll, beforeEach, test, expect } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import { baseSetup, baseVars, getAxiosMock, subscribeSpyTo } from '@services-test-base';
// Additional imports as needed:
// import { generateResultDTO, generatePlexServers } from '@mock';
// import { SomePaths } from '@api-urls';
// import { useXxxStore } from '@store';
// import { StoreNames, type ISetupResult } from '@interfaces';
describe('XxxStore.methodName()', () => {
let { mock, config } = baseVars();
beforeAll(() => {
baseSetup();
});
beforeEach(() => {
mock = getAxiosMock();
setActivePinia(createPinia());
});
test('Should <expected outcome> when <condition>', async () => {
// Arrange
// Act
// Assert
});
});
Rules:
baseVars() at describe scope. Reassign in beforeEach.baseSetup() in beforeAll.getAxiosMock() and setActivePinia(createPinia()) in beforeEach — fresh mock and fresh Pinia per test.// Arrange, // Act, // Assert comments.Store methods return RxJS Observable. Never subscribe manually. Use subscribeSpyTo from @services-test-base (re-exported from @hirez_io/observer-spy):
// Await until observable completes
const result = subscribeSpyTo(store.someAction());
await result.onComplete();
// Read emissions
result.getFirstValue() // first emitted value
result.getLastValue() // last emitted value
result.getValues() // all emitted values (array)
result.receivedComplete() // true if observable completed
Always await result.onComplete() before asserting unless you are testing intermediate emissions or synchronous observables.
Before testing any store behavior, always initialise the store:
await subscribeSpyTo(store.setup()).onComplete();
For a setup.test.ts that just verifies the store initialises correctly:
const store = useXxxStore();
const setupResult: ISetupResult = {
isSuccess: true,
name: StoreNames.XxxStore,
};
const result = subscribeSpyTo(store.setup());
await result.onComplete();
expect(result.getFirstValue()).toEqual(setupResult);
expect(result.receivedComplete()).toEqual(true);
Use mock (axios-mock-adapter) assigned from getAxiosMock() in beforeEach. Wrap response values with generateResultDTO.
// Single reply
mock.onGet(PlexServerPaths.getAllPlexServersEndpoint())
.reply(200, generateResultDTO(servers));
// One-time reply then a permanent reply (for setup + refresh flows)
mock.onGet(PlexServerPaths.getAllPlexServersEndpoint())
.replyOnce(200, generateResultDTO([]))
.onGet(PlexServerPaths.getAllPlexServersEndpoint())
.reply(200, generateResultDTO(servers));
// Regex URL match
mock.onGet(new RegExp('/api/PlexMedia')).reply(200, generateResultDTO(data));
generateResultDTO(value) returns { value, isSuccess: true, statusCode: 200, errors: [], successes: [] }.
These are registered globally — do not re-mock them unless you need different behaviour:
| Endpoint | Default response |
|---|---|
GET /api/Authentication/status | { isLoggedIn: true, userName: 'test-user', claims: [] } |
GET /api/BackgroundJobs | [] |
GET /api/PlexAccount | [] |
GET /api/Download | [] |
GET /api/FolderPath | [] |
GET /api/PlexLibrary | [] |
GET /api/PlexLibrary/sync-status | [] |
GET /api/Notification | [] |
GET /api/PlexServerConnection | [] |
GET /api/PlexServer | [] |
GET /api/Settings | full default SettingsModelDTO |
Any endpoint not in this list that your test triggers must be explicitly mocked. The mock adapter is configured with { onNoMatch: 'throwException' } — unmocked requests throw.
Use @mock (barrel re-exporting factories, helpers, and interfaces) for all test data:
import { generateResultDTO, generatePlexServers, generateSettingsModel, Seed } from '@mock';
import { generateJobStatusUpdate } from '@factories';
Use a deterministic config seed so tests never produce random data:
config = {
seed: 263,
plexServerCount: 3,
plexMovieLibraryCount: 2,
};
const seed = new Seed(config.seed!);
const plexServers = generatePlexServers({ config });
Available factory functions (non-exhaustive):
| Factory | Produces |
|---|---|
generatePlexServers({ config }) | PlexServerDTO[] |
generatePlexAccount({ id, plexServers, plexLibraries, config }) | PlexAccountDTO |
generatePlexLibrariesFromPlexServers({ seed, plexServers, config }) | PlexLibraryDTO[] |
generateSettingsModel({ config }) | SettingsModelDTO |
generateJobStatusUpdate({ jobType, jobStatus, data }) | JobStatusUpdateDTO |
generatePlexMediaSlims({ config, partialData }) | PlexMediaSlimDTO[] |
generateResultDTO(value) | ResultDTO<T> |
generateFailedResultDTO(partial?) | BaseResultDTO (failure) |
| Alias | Resolves to |
|---|---|
@services-test-base | tests/_base/base.ts |
@store | src/store/index.ts |
@mock | src/mock-data/index.ts |
@factories | src/mock-data/factories/index.ts |
@dto | src/types/api/generated/data-contracts.ts |
@api-urls | src/types/api/api-paths.ts |
@api/* | src/types/api/* |
@interfaces | src/types/interfaces/index.ts |
@class/* | src/types/class/* |
@const/* | src/types/const/* |
@enums/* | src/types/enums/* |
Prefer @api-urls over @api/api-paths — they resolve to the same file.
<method-or-behavior>.test.ts (kebab-case), e.g., get-servers.test.tsdescribe: 'StoreName.methodName()' or 'StoreName - Behavior Group'test: 'Should <verb phrase> when <condition>'Run all frontend unit tests:
bun --cwd src/AppHost/ClientApp test
Run a specific test file:
bun --cwd src/AppHost/ClientApp vitest run tests/nuxt/stores/server-store/get-servers.test.ts
import { describe, beforeAll, beforeEach, test, expect } from 'vitest';
import { createPinia, setActivePinia } from 'pinia';
import { baseSetup, baseVars, getAxiosMock, subscribeSpyTo } from '@services-test-base';
import { generatePlexServers, generateResultDTO } from '@mock';
import { PlexServerPaths } from '@api-urls';
import { useServerStore } from '@store';
describe('ServerStore.getServers()', () => {
let { mock, config } = baseVars();
beforeAll(() => {
baseSetup();
});
beforeEach(() => {
mock = getAxiosMock();
setActivePinia(createPinia());
});
test('Should return all servers when servers are set in the store', async () => {
// Arrange
config = { plexServerCount: 3 };
const serverStore = useServerStore();
const servers = generatePlexServers({ config });
mock.onGet(PlexServerPaths.getAllPlexServersEndpoint()).reply(200, generateResultDTO(servers));
// Act
await subscribeSpyTo(serverStore.setup()).onComplete();
const result = serverStore.getServers();
// Assert
expect(result).toEqual(servers);
});
});
await result.onComplete() before asserting — emissions may not have arrived yet.getAxiosMock() in beforeAll instead of beforeEach — mocks leak between tests.serverStore.setup() (or equivalent) before testing behavior that depends on loaded state.seed from config) — tests become flaky across runs.onNoMatch: throwException adapter will throw during setup(), not your action under test.tests/unit/ instead of tests/nuxt/ — only the nuxt project matches tests/nuxt/**.