--- name: unit-testing description: >- Guides writing and maintaining Vitest unit tests for the bootc extension's backend, frontend, and shared packages. Covers mocking @podman-desktop/api, RPC clients, Svelte stores, and Node.js modules. Triggers when creating or modifying .spec.ts files, fixing Vitest failures, or adding test coverage. paths: - packages/backend/src/**/*.spec.ts - packages/frontend/src/**/*.spec.ts - packages/shared/src/**/*.spec.ts argument-hint: '[test-file-or-pattern]' --- # Unit Testing for Bootc Extension ## Framework - **Vitest 4** with v8 coverage provider - **Backend**: Node.js environment, mocked `@podman-desktop/api` - **Frontend**: JSDOM environment, `@testing-library/svelte` for components - **Shared**: Node.js environment for message proxy and model tests ## Running Tests ```bash pnpm test # all packages pnpm test:backend # packages/backend with coverage pnpm test:frontend # packages/frontend with coverage pnpm test:shared # packages/shared with coverage # Single file npx vitest run packages/backend/src/build-disk-image.spec.ts # Watch mode npx vitest watch packages/backend/src/ ``` ## Backend Test Patterns ### Mocking @podman-desktop/api The backend's `vite.config.js` aliases `@podman-desktop/api` to a mock file. Mock only the namespaces your test actually uses: ```typescript import { vi, test, expect, beforeEach, describe } from 'vitest'; import * as extensionApi from '@podman-desktop/api'; vi.mock('@podman-desktop/api', async () => ({ // Mock only what your test needs — check existing tests for patterns: // grep -l "vi.mock('@podman-desktop/api'" packages/backend/src/*.spec.ts })); ``` To see the full mock shape used in existing tests: ```bash grep -A 30 "vi.mock('@podman-desktop/api'" packages/backend/src/*.spec.ts | head -60 ``` ### Testing Backend Functions ```typescript import { describe, test, expect, vi, beforeEach } from 'vitest'; import { buildDiskImage } from './build-disk-image'; vi.mock('@podman-desktop/api'); vi.mock('./container-utils'); vi.mock('./history'); beforeEach(() => { vi.clearAllMocks(); }); describe('buildDiskImage', () => { test('should create build container with correct volumes', async () => { // Arrange const buildInfo = { id: 'test-id', image: 'quay.io/test/image', tag: 'latest', type: ['qcow2'] as BuildType[], folder: '/output', // ... }; // Act await buildDiskImage(buildInfo); // Assert expect(containerEngine.createContainer).toHaveBeenCalledWith( expect.anything(), expect.objectContaining({ Image: expect.stringContaining('bootc-image-builder'), }), ); }); }); ``` ### Mocking Node.js Modules ```typescript import { vi } from 'vitest'; vi.mock('node:fs', async () => ({ promises: { mkdir: vi.fn(), readFile: vi.fn(), writeFile: vi.fn(), }, existsSync: vi.fn(), })); vi.mock('node:child_process', async () => ({ exec: vi.fn(), })); ``` ## Frontend Test Patterns ### Component Testing with @testing-library/svelte ```typescript import { vi, test, expect, beforeEach } from 'vitest'; import { render, screen, fireEvent } from '@testing-library/svelte'; import '@testing-library/jest-dom/vitest'; import MyComponent from './MyComponent.svelte'; // Mock the RPC client vi.mock('/@/api/client', async () => ({ bootcClient: { listBootcImages: vi.fn().mockResolvedValue([]), buildImage: vi.fn(), }, rpcBrowser: { subscribe: vi.fn().mockReturnValue({ unsubscribe: vi.fn() }), }, })); test('renders heading', async () => { render(MyComponent, { props: { imageName: 'test' } }); expect(screen.getByText('Build Disk Image')).toBeInTheDocument(); }); test('clicking build button triggers build', async () => { render(MyComponent); const buildButton = screen.getByRole('button', { name: 'Build' }); await fireEvent.click(buildButton); expect(bootcClient.buildImage).toHaveBeenCalled(); }); ``` ### Mocking Svelte Stores ```typescript import { vi } from 'vitest'; import { readable } from 'svelte/store'; vi.mock('/@/stores/imageInfo', async () => ({ imageInfos: readable([{ Id: 'abc123', RepoTags: ['quay.io/test:latest'], Labels: { 'containers.bootc': '' } }]), })); ``` ### Path Aliases in Tests Frontend tests resolve `/@/` to `packages/frontend/src/` and `/@shared/` to `packages/shared/` via Vite config. Use these aliases in mocks: ```typescript vi.mock('/@/api/client'); // → packages/frontend/src/api/client vi.mock('/@shared/src/models/bootc'); // → packages/shared/src/models/bootc ``` ## Shared Package Tests ### MessageProxy Tests ```typescript import { test, expect, vi } from 'vitest'; import { RpcBrowser, RpcExtension } from './MessageProxy'; test('RPC call resolves with result', async () => { // Test the RPC proxy mechanism }); ``` ## Test File Naming - Test files: `*.spec.ts` colocated with source files - Example: `build-disk-image.ts` → `build-disk-image.spec.ts` ## Coverage Coverage reports are generated in `lcov` and `text` format: ```bash pnpm test:backend # generates packages/backend/coverage/ pnpm test:frontend # generates packages/frontend/coverage/ ``` ## Common Issues ### "Cannot find module @podman-desktop/api" The mock alias is configured in `packages/backend/vite.config.js`. If you see this error, ensure the test file is being run from the correct package. ### "Not a Svelte component" The test environment needs the Svelte vite plugin. Frontend tests use the config from `packages/frontend/vite.config.js` which includes `@sveltejs/vite-plugin-svelte`. ### Test Retries Frontend tests retry failed tests 2x (configured in `packages/frontend/vite.config.js`). Backend tests do not retry.