Frontend Integration, Visual, and Accessibility Testing

SkillWeb & browsing

This skill guides your AI to write and fix browser tests for the Camunda orchestration cluster web app. Once added, it can create and debug integration, visual regression, and accessibility tests, using mocked network responses and reusable page objects to keep tests reliable. Tests belong in the webapp's test directory.

Available today. Use it from your connected AI after setup.

Ask your AI to write or fix a test in webapp/client/apps/orchestration-cluster-webapp/test/. It will follow the skill's patterns for Playwright tests, MSW mocks, and Page Object Models.

Then ask your AI: use the Frontend Integration, Visual, and Accessibility Testing skill

What your AI can do with it

  • Write Playwright integration tests for the orchestration cluster web app
  • Fix and debug existing tests
  • Set up visual regression checks that catch unwanted interface changes
  • Run accessibility checks with axe-core
  • Mock network responses with MSW so tests run without a live backend
  • Organize tests with Page Object Models

What this skill tells your AI

The instructions your AI receives, as published by camunda/camunda in .claude/skills/frontend-integration-test/SKILL.md and read by ahel’s review.

Playwright tests in @camunda/orchestration-cluster-webapp cover three categories — integration, visual regression, and accessibility — across 7 Playwright projects. Tests run against the built app served by vite preview on port 3003. MSW intercepts HTTP at the network level via @msw/playwright, so no real backend is needed.

Test granularity

Each test() spins up a fresh browser context and every goto() reboots the full SPA bundle plus MSW interception. Page loads are the dominant Playwright cost — structure tests to minimize them.

Scope each test to one feature (observable behavior), not to one assertion and not to an entire page.

A feature test should:

  • Navigate (or set up) once
  • Make all assertions that verify that single behavior — visibility, content, state, attributes — in sequence
  • End when the feature has been fully verified

Do not split a feature into multiple tests just to isolate individual assertions. A test named "should show the info sidebar links" should assert every expected link in one go, not spawn four separate tests that each check one link.

Do not collapse an entire page into a single test. Each distinct behavior — navigation, error states, conditional/role-gated UI, logout, i18n — is its own feature and belongs in its own test.

Decision rule: put assertions in the same test when they verify the same behavior under the same mock setup and navigation. Split into a new test when the behavior, mock setup, or user flow changes.

Use await test.step('description', async () => { ... }) for readability inside a feature test instead of artificially splitting it.

// WRONG — one assertion per test creates redundant page loads
test('should show Documentation link', async ({tasklistIndexPage}) => {
  await tasklistIndexPage.goto();
  await tasklistIndexPage.header.openInfoSidebar();
  await expect(tasklistIndexPage.header.documentationLink).toBeVisible();
});

test('should show Camunda Academy link', async ({tasklistIndexPage}) => {
  await tasklistIndexPage.goto();
  await tasklistIndexPage.header.openInfoSidebar();
  await expect(tasklistIndexPage.header.camundaAcademyLink).toBeVisible();
});

test('should show Community Forum link', async ({tasklistIndexPage}) => {
  await tasklistIndexPage.goto();
  await tasklistIndexPage.header.openInfoSidebar();
  await expect(tasklistIndexPage.header.communityForumLink).toBeVisible();
});

// CORRECT — one feature test, one page load, all assertions for that behavior
test('should show expected links in the info sidebar', async ({tasklistIndexPage}) => {
  await tasklistIndexPage.goto();
  await tasklistIndexPage.header.openInfoSidebar();

  await expect(tasklistIndexPage.header.documentationLink).toBeVisible();
  await expect(tasklistIndexPage.header.camundaAcademyLink).toBeVisible();
  await expect(tasklistIndexPage.header.communityForumLink).toBeVisible();
  await expect(tasklistIndexPage.header.feedbackAndSupportLink).not.toBeVisible();
});

// Paid-plan link is a SEPARATE test — it requires different mock setup (different behavior)
test('should show the Feedback and Support link for paid plan users', async ({network, tasklistIndexPage}) => {
  network.use(
    mockCurrentUserEndpoint({successResponse: HttpResponse.json(createCurrentUser({salesPlanType: 'paid-cc'}))}),
  );

  await tasklistIndexPage.goto();
  await tasklistIndexPage.header.openInfoSidebar();

  await expect(tasklistIndexPage.header.feedbackAndSupportLink).toBeVisible();
});

Key rules

  • Import test and expect from #/pw-modules/test-extend, not from @playwright/test. The custom test fixture auto-starts MSW network interception per test and provides the makeAxeBuilder fixture for accessibility checks.
  • Use endpoint mocks from #/shared-test-modules/mock-handlers with network.use(). Each mock is an individually named export (e.g., mockCurrentUserEndpoint, mockLoginEndpoint) created with createEndpointMock — 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.
  • Prefer testing library selectors: page.getByRole(), page.getByLabel(), page.getByText(). Playwright includes these out of the box. They enforce accessible markup and survive structural changes. Avoid page.locator('.css-class') and page.getByTestId().
  • Place tests in the correct category directory: test/integration/ for MSW-mocked user flows, test/visual/ for screenshot comparisons, test/a11y/ for accessibility checks.
  • Use Page Object Model for page interactions — one class per page under test/pages/. Encapsulate navigation, locators, and composite actions (e.g., fillCredentials(username, password)) so tests read as user stories, not DOM queries. Page objects are registered as Playwright fixtures in test/pw-modules/test-extend.ts and destructured from the test parameters — NEVER import page object classes in test files. Test files must not contain any import {LoginPage} or import {SomePage} statements.
// WRONG — never do this in a test file
import {LoginPage} from '../pages/Login.page';
const loginPage = new LoginPage(page);

// CORRECT — destructure from test parameters
test('should ...', async ({loginPage, network, page}) => {
  await loginPage.goto();
});
  • Visual tests require a containerized browser for consistent cross-machine rendering (CONTAINERIZED_BROWSER=true runs the official mcr.microsoft.com/playwright Docker image).
  • Accessibility tests check both light and dark themes automatically — the Playwright config runs a11y tests through a11y-light and a11y-dark projects.

Test categories

Integration tests (test/integration/)

Test user flows across pages and components: navigation, data loading, error states, multi-step interactions. Mock the backend with MSW via the network fixture. Use page objects from fixtures — never import and instantiate them manually.

import {test, expect} from '#/pw-modules/test-extend';
import {mockCurrentUserEndpoint, mockLoginEndpoint} from '#/shared-test-modules/mock-handlers';
import {HttpResponse} from 'msw';

test('should redirect to the initial page on success', async ({network, page, loginPage}) => {
  network.use(
    mockCurrentUserEndpoint({successResponse: new HttpResponse(null, {status: 401})}),
    mockLoginEndpoint({successResponse: new HttpResponse(null, {status: 200})}),
  );

  await loginPage.goto();

  network.use(mockCurrentUserEndpoint({successResponse: HttpResponse.json({})}));

  await loginPage.fillCredentials('demo', 'demo');
  await loginPage.submitButton.click();

  await expect(page).toHaveURL('/');
});

Visual regression tests (test/visual/)

Screenshot comparison via expect(page).toHaveScreenshot(). Four projects cover light/dark themes and desktop/tablet viewports. Always use the containerized browser for deterministic rendering.

import {test, expect} from '#/pw-modules/test-extend';

test('should match snapshot', async ({page}) => {
  await page.goto('/some-page');
  await expect(page).toHaveScreenshot('some-page.png', {fullPage: true});
});

Accessibility tests (test/a11y/)

Playwright + @axe-core/playwright. Use the makeAxeBuilder fixture and assert zero violations. The Playwright config runs every a11y test in both light and dark themes automatically.

import {test, expect} from '#/pw-modules/test-extend';

test('should have no a11y violations', async ({makeAxeBuilder, page}) => {
  await page.goto('/some-page');
  const results = await makeAxeBuilder().analyze();
  expect(results.violations).toEqual([]);
});

Fixtures

The custom test from #/pw-modules/test-extend provides these fixtures:

FixtureAutoDescription
networkyesMSW interception via @msw/playwright. Starts before each test, stops after. Use network.use() to add handlers.
handlersno (option)Pre-configure MSW handlers at suite level via test.use({handlers: [...]}). Useful when every test in a file shares the same mock setup.
makeAxeBuildernoCreates an AxeBuilder instance scoped to the current page. Call makeAxeBuilder().analyze() to run the audit.
loginPagenoPage object for the login page. Every page object follows this pattern — registered as a fixture, destructured in tests.

The network fixture errors on unhandled requests (except HTML page navigations), so tests fail fast if they hit an un-mocked endpoint. This is intentional — it catches missing mocks early.

Endpoint mocks are functions — always call them with a config object containing successResponse:

// Correct
mockCurrentUserEndpoint({successResponse: HttpResponse.json({})})
mockLoginEndpoint({successResponse: new HttpResponse(null, {status: 200})})

// Wrong — bare reference, dot-chained, or wrong key names
mockCurrentUserEndpoint
mockLoginEndpoint.success()
mockLoginEndpoint({serverResponse: ...})  // wrong key

network.use() is synchronous and takes handlers as spread arguments, not an array:

// Correct
network.use(
  mockCurrentUserEndpoint({successResponse: HttpResponse.json({})}),
  mockLoginEndpoint({successResponse: new HttpResponse(null, {status: 200})}),
);

// Wrong
await network.use([...]);

Do not use // given / when / then comments in frontend tests — that is a Java backend convention. Structure tests by visual grouping instead.

Page Object Model

Encapsulate page interactions in classes under test/pages/. Every page object must have a goto() method for navigation. Use getter-based locators and composite actions (multi-step user operations like filling a form). Tests navigate via loginPage.goto(), never via page.goto('/login') directly.

// test/pages/Login.page.ts
import {type Page} from '@playwright/test';

class LoginPage {
  private page: Page;

  constructor(page: Page) {
    this.page = page;
  }

  async goto() {
    await this.page.goto('/login');
  }

  get usernameInput() {
    return this.page.getByLabel(/username/i);
  }

  get passwordInput() {
    return this.page.getByLabel(/^password$/i);
  }

  get submitButton() {
    return this.page.getByRole('button', {name: /login/i});
  }

  get errorMessage() {
    return this.page.getByRole('alert').filter({hasText: /.+/});
  }

  async fillCredentials(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
  }
}

export {LoginPage};

Registering page objects as fixtures

Page objects are never imported directly in test files. Register them as Playwright fixtures in test/pw-modules/test-extend.ts:

// test/pw-modules/test-extend.ts (excerpt)
import {LoginPage} from '#/pages/Login.page';

type Fixtures = {
  // ...existing fixtures...
  loginPage: LoginPage;
};

const test = base.extend<Fixtures>({
  // ...existing fixtures...
  loginPage: async ({page}, use) => {
    await use(new LoginPage(page));
  },
});

Usage in tests — destructure from the test parameters:

import {test, expect} from '#/pw-modules/test-extend';
import {mockCurrentUserEndpoint, mockLoginEndpoint} from '#/shared-test-modules/mock-handlers';
import {HttpResponse} from 'msw';

test('should show an error for wrong credentials', async ({network, loginPage}) => {
  network.use(
    mockCurrentUserEndpoint({successResponse: new HttpResponse(null, {status: 401})}),
    mockLoginEndpoint({successResponse: new HttpResponse(null, {status: 401})}),
  );

  await loginPage.goto();
  await loginPage.fillCredentials('demo', 'wrong-password');
  await loginPage.submitButton.click();

  await expect(loginPage.errorMessage).toContainText(/username and password do not match/i);
});

Playwright config overview

The config at playwright.config.ts defines 7 projects:

ProjectCategoryThemeViewport
visual-lightvisuallightdesktop
visual-darkvisualdarkdesktop
visual-light-tabletvisuallighttablet
visual-dark-tabletvisualdarktablet
a11y-lighta11ylightdesktop
a11y-darka11ydarkdesktop
integrationintegrationdesktop

Tests match by directory: visual/**/*.test.ts, a11y/**/*.test.ts, integration/**/*.test.ts. The app is served via npx vite preview on port 3003 (build must exist first). Retries are 2x on CI, traces and screenshots are captured on failure.

Commands

Run from webapp/client/apps/orchestration-cluster-webapp/:

npm run test:integration    # Integration tests (MSW-mocked flows)
npm run test:visual         # Visual regression (requires Docker for containerized browser)
npm run test:a11y           # Accessibility (light + dark themes)

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.

Template references

  • test/integration/about.test.ts — integration test with MSW.
  • test/visual/login.test.ts — visual regression test.
  • test/a11y/about.test.ts — accessibility test.
  • test/pages/Login.page.ts — Page Object Model.
  • test/pw-modules/test-extend.ts — custom test fixture source.
  • shared-test-modules/mock-endpoint.tscreateEndpointMock factory source.
  • shared-test-modules/mock-handlers.ts — shared endpoint mock definitions.
  • docs/monorepo-docs/frontend/testing.md — full testing guide.

Signals

GitHub stars
4k
Forks
818
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
frontend-integration-test
Source
github.com/camunda/camunda