Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when writing, modifying, or debugging unit tests (*.test.tsx) in the orchestration cluster webapp (webapp/client/apps/orchestration-cluster-webapp/src/). Covers Vitest browser mode, MSW mocking, and vitest-browser-react rendering.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 97% | 0% |
| case-01 | ✗→✓ | ▲ Improved | 116% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 44% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 84% | 0% |
Unit tests in @camunda/orchestration-cluster-webapp run in a real Chromium browser via Vitest Browser Mode — not jsdom or happy-dom. This matters because components render in an actual DOM with real layout, events, and browser APIs. MSW intercepts all HTTP at the service worker level.
This is fundamentally different from the @testing-library/react + vi.mock() approach used in the legacy Operate and Tasklist frontends. If you're familiar with those patterns, read the key rules carefully — several habits from the legacy apps will produce broken or flaky tests here.
it from #/vitest-modules/test-extend, not from vitest. The custom fixture auto-starts an MSW service worker before each test and resets/stops it after. Importing from vitest directly means no MSW interception — HTTP calls will fail or hit real endpoints.render() from vitest-browser-react, not from @testing-library/react. The vitest-browser-react renderer is designed for browser-mode Vitest and handles the real DOM lifecycle correctly. Do NOT import screen from @testing-library/react — it does not exist in this setup.<Link>, useNavigate, route hooks), use renderWithRouter(Component, {path, initialEntry?}) from #/vitest-modules/render-with-router instead of bare render(). It mounts the component in a minimal isolated route tree backed by an in-memory history — no full routeTree.gen is loaded, no beforeLoad runs.render() and renderWithRouter() return the screen object — you MUST capture the return value: const screen = await renderWithRouter(MyPage, {path: '/my-path'}). There is no global screen import.expect.element() for DOM assertions — it retries automatically until the assertion passes or times out. This replaces the waitFor / findBy* / screen.findByRole patterns you may know from Testing Library. There is no waitFor here.userEvent imported from vitest/browser — e.g. await userEvent.click(screen.getByRole('button', {name: /save/i})), await userEvent.fill(screen.getByLabelText('Name'), 'value'). It dispatches a realistic full browser event sequence (pointer, mouse, focus, input). Keep using screen.getBy* for queries and expect.element() for assertions — userEvent is only for interactions. Do NOT use locator .click() or .fill() methods directly.worker fixture using endpoint mocks from #/shared-test-modules/mock-handlers. Each mock is an individually named export (e.g., mockCurrentUserEndpoint, mockLoginEndpoint) created with createEndpointMock from #/shared-test-modules/mock-endpoint. Both unit and Playwright tests use the same definitions. All endpoint mocks must be defined in apps/orchestration-cluster-webapp/shared-test-modules/mock-handlers.ts — never create createEndpointMock calls inline in test files. Never use vi.mock() for API calls; it couples tests to implementation details and breaks on refactors.getByRole, getByLabelText, getByText. They enforce accessible markup and survive structural refactors. Avoid querySelector and getByTestId — they test DOM structure, not behavior.src/shared/foo/bar.tsx sits at src/shared/foo/bar.test.tsx; a test for src/operate/components/Foo.tsx sits next to it at src/operate/components/Foo.test.tsx. Pod areas follow their own conventions for test placement.should (e.g., it('should display an error on invalid credentials')).renderWithRouter(Component, {path}) from #/vitest-modules/render-with-router when the component needs routing context. It mounts the component in a fresh, isolated TanStack Router backed by an in-memory history — the component receives real route params, search params, and navigation. No full application route tree or global providers are loaded, which keeps tests fast and self-contained. Typed file-route hooks (Route.useParams()) will not resolve under the isolated router; use useParams({from: '/your-path'}) instead.vi.mock() in general. Prefer MSW and real implementations. Vitest mocks couple tests to internals and break on refactors. Reach for them only when there is no practical alternative, such as faking time with vi.useFakeTimers.// given / when / then comments — that is a Java backend convention. Structure tests by visual grouping (blank lines between setup, action, and assertion).Endpoint mocks live in #/shared-test-modules/endpoints as a shared dictionary. Each entry is created with createEndpointMock from #/shared-test-modules/mock-endpoint, which builds a typed MSW handler factory for a given endpoint + HTTP method.
Two shapes:
schema, successResponse, and failureResponse. The handler validates the request body against the schema and returns the failure response if it doesn't match.successResponse. The handler always returns the success response.The worker fixture is injected by the custom it and auto-resets between tests — no manual worker.resetHandlers() needed.
tsximport {render} from 'vitest-browser-react'; import {it} from '#/vitest-modules/test-extend'; import {mockUsersEndpoint} from '#/shared-test-modules/mock-handlers'; import {describe, expect} from 'vitest'; import {HttpResponse} from 'msw'; import {UserList} from './UserList'; describe('<UserList />', () => { it('should render users from the API', async ({worker}) => { worker.use( mockUsersEndpoint({ successResponse: HttpResponse.json([ {name: 'Alice'}, {name: 'Bob'}, ]), }), ); const screen = await render(<UserList />); await expect.element(screen.getByRole('cell', {name: 'Alice'})).toBeVisible(); await expect.element(screen.getByRole('cell', {name: 'Bob'})).toBeVisible(); }); });
Use renderWithRouter(Component, {path, initialEntry?}) — it mounts the component in a minimal isolated route tree with a fresh QueryClient and memory history. No beforeLoad, no global providers, no full route tree. Pass initialEntry when the path contains params (e.g. path: '/users/$id', initialEntry: '/users/42').
tsximport {it} from '#/vitest-modules/test-extend'; import {renderWithRouter} from '#/vitest-modules/render-with-router'; import {describe, expect} from 'vitest'; import {userEvent} from 'vitest/browser'; import {LoginPage} from './LoginPage'; describe('<Login />', () => { it('should not allow the form to be submitted with empty fields', async () => { const screen = await renderWithRouter(LoginPage, {path: '/login'}); await userEvent.click(screen.getByRole('button', {name: /login/i})); await expect.element(screen.getByLabelText(/username/i)).toBeInvalid(); await expect.element(screen.getByLabelText(/^password$/i)).toBeInvalid(); }); });
ts// Visibility await expect.element(screen.getByRole('button', {name: /submit/i})).toBeVisible(); // Text content await expect.element(screen.getByRole('heading')).toHaveTextContent('Dashboard'); // Attributes await expect.element(screen.getByRole('link', {name: 'Docs'})).toHaveAttribute('href', '/docs'); // Absence — element should not be in the document await expect.element(screen.getByText('Loading...')).not.toBeVisible();
expect.element() is always async and retries — no need to wrap in waitFor or use findBy*.
All interactions use userEvent from vitest/browser. It accepts both Element and Locator (the return type of screen.getBy*).
tsimport {userEvent} from 'vitest/browser'; // Click await userEvent.click(screen.getByRole('button', {name: /submit/i})); // Fill an input (clears existing value first) await userEvent.fill(screen.getByLabelText('Name'), 'Alice'); // Type without clearing (appends to existing value) await userEvent.type(screen.getByLabelText('Name'), ' Bob'); // Clear an input await userEvent.clear(screen.getByLabelText('Name')); // Select options in a <select> await userEvent.selectOptions(screen.getByRole('combobox'), ['option-value']); // Keyboard await userEvent.keyboard('{Enter}'); // Tab await userEvent.tab(); // Hover / unhover await userEvent.hover(screen.getByText('Tooltip trigger')); await userEvent.unhover(screen.getByText('Tooltip trigger'));
waitFor or findBy*: expect.element() handles async natively. Writing await waitFor(() => ...) will error — it doesn't exist in this setup.queryByText / queryByRole: these don't exist. Use getByText / getByRole with expect.element(...).not.toBeVisible() for absence checks.userEvent is not a fixture — import it directly: import {userEvent} from 'vitest/browser';. Use userEvent.click(), userEvent.fill(), userEvent.type(), etc. for all interactions. Do not use locator .click() or .fill() methods — they bypass the realistic event sequence that userEvent provides.mockCurrentUserEndpoint({successResponse: HttpResponse.json({})}), not mockCurrentUserEndpoint bare.document.querySelector technically works but defeats the purpose. Use screen.getBy* queries.msw/browser, not msw/node: the MSW worker runs in the browser via setupWorker. If you see imports from msw/node, that's wrong.vi.mock() for HTTP: it silently breaks in browser mode and is the wrong abstraction anyway. Use MSW.render() returns screen: unlike Testing Library where screen is a global import, here render() returns the screen object. Use const screen = await render(<Comp />). Same for renderWithRouter() — const screen = await renderWithRouter(MyPage, {path: '/my-path'}).Run from webapp/client/apps/orchestration-cluster-webapp/:
bashnpm run test:unit # Headless Chromium — CI and local npm run test:unit:ui # Visible browser — useful for debugging
Format changed files via npm run prettier:format from webapp/client/ and typecheck via npm run typecheck from the app directory — never invoke Prettier or tsc directly.
src/shared/pages/LoginPage.test.tsx — page-level test using renderWithRouter.src/shared/mock-test.test.tsx — component test with MSW mocking.src/vitest-modules/test-extend.ts — custom it fixture source.src/vitest-modules/render-with-router.tsx — renderWithRouter utility source.shared-test-modules/mock-endpoint.ts — createEndpointMock factory source.shared-test-modules/mock-handlers.ts — shared endpoint mock definitions.docs/monorepo-docs/frontend/testing.md — full testing guide.Other measured skills in the registry, with their headline benchmark lift.