Skip to content

feat: add Playwright implementation of e2e tests#32472

Open
teredasites wants to merge 2 commits intojhipster:mainfrom
teredasites:feat/playwright-e2e
Open

feat: add Playwright implementation of e2e tests#32472
teredasites wants to merge 2 commits intojhipster:mainfrom
teredasites:feat/playwright-e2e

Conversation

@teredasites
Copy link

/claim #13755

Summary

Adds a new playwright generator as an alternative to Cypress for end-to-end testing, addressing the long-standing request in #13755.

This is a complete port of the existing Cypress e2e test infrastructure to Playwright, following the same generator architecture and template patterns used throughout JHipster.

What's included

Generator infrastructure

  • generators/playwright/generator.ts — Main generator class extending BaseApplicationGenerator, implementing preparing, writing, writingEntities, and postWriting lifecycle phases
  • generators/playwright/files.ts — File section definitions mapping templates to output paths using playwrightDir context variable
  • generators/playwright/command.ts — Command definition (no special CLI options needed)
  • generators/playwright/types.d.ts — TypeScript types (playwrightDir, playwrightTemporaryDir, playwrightBootstrapEntities, generateEntityPlaywright)
  • generators/playwright/index.ts — Module exports
  • generators/playwright/generator.spec.ts — Generator test spec with matrix coverage across all auth types, client frameworks, and admin UI configurations

Framework integration

  • lib/jhipster/test-framework-types.ts — Added PLAYWRIGHT to the test framework enum
  • lib/jhipster/application-options.ts — Registered Playwright in the TEST_FRAMEWORKS option values
  • generators/client/generator.ts — Added composition with the playwright generator when selected
  • generators/client/command.ts — Added Playwright as a choice in the clientTestFrameworks prompt

Support utilities (templates)

  • commands.ts.ejs — Selectors, credentials helper, login functions (JWT/Session/OAuth2), navbar helpers. All functions take page: Page as the first parameter instead of relying on Cypress's global cy object.
  • entity.ts.ejs — Entity selectors and helpers (getEntityHeading, setFieldImageAsBytesOfEntity, setFieldSelectToLastOfEntity)
  • management.ts.ejsgetManagementInfo() using Playwright's request API
  • account.ts.ejsgetAccount() and saveAccount() using API request context
  • login.ts.ejs — Re-exports from commands
  • oauth2.ts.ejs — OAuth2 login flows for Keycloak, Auth0, and Okta

Test templates

  • Account tests: login, logout, register, settings, password change, password reset
  • Administration tests: user management, metrics, health, logs, configuration, swagger docs (with iframe handling via page.frameLocator()), websocket tracker
  • Entity CRUD tests: Full create/read/update/delete coverage with relationship support, pagination handling, and field type awareness
  • Playwright config: Chromium project, 1200x720 viewport, server port-aware baseURL

Key Playwright API translations from Cypress

Cypress Playwright
cy.get(selector) page.locator(selector)
cy.intercept().as() + cy.wait('@alias') page.waitForResponse(predicate)
cy.intercept({...}, response) page.route(url, route => route.fulfill({...}))
cy.session() context.addInitScript() for JWT token injection
Cypress.Commands.add() Exported async functions with page: Page parameter
.should('be.visible') expect(locator).toBeVisible()
.should('have.class', ...) expect(locator).toHaveClass(new RegExp(...))

Test plan

  • Generator spec passes with all matrix combinations (auth types × client frameworks × adminUI)
  • Generated Playwright tests compile without TypeScript errors
  • Generated tests run successfully against a JHipster monolith (JWT + Angular)
  • Generated tests run successfully against a JHipster monolith (Session + React)
  • Generated tests run successfully against a JHipster monolith (OAuth2 + Vue)
  • Entity CRUD tests work with relationships and various field types
  • Swagger/API docs iframe test works correctly
  • Existing Cypress generator remains unaffected

Adds a new Playwright generator as an alternative to Cypress for end-to-end
testing. This mirrors the existing Cypress generator architecture with full
support for all authentication types (JWT, Session, OAuth2) and all client
frameworks (Angular, React, Vue).

The implementation includes:
- Full generator infrastructure (generator.ts, files.ts, command.ts, types)
- Playwright config template with sensible defaults
- Support utilities (commands, entity helpers, management API, account API)
- OAuth2 login helpers (Keycloak, Auth0, Okta)
- Account test specs (login, logout, register, settings, password, reset)
- Administration test specs (user management, metrics, health, logs, config, docs)
- Entity CRUD test template with relationship and pagination support
- Generator spec with matrix coverage matching Cypress dimensions
- Client command updated to offer Playwright as a test framework choice
- Test framework type system and application options updated

Closes jhipster#13755
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Playwright-based E2E testing generator alongside the existing Cypress infrastructure, integrating it into the JHipster generator option/prompt flow and providing Playwright templates/configuration for generated applications.

Changes:

  • Registers PLAYWRIGHT as a supported test framework option and exposes it in the client prompts.
  • Adds a new generators/playwright implementation (generator, command, file mappings, types) and associated template suite (config, support helpers, and E2E specs).
  • Adds generator test coverage via a matrix snapshot spec for Playwright generation.

Reviewed changes

Copilot reviewed 25 out of 27 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
lib/jhipster/test-framework-types.ts Adds PLAYWRIGHT to the test framework type registry.
lib/jhipster/application-options.ts Makes Playwright selectable via TEST_FRAMEWORKS option values.
generators/client/generator.ts Composes with the playwright generator when selected.
generators/client/command.ts Adds Playwright to the clientTestFrameworks prompt choices/defaults.
generators/playwright/command.ts Declares the Playwright generator command definition.
generators/playwright/files.ts Maps Playwright templates into the generated project structure.
generators/playwright/generator.ts Implements lifecycle tasks to generate Playwright config/tests and add deps/scripts.
generators/playwright/generator.spec.ts Adds snapshot-based matrix coverage for generator output.
generators/playwright/index.ts Exports generator/command/files for module integration.
generators/playwright/types.d.ts Provides generator-specific typing additions (dirs, flags, entity options).
generators/playwright/templates/playwright.config.ts.ejs Adds Playwright runner configuration template.
generators/playwright/templates/README.md.jhi.playwright.ejs Adds README fragment documenting Playwright E2E usage.
generators/playwright/templates/src/test/javascript/playwright/support/*.ejs Adds Playwright support helpers (commands/entity/account/management/oauth2).
generators/playwright/templates/src/test/javascript/playwright/e2e/**/*.ejs Adds Playwright E2E specs for account/admin/entity flows.
generators/playwright/templates/src/test/javascript/playwright/fixtures/integration-test.png Adds a fixture used by file-upload/blob field tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +100 to +104
test.afterEach(async ({ page }) => {
if (<%= entityInstance %>) {
await page.context().request.delete(`/<%= baseApi + entityApiUrl %>/${<%= entityInstance %>.<%= primaryKey.name %>}`);
<%= entityInstance %> = undefined;
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These API setup/cleanup calls use page.context().request directly. For JWT/session apps this request context won't automatically include the Bearer token / XSRF header, so entity create/delete will return 401/403 and make the tests fail. Use a shared helper (e.g., authenticatedRequest from support/commands) that attaches the correct auth+CSRF headers, and use it consistently for all direct API calls in this spec.

Copilot uses AI. Check for mistakes.
* Create an authenticated API request that includes the XSRF token.
*/
export async function authenticatedRequest(page: Page): Promise<APIRequestContext> {
return page.context().request;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For session/CSRF auth, authenticatedRequest() returns the raw request context and doesn't include the required X-XSRF-TOKEN header for state-changing requests. Any request.post/put/delete calls in the generated suite (e.g., updating account, creating entities) will likely fail with 403. Consider implementing a helper that reads the XSRF-TOKEN cookie and injects the matching header on each request.

Suggested change
return page.context().request;
const context = page.context();
const baseRequest = context.request;
// Retrieve XSRF token from cookies
const cookies = await context.cookies();
const xsrfCookie = cookies.find(c => c.name === 'XSRF-TOKEN');
const xsrfToken = xsrfCookie?.value;
// If there is no XSRF token, fall back to the base request context.
if (!xsrfToken) {
return baseRequest;
}
// Wrap the base request context to inject the XSRF header on state-changing requests.
const proxiedRequest = new Proxy(baseRequest as any, {
get(target, prop, receiver) {
if (prop === 'post' || prop === 'put' || prop === 'patch' || prop === 'delete') {
return (url: any, options?: any) => {
const headers = {
...(options?.headers ?? {}),
'X-XSRF-TOKEN': xsrfToken,
};
return target[prop](url, { ...options, headers });
};
}
if (prop === 'fetch') {
return (input: any, init?: any) => {
const headers = {
...(init?.headers ?? {}),
'X-XSRF-TOKEN': xsrfToken,
};
return target[prop](input, { ...init, headers });
};
}
return Reflect.get(target, prop, receiver);
},
});
return proxiedRequest as APIRequestContext;

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +20
export async function saveAccount(page: Page, account: Account): Promise<number> {
const response = await page.context().request.post('/api/account', {
data: account,
});
return response.status();
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

saveAccount() uses page.context().request.post(...) directly. With JWT/session auth this request won't automatically include a Bearer token / XSRF header, so saving settings will fail with 401/403 in generated apps. Route this through an authenticated request helper that injects the proper headers.

Copilot uses AI. Check for mistakes.
Comment on lines +186 to +190
// Intercept the entities list request and return our entity
await page.route('/<%= baseApi + entityApiUrl %>?(\\?*|)', async route => {
await route.fulfill({
status: 200,
<%_ if (!paginationNo) { _%>
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

page.route() is passed a string that looks like a regex (?...(\\?*|)), but Playwright string matchers are glob-style patterns, not regular expressions. This route likely won't match the entity list request with query params, so the interception won't happen. Use a RegExp matcher or a glob like **/<apiPath>* to reliably match the list request.

Copilot uses AI. Check for mistakes.
export async function login(page: Page, username: string, password: string): Promise<void> {
const context = page.context();
// Warm up the session cookie / CSRF
const warmup = await context.request.get('/api/account', { failOnStatusCode: false });
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warmup is assigned but never used. Either remove the variable or add an assertion/check that the warm-up request succeeded; leaving it unused makes the intent unclear and will typically be flagged by linting/TS tooling in generated projects.

Suggested change
const warmup = await context.request.get('/api/account', { failOnStatusCode: false });
await context.request.get('/api/account', { failOnStatusCode: false });

Copilot uses AI. Check for mistakes.
Comment on lines +128 to +132
* Create an authenticated API request context for direct API calls.
*/
export async function authenticatedRequest(page: Page): Promise<APIRequestContext> {
return page.context().request;
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For JWT auth, authenticatedRequest() currently returns page.context().request without attaching an Authorization: Bearer ... header. The generated tests use direct request.post/delete calls (e.g., entity setup/cleanup), which will be unauthenticated and fail with 401. Capture the JWT from /api/authenticate and ensure API requests include the bearer token (similar to Cypress's authenticatedRequest).

Copilot uses AI. Check for mistakes.
Comment on lines +65 to +67

// Submit credentials
await request.post(formUrl, {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keycloak login: formUrl is derived from the HTML and then used as-is in request.post(...). Keycloak often emits a relative form action, which a browser would resolve to an absolute URL. Resolve formUrl against the login/redirect URL (e.g., with new URL(formUrl, redirectUrl)) before posting, otherwise login will fail.

Suggested change
// Submit credentials
await request.post(formUrl, {
const resolvedFormUrl = new URL(formUrl, redirectUrl).toString();
// Submit credentials
await request.post(resolvedFormUrl, {

Copilot uses AI. Check for mistakes.
Comment on lines +54 to +56
const fixturePath = resolve(__dirname, '..', 'fixtures', fileName);
const fileInput = page.locator(`[data-cy="${fieldName}"]`);
await fileInput.setInputFiles({
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

__dirname is used to locate fixtures, but generated client package.json templates set "type": "module" (ESM). In ESM, __dirname is undefined, so this helper will crash at runtime when Playwright tests try to upload a file. Use an ESM-safe path resolution (e.g., derive directory from import.meta.url) or avoid filesystem path math by using new URL('../fixtures/...', import.meta.url).

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +5
import type { Page } from '@playwright/test';

export type Account = Record<string, string | boolean | number>;

/**
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This template is missing the standard JHipster Apache 2.0 header comment block that other Playwright templates in this PR include. Add the header for consistency and to avoid mixed licensing metadata across generated files.

Copilot uses AI. Check for mistakes.
- Add JWT Bearer token injection to authenticatedRequest helper
- Add XSRF token proxy for session-based authenticatedRequest
- Use authenticatedRequest for all direct API calls in entity spec
- Fix page.route() to use RegExp instead of invalid string pattern
- Remove unused warmup variable assignment in JWT login
- Resolve Keycloak form URL against redirect URL for relative paths
- Use import.meta.url instead of __dirname for ESM compatibility
- Add Apache 2.0 license header to account.ts.ejs template
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants