feat: add Playwright implementation of e2e tests#32472
feat: add Playwright implementation of e2e tests#32472teredasites wants to merge 2 commits intojhipster:mainfrom
Conversation
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
There was a problem hiding this comment.
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
PLAYWRIGHTas a supported test framework option and exposes it in the client prompts. - Adds a new
generators/playwrightimplementation (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.
| test.afterEach(async ({ page }) => { | ||
| if (<%= entityInstance %>) { | ||
| await page.context().request.delete(`/<%= baseApi + entityApiUrl %>/${<%= entityInstance %>.<%= primaryKey.name %>}`); | ||
| <%= entityInstance %> = undefined; | ||
| } |
There was a problem hiding this comment.
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.
| * Create an authenticated API request that includes the XSRF token. | ||
| */ | ||
| export async function authenticatedRequest(page: Page): Promise<APIRequestContext> { | ||
| return page.context().request; |
There was a problem hiding this comment.
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.
| 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; |
| export async function saveAccount(page: Page, account: Account): Promise<number> { | ||
| const response = await page.context().request.post('/api/account', { | ||
| data: account, | ||
| }); | ||
| return response.status(); |
There was a problem hiding this comment.
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.
| // Intercept the entities list request and return our entity | ||
| await page.route('/<%= baseApi + entityApiUrl %>?(\\?*|)', async route => { | ||
| await route.fulfill({ | ||
| status: 200, | ||
| <%_ if (!paginationNo) { _%> |
There was a problem hiding this comment.
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.
| 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 }); |
There was a problem hiding this comment.
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.
| const warmup = await context.request.get('/api/account', { failOnStatusCode: false }); | |
| await context.request.get('/api/account', { failOnStatusCode: false }); |
| * Create an authenticated API request context for direct API calls. | ||
| */ | ||
| export async function authenticatedRequest(page: Page): Promise<APIRequestContext> { | ||
| return page.context().request; | ||
| } |
There was a problem hiding this comment.
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).
|
|
||
| // Submit credentials | ||
| await request.post(formUrl, { |
There was a problem hiding this comment.
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.
| // Submit credentials | |
| await request.post(formUrl, { | |
| const resolvedFormUrl = new URL(formUrl, redirectUrl).toString(); | |
| // Submit credentials | |
| await request.post(resolvedFormUrl, { |
| const fixturePath = resolve(__dirname, '..', 'fixtures', fileName); | ||
| const fileInput = page.locator(`[data-cy="${fieldName}"]`); | ||
| await fileInput.setInputFiles({ |
There was a problem hiding this comment.
__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).
| import type { Page } from '@playwright/test'; | ||
|
|
||
| export type Account = Record<string, string | boolean | number>; | ||
|
|
||
| /** |
There was a problem hiding this comment.
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.
- 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
/claim #13755
Summary
Adds a new
playwrightgenerator 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 extendingBaseApplicationGenerator, implementing preparing, writing, writingEntities, and postWriting lifecycle phasesgenerators/playwright/files.ts— File section definitions mapping templates to output paths usingplaywrightDircontext variablegenerators/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 exportsgenerators/playwright/generator.spec.ts— Generator test spec with matrix coverage across all auth types, client frameworks, and admin UI configurationsFramework integration
lib/jhipster/test-framework-types.ts— AddedPLAYWRIGHTto the test framework enumlib/jhipster/application-options.ts— Registered Playwright in theTEST_FRAMEWORKSoption valuesgenerators/client/generator.ts— Added composition with theplaywrightgenerator when selectedgenerators/client/command.ts— Added Playwright as a choice in theclientTestFrameworkspromptSupport utilities (templates)
commands.ts.ejs— Selectors, credentials helper, login functions (JWT/Session/OAuth2), navbar helpers. All functions takepage: Pageas the first parameter instead of relying on Cypress's globalcyobject.entity.ts.ejs— Entity selectors and helpers (getEntityHeading,setFieldImageAsBytesOfEntity,setFieldSelectToLastOfEntity)management.ts.ejs—getManagementInfo()using Playwright's request APIaccount.ts.ejs—getAccount()andsaveAccount()using API request contextlogin.ts.ejs— Re-exports from commandsoauth2.ts.ejs— OAuth2 login flows for Keycloak, Auth0, and OktaTest templates
page.frameLocator()), websocket trackerKey Playwright API translations from Cypress
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 injectionCypress.Commands.add()page: Pageparameter.should('be.visible')expect(locator).toBeVisible().should('have.class', ...)expect(locator).toHaveClass(new RegExp(...))Test plan