# Multiforum Development Guide > Use **Node.js 26** for development (`nvm use`). Node.js ≥ 24.13.0 remains > supported while Vercel deploys on Node 24; older majors break the tooling. ## Build & Run Commands - `pnpm run graphql:check` - Validate production GraphQL documents against the pinned schema with no outstanding errors - `pnpm run graphql:test` - Test GraphQL extraction, source locations, and baseline enforcement - `pnpm run schema:check` - Verify the pinned backend schema and report its source commit - `pnpm run schema:update /path/to/backend-artifact` - Update the schema pin; see [contract workflow](docs/graphql-contract-campaign.md) - `pnpm run compile` - Generate schema and operation types offline from the pinned contract - `pnpm run operations:check` - Verify typed-document freshness and production consumer contracts; see the [migration guide](docs/graphql-contract-campaign.md) - `pnpm run dev` - Start development server - `pnpm run build` - Build for production - `pnpm run test:playwright` - Run Playwright tests - `pnpm run build:playwright:mocked` - Build the mocked app once for fast parallel Playwright runs - `pnpm run test:playwright:mocked:build` - Run the mocked Playwright suite against the prebuilt app - `pnpm run test:unit` - Run unit tests with Vitest - `pnpm run test:playwright:mocked -- tests/playwright/mocked/discussions/createEditDeleteDiscussions.spec.ts` - Run a specific mocked Playwright test - `pnpm run tsc` - Run TypeScript type checking - `pnpm exec eslint --fix path/to/file.vue` - Fix linting issues ## Playwright Testing (E2E) - Use `installMockAuth()` to handle authentication for tests that need it - Most Playwright tests use mocked GraphQL responses and require no backend. - Cross-repository workflows that need to prove browser, API, and persistence behavior use the opt-in stateful suites under `tests/playwright/stateful`. Run the admin moderation suite with a backend checkout available via `PLAYWRIGHT_BACKEND_CWD`: start Neo4j with `pnpm run playwright:neo4j:up`, run `pnpm run test:playwright:stateful:admin`, then stop it with `pnpm run playwright:neo4j:down`. ### Playwright Test Optimizations - **Replace arbitrary timeouts with network waits**: ```javascript // Before: Fixed timeout that might be too short or too long await page.getByRole('button', { name: 'Save' }).click(); // After: Wait for the actual network request to complete await page.waitForResponse((response) => response.url().includes('/graphql')); ``` - **Validate network responses**: Add status code checks to ensure operations completed successfully ```javascript cy.wait('@graphqlRequest').its('response.statusCode').should('eq', 200); ``` - **Intercept specific GraphQL operations** by pattern-matching on the request body: ```javascript // Match specific GraphQL operations cy.intercept('POST', '**/graphql', (req) => { if (req.body.query.includes('createDiscussion')) { req.alias = 'createDiscussionRequest'; } }); // Later in the test cy.wait('@createDiscussionRequest'); ``` ## Vitest Testing (Unit) - Unit tests live in `tests/unit` or colocated next to the file under test (`Foo.vue` → `Foo.spec.ts`) - Run all unit tests with `pnpm run test:unit` - Run specific tests with `pnpm run test:unit -- --run tests/unit/path/to/test.spec.ts` - Use exactly one `expect(...)` assertion per `it(...)` test case - If you need multiple checks, split into multiple `it(...)` blocks or combine into a single structured `expect(...)` - Prefer `it.each(...)` tables when it helps keep tests DRY - Keep `vi.mock(...)` at the module top level — Vitest hoists it, so calling it inside `beforeEach`/`describe` is deprecated. If the factory references a local variable, prefix its name with `mock` (e.g. `mockThemeRef`) - Test thin page wrappers via `shallowMount` + `findComponent(Child).props(...)`; stub slot wrappers (`NuxtLayout`, `FormRow`) to render their slot, and `vi.stubGlobal('definePageMeta', vi.fn())` for pages that call it. See [CONTRIBUTING.md](./CONTRIBUTING.md#frontend-testing-patterns) ### Unit Test Best Practices - **Test Real Code, Not Mocks**: Ensure tests verify actual application code rather than reimplementing logic in test files - Extract component logic into utility files when appropriate for better testability - Import and test the actual functions from your codebase rather than creating test-only implementations - **Component Testing**: - Mount components to test actual validation logic, error messages, and UI state - Mock only external dependencies and services, not the core logic being tested - Verify that error/validation messages appear in the component when expected - **Meaningful Tests**: Tests should validate application behavior, not trivial language features - Avoid tests that only check basic JavaScript functionality - Focus on edge cases, expected validations, and user interactions - **Testable Code**: Refactor components to make logic more testable - Move complex formatting/validation logic to separate utility files - Use dependency injection to make components easier to test - **Test Structure**: Organize tests to mirror the structure of the code being tested - Group related tests with descriptive names - Test both success and failure paths #### Unit Test Gotchas Hard-won pitfalls (full detail + code in [CONTRIBUTING.md](./CONTRIBUTING.md#frontend-testing-patterns)): - **Mount the real component, never a hand-written stand-in.** A spec that defines `const FooTest = { template: '...' }` and mounts that adds **zero** coverage to `Foo.vue`. Mock the component's dependencies instead of reimplementing it. - **Mock Apollo at module level.** `mountWithDefaults` does not wire Apollo. Mock `@vue/apollo-composable`; to run a mutation's `onDone` handlers, make `mutate()` synchronously fire the registered `onDone` callbacks, and return an empty `useQuery` result. - **Seed hoisted `vi.fn` mocks with the full return shape.** `vi.fn(() => ({ valid: true }))` narrows the type, so a later `mockReturnValue({ valid: false, message })` fails CI's `vue-tsc` even when the test passes locally. Seed `{ valid: true, message: '' }`. - **`@/utils` and `@/utils/index` are the same module** — if you mock one, mock both with every needed export. - **Teleported UI** (`` tooltips/modals) renders into `document.body`, not the wrapper — query the document. - **Mocked GraphQL entities need `__typename`** or Apollo normalization hands the component only the key field. ## Pre-commit Workflow - The pre-commit hook is intentionally fast and runs only `pnpm exec lint-staged` - Full linting, type checking, unit tests, and Playwright checks run in CI - To skip pre-commit hooks temporarily: `git commit --no-verify` ## Code Style Guidelines - **TypeScript**: Use strict typing whenever possible, proper interfaces in `types/` directory - **Typed operations**: Import documents and exact selected result types from `@/__generated__/operations`; let Apollo infer variables/results. Prefer operation or fragment result subsets over full schema entities for migrated paths. See [the migration guide](docs/graphql-contract-campaign.md#phase-3-typed-operation-pilot). - **GraphQL results and variables**: Import documents and selected result types from `@/__generated__/operations`; let Apollo infer variables/results. Schema entities from `@/__generated__/graphql` describe the complete schema, not a query's selections. Use schema input types/enums for genuine inputs, and selected operation/fragment types for props and caches. Do not invent unselected fields to satisfy a schema entity. - **Avoid `any`**: `@typescript-eslint/no-explicit-any` is an **error** in application source (relaxed only in `*.spec.ts`). Only use `any` for genuinely untyped third-party interop, with a targeted `// eslint-disable-next-line` + reason — never for GraphQL data. See the `apollo-data` skill. - **Type Checking**: Use `pnpm run tsc` (which runs `vue-tsc --noEmit`) for proper Vue component type checking ### Common TypeScript Patterns and Fixes - **GraphQL fixtures and cache writes**: Use the exact generated operation or fragment selection, including selected nullable fields. Never fabricate connection fields merely to satisfy a full schema entity. Preserve Apollo normalization keys and test cache writes against the real selected document. - **Vue Router Type Safety**: Use proper route object structure for navigation ```typescript // Avoid: path property may not be compatible with router types routeAndClose({ path: '/account_settings' }); // Prefer: use name-based routing routeAndClose({ name: 'account_settings' }); ``` - **Error Type Handling**: GraphQL error objects may have inferred types as `never` in some contexts ```typescript // Cast to access message property when TypeScript can't infer the error type :text="(getCommentError as any)?.message || 'Error loading comment'" ``` - **Form Value Types**: Ensure form default values match the expected type interface completely ```typescript // If CreateEditChannelFormValues includes eventsEnabled and feedbackEnabled const defaultValues = { // ... other properties eventsEnabled: true, // Don't forget these! feedbackEnabled: true, }; ``` - **Nuxt Page Meta**: Use proper placement and TypeScript handling ```typescript // Place at top of script setup block with TypeScript ignore // @ts-ignore - definePageMeta is auto-imported by Nuxt definePageMeta({ middleware: 'some-middleware', }); ``` - **GraphQL Query Imports**: When refactoring queries, check that exports exist in the target file ```typescript // Before using GET_MOD_SUSPENSION, verify it exists in the queries file // If not available, find similar queries like GET_SUSPENDED_MODS_BY_CHANNEL ``` - **Optional Chaining vs Null Coalescing**: Use appropriate patterns for component props ```typescript // For optional props that might be undefined :old-version="activeRevision.oldVersionData || {}" :new-version="activeRevision.newVersionData || {}" ``` - **Function Parameters**: For functions with more than one parameter, use a typed object instead of positional arguments ```typescript // Avoid: function updateUser(id: string, name: string, email: string) { ... } // Prefer: type UpdateUserParams = { id: string; name: string; email: string; }; function updateUser(params: UpdateUserParams) { ... } ``` - **Vue Components**: Use script setup API with TypeScript and properly typed props/emits - **RequireAuth slot pattern**: For auth-gated controls that should look identical in both auth states, extract a shared presentational child component. Use a thin functional wrapper for the authenticated slot when the real behavior needs to be attached, and render the shared child directly in the unauthenticated slot unless a placebo wrapper is genuinely useful. - **Error Handling**: Use try/catch with specific error types, validate GraphQL responses - **Apollo state (useQuery & useMutation)**: Always leverage the hook's built-in refs — `useMutation`'s `loading` / `error` / `onDone` and `useQuery`'s `result` / `loading` / `error` / `onResult` — instead of recreating that state in hand-written `ref`s. Bind UI disabled/loading/error states directly to the hook's refs, and put side effects in `onDone` / `onResult` / `onError`. Do not hand-roll `mutate()`/`refetch()` in try/catch to mirror state the hook already exposes. See the `apollo-data` skill and [docs/code-review-checklist.md](./docs/code-review-checklist.md). - **Naming**: camelCase for variables/functions, PascalCase for components/interfaces - **Imports**: Group imports by type (Vue, libraries, local components, utils) - **Testing**: Each feature should have Playwright coverage, with shared seed data and cleanup helpers - **CSS**: Use Tailwind utility classes, dark mode compatible with `dark:` prefix - **Accessibility**: build to WCAG 2.2 AA. The `accessibility` skill carries the full contract, AA thresholds, and Vue patterns (invoke it for any UI/component work); `pnpm run lint:a11y` and the axe Playwright helper are the detection layers. Baseline: 4.5:1 contrast for normal text; accessible names (`