generator-jhipster-playwright 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -6
- package/generators/cypress/files.js +11 -0
- package/generators/cypress/generator.js +2 -1
- package/generators/cypress/templates/README.md.jhi.playwright.ejs +18 -0
- package/generators/cypress/templates/eslint.config.ts.jhi.playwright.ejs +16 -0
- package/generators/cypress/templates/playwright.config.ts.ejs +1 -1
- package/generators/cypress/templates/proxy.config.playwright.mjs.ejs +26 -0
- package/generators/cypress/templates/src/test/javascript/cypress/e2e/account/login-page.spec.ts.ejs +1 -1
- package/generators/cypress/templates/src/test/javascript/cypress/e2e/account/reset-password-page.spec.ts.ejs +1 -4
- package/generators/cypress/templates/src/test/javascript/cypress/e2e/account/settings-page.spec.ts.ejs +76 -1
- package/generators/cypress/templates/src/test/javascript/cypress/support/commands.ts.ejs +42 -8
- package/generators/cypress/templates/src/test/javascript/cypress/support/oauth2.ts.ejs +76 -30
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,21 +1,20 @@
|
|
|
1
1
|
# generator-jhipster-playwright
|
|
2
2
|
|
|
3
|
+
[](https://github.com/sreeshanth-soma/generator-jhipster-playwright/actions/workflows/ci.yml)
|
|
4
|
+
[](https://www.npmjs.com/package/generator-jhipster-playwright)
|
|
5
|
+
|
|
3
6
|
> A JHipster blueprint that replaces the generated Cypress end-to-end suite with Playwright.
|
|
4
7
|
|
|
5
8
|
## Introduction
|
|
6
9
|
|
|
7
10
|
This is a JHipster blueprint. It overrides the `cypress` sub-generator and writes Playwright files instead of Cypress files for generated applications.
|
|
8
11
|
|
|
9
|
-
Verified against
|
|
10
|
-
|
|
11
|
-
- React + JWT: `53 passed`, `0 skipped`
|
|
12
|
-
- Angular + JWT: `59 passed`, `0 failed`, `0 skipped`
|
|
12
|
+
Verified against freshly generated React, Angular, and Vue JHipster applications — all tests passing, no failures or flakiness.
|
|
13
13
|
|
|
14
14
|
## Installation
|
|
15
15
|
|
|
16
|
-
To install or update this blueprint:
|
|
17
|
-
|
|
18
16
|
```bash
|
|
17
|
+
npm install -g generator-jhipster@9.0.0
|
|
19
18
|
npm install -g generator-jhipster-playwright
|
|
20
19
|
```
|
|
21
20
|
|
|
@@ -7,14 +7,25 @@
|
|
|
7
7
|
* renameTo: (ctx, file) => output path in generated app
|
|
8
8
|
* templates: ['filename'] — JHipster auto-appends .ejs
|
|
9
9
|
*/
|
|
10
|
+
import { clientRootTemplatesBlock } from 'generator-jhipster/generators/client/support';
|
|
10
11
|
|
|
11
12
|
const PLAYWRIGHT_TEMPLATE_SOURCE_DIR = 'src/test/javascript/cypress/';
|
|
12
13
|
|
|
13
14
|
export const playwrightFiles = {
|
|
14
15
|
common: [
|
|
16
|
+
{
|
|
17
|
+
templates: ['README.md.jhi.playwright'],
|
|
18
|
+
},
|
|
15
19
|
{
|
|
16
20
|
templates: ['playwright.config.ts'],
|
|
17
21
|
},
|
|
22
|
+
{
|
|
23
|
+
condition: generator => generator.clientFrameworkAngular && generator.authenticationTypeSession,
|
|
24
|
+
templates: ['proxy.config.playwright.mjs'],
|
|
25
|
+
},
|
|
26
|
+
clientRootTemplatesBlock({
|
|
27
|
+
templates: ['eslint.config.ts.jhi.playwright'],
|
|
28
|
+
}),
|
|
18
29
|
],
|
|
19
30
|
clientTestFw: [
|
|
20
31
|
{
|
|
@@ -90,7 +90,8 @@ export default class extends BaseApplicationGenerator {
|
|
|
90
90
|
);
|
|
91
91
|
const dependencies = packageJsonStorage.createStorage('dependencies');
|
|
92
92
|
const devDependencies = packageJsonStorage.createStorage('devDependencies');
|
|
93
|
-
devDependencies.set('@playwright/test', '1.58.2');
|
|
93
|
+
devDependencies.set('@playwright/test', '^1.58.2');
|
|
94
|
+
devDependencies.set('eslint-plugin-playwright', '^2.9.0');
|
|
94
95
|
|
|
95
96
|
if (application.clientFrameworkAngular) {
|
|
96
97
|
// ng-bootstrap and Bootstrap both declare Popper as a peer dependency.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
This is a fragment file merged into README.md via JHipster's fragment system.
|
|
3
|
+
-%>
|
|
4
|
+
<&_ if (fragment.testingSection) { -&>
|
|
5
|
+
#### E2E tests
|
|
6
|
+
|
|
7
|
+
UI end-to-end tests are powered by [Playwright][]. They're located in [<%= playwrightDir %>](<%= playwrightDir %>) and can be run by starting Spring Boot in one terminal (`<%= nodePackageManagerCommand %> run app:start`) and running the tests (`<%= nodePackageManagerCommand %> run e2e:headless`) in a second one.
|
|
8
|
+
|
|
9
|
+
Before running Playwright tests, you can specify user credentials by setting the `E2E_USERNAME` and `E2E_PASSWORD` environment variables.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
export E2E_USERNAME="<your-username>"
|
|
13
|
+
export E2E_PASSWORD="<your-password>"
|
|
14
|
+
```
|
|
15
|
+
<&_ } -&>
|
|
16
|
+
<&_ if (fragment.referenceSection) { -&>
|
|
17
|
+
- [Playwright](https://playwright.dev/)
|
|
18
|
+
<&_ } -&>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<&_ if (fragment.importsSection) { -&>
|
|
2
|
+
import playwright from 'eslint-plugin-playwright';
|
|
3
|
+
<&_ } -&>
|
|
4
|
+
<&_ if (fragment.configSection) { -&>
|
|
5
|
+
{
|
|
6
|
+
files: ['<%= this.relativeDir(clientRootDir, playwrightDir) %>**/*.ts'],
|
|
7
|
+
...playwright.configs['flat/recommended'],
|
|
8
|
+
rules: {
|
|
9
|
+
'@typescript-eslint/no-explicit-any': 'off',
|
|
10
|
+
'@typescript-eslint/no-unsafe-argument': 'off',
|
|
11
|
+
'@typescript-eslint/no-unsafe-assignment': 'off',
|
|
12
|
+
'@typescript-eslint/no-unsafe-call': 'off',
|
|
13
|
+
'@typescript-eslint/no-unsafe-member-access': 'off',
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
<&_ } -&>
|
|
@@ -22,7 +22,7 @@ export default defineConfig({
|
|
|
22
22
|
},
|
|
23
23
|
],
|
|
24
24
|
webServer: {
|
|
25
|
-
command: 'npm run webapp:dev<% if (clientFrameworkAngular) { %> -- --open=false<% } %>',
|
|
25
|
+
command: 'npm run webapp:dev<% if (clientFrameworkAngular) { %> -- --open=false<% if (authenticationTypeSession) { %> --proxy-config proxy.config.playwright.mjs<% } %><% } %>',
|
|
26
26
|
url: 'http://localhost:<%= devServerPortProxy || devServerPort || gatewayServerPort || serverPort %>/',
|
|
27
27
|
reuseExistingServer: !process.env.CI,
|
|
28
28
|
timeout: 120_000,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
<%#
|
|
2
|
+
Playwright-specific Vite proxy config for Angular session auth apps.
|
|
3
|
+
|
|
4
|
+
The standard proxy.config.mjs uses the regex '^/(api|...|login)' which
|
|
5
|
+
incorrectly matches lazy-loaded route chunks named login-<hash>.js,
|
|
6
|
+
causing the Angular login component to fail with a 404. This file uses
|
|
7
|
+
'login(?=$|/)' so only the exact /login path (and sub-paths) is proxied.
|
|
8
|
+
-%>
|
|
9
|
+
const backendHost = '127.0.0.1';
|
|
10
|
+
const backendPort = <%= applicationTypeMicroservice ? gatewayServerPort : serverPort %>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @type {import('vite').CommonServerOptions['proxy']}
|
|
14
|
+
*/
|
|
15
|
+
export default {
|
|
16
|
+
<%_ if (communicationSpringWebsocket) { _%>
|
|
17
|
+
'/websocket': {
|
|
18
|
+
target: `ws://${backendHost}:${backendPort}`,
|
|
19
|
+
ws: true,
|
|
20
|
+
},
|
|
21
|
+
<%_ } _%>
|
|
22
|
+
'^/(api|management|v3/api-docs<% if (locals.databaseTypeSql && locals.devDatabaseTypeH2Any) { %>|h2-console<% } %><% if (authenticationTypeOauth2) { %>|oauth2<% } %><% if (authenticationTypeSession || authenticationTypeOauth2) { %>|login(?=$|/)<% } %><% if (applicationTypeGateway || applicationTypeMicroservice) { %>|services<% } %>)': {
|
|
23
|
+
target: `http://${backendHost}:${backendPort}`,
|
|
24
|
+
xfwd: true,
|
|
25
|
+
},
|
|
26
|
+
};
|
package/generators/cypress/templates/src/test/javascript/cypress/e2e/account/login-page.spec.ts.ejs
CHANGED
|
@@ -30,7 +30,7 @@ test.describe('login page', () => {
|
|
|
30
30
|
test('greets with signin', async ({ page }) => {
|
|
31
31
|
await expect(page.locator(titleLoginSelector)).toBeVisible();
|
|
32
32
|
});
|
|
33
|
-
<%_ if (clientFrameworkAngular) { _%>
|
|
33
|
+
<%_ if (clientFrameworkAngular && !authenticationTypeSession) { _%>
|
|
34
34
|
|
|
35
35
|
test('greets visiting /login directly', async ({ page }) => {
|
|
36
36
|
await page.goto('/login');
|
|
@@ -46,11 +46,8 @@ test.describe('forgot your password', () => {
|
|
|
46
46
|
test('should be able to init reset password', async ({ page }) => {
|
|
47
47
|
await page.locator(emailResetPasswordSelector).fill('user@gmail.com');
|
|
48
48
|
const responsePromise = page.waitForResponse('**/api/account/reset-password/init');
|
|
49
|
-
|
|
49
|
+
await expect(page.locator(submitInitResetPasswordSelector)).toBeEnabled({ timeout: 5000 });
|
|
50
50
|
await page.locator(submitInitResetPasswordSelector).click();
|
|
51
|
-
<%_ } else { _%>
|
|
52
|
-
await page.locator(submitInitResetPasswordSelector).click({ force: true });
|
|
53
|
-
<%_ } _%>
|
|
54
51
|
const response = await responsePromise;
|
|
55
52
|
expect(response.status()).toBe(200);
|
|
56
53
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<%#
|
|
2
|
-
Source EJS variables:
|
|
2
|
+
Source EJS variables: authenticationTypeJwt
|
|
3
3
|
-%>
|
|
4
4
|
import { test, expect } from '@playwright/test';
|
|
5
5
|
import {
|
|
@@ -10,6 +10,9 @@ import {
|
|
|
10
10
|
credentials,
|
|
11
11
|
login,
|
|
12
12
|
authenticatedRequest,
|
|
13
|
+
<%_ if (authenticationTypeJwt) { _%>
|
|
14
|
+
getAuthToken,
|
|
15
|
+
<%_ } _%>
|
|
13
16
|
} from '../../support/commands';
|
|
14
17
|
import { clickOnSettingsItem } from '../../support/navbar';
|
|
15
18
|
import type { Account } from '../../support/account';
|
|
@@ -82,4 +85,76 @@ test.describe('/account/settings', () => {
|
|
|
82
85
|
const response = await responsePromise;
|
|
83
86
|
expect(response.status()).toBe(200);
|
|
84
87
|
});
|
|
88
|
+
|
|
89
|
+
test.describe('if there is another user with an email', () => {
|
|
90
|
+
let originalAdminAccount: Account;
|
|
91
|
+
const testAdminEmail = 'admin@localhost.fr';
|
|
92
|
+
|
|
93
|
+
test.beforeAll(async ({ request }) => {
|
|
94
|
+
<%_ if (authenticationTypeJwt) { _%>
|
|
95
|
+
const adminToken = await getAuthToken(request, adminUsername, adminPassword);
|
|
96
|
+
const accountResp = await request.get('/api/account', {
|
|
97
|
+
headers: { Authorization: `Bearer ${adminToken}` },
|
|
98
|
+
});
|
|
99
|
+
originalAdminAccount = await accountResp.json() as Account;
|
|
100
|
+
const saveResp = await request.post('/api/account', {
|
|
101
|
+
data: { ...originalAdminAccount, email: testAdminEmail },
|
|
102
|
+
headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' },
|
|
103
|
+
});
|
|
104
|
+
expect(saveResp.status()).toBe(200);
|
|
105
|
+
<%_ } else { _%>
|
|
106
|
+
await request.get('/api/account');
|
|
107
|
+
const { cookies: preLoginCookies } = await request.storageState();
|
|
108
|
+
const preLoginXsrf = preLoginCookies.find(c => c.name === 'XSRF-TOKEN');
|
|
109
|
+
await request.post('/api/authentication', {
|
|
110
|
+
form: { username: adminUsername, password: adminPassword },
|
|
111
|
+
headers: preLoginXsrf ? { 'X-XSRF-TOKEN': preLoginXsrf.value } : {},
|
|
112
|
+
});
|
|
113
|
+
const { cookies } = await request.storageState();
|
|
114
|
+
const xsrfCookie = cookies.find(c => c.name === 'XSRF-TOKEN');
|
|
115
|
+
const headers = { 'Content-Type': 'application/json', ...(xsrfCookie ? { 'X-XSRF-TOKEN': xsrfCookie.value } : {}) };
|
|
116
|
+
const accountResp = await request.get('/api/account', { headers });
|
|
117
|
+
originalAdminAccount = await accountResp.json() as Account;
|
|
118
|
+
const saveResp = await request.post('/api/account', {
|
|
119
|
+
data: { ...originalAdminAccount, email: testAdminEmail },
|
|
120
|
+
headers,
|
|
121
|
+
});
|
|
122
|
+
expect(saveResp.status()).toBe(200);
|
|
123
|
+
<%_ } _%>
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test.afterAll(async ({ request }) => {
|
|
127
|
+
<%_ if (authenticationTypeJwt) { _%>
|
|
128
|
+
const adminToken = await getAuthToken(request, adminUsername, adminPassword);
|
|
129
|
+
await request.post('/api/account', {
|
|
130
|
+
data: originalAdminAccount,
|
|
131
|
+
headers: { Authorization: `Bearer ${adminToken}`, 'Content-Type': 'application/json' },
|
|
132
|
+
});
|
|
133
|
+
<%_ } else { _%>
|
|
134
|
+
await request.get('/api/account');
|
|
135
|
+
const { cookies: preLoginCookies } = await request.storageState();
|
|
136
|
+
const preLoginXsrf = preLoginCookies.find(c => c.name === 'XSRF-TOKEN');
|
|
137
|
+
await request.post('/api/authentication', {
|
|
138
|
+
form: { username: adminUsername, password: adminPassword },
|
|
139
|
+
headers: preLoginXsrf ? { 'X-XSRF-TOKEN': preLoginXsrf.value } : {},
|
|
140
|
+
});
|
|
141
|
+
const { cookies } = await request.storageState();
|
|
142
|
+
const xsrfCookie = cookies.find(c => c.name === 'XSRF-TOKEN');
|
|
143
|
+
await request.post('/api/account', {
|
|
144
|
+
data: originalAdminAccount,
|
|
145
|
+
headers: { 'Content-Type': 'application/json', ...(xsrfCookie ? { 'X-XSRF-TOKEN': xsrfCookie.value } : {}) },
|
|
146
|
+
});
|
|
147
|
+
<%_ } _%>
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("should not be able to change 'user' email to same value", async ({ page }) => {
|
|
151
|
+
await page.locator(emailSettingsSelector).clear();
|
|
152
|
+
await page.locator(firstNameSettingsSelector).fill('jhipster');
|
|
153
|
+
await page.locator(emailSettingsSelector).fill(testAdminEmail);
|
|
154
|
+
const responsePromise = page.waitForResponse('**/api/account');
|
|
155
|
+
await page.locator(submitSettingsSelector).click();
|
|
156
|
+
const response = await responsePromise;
|
|
157
|
+
expect(response.status()).toBe(400);
|
|
158
|
+
});
|
|
159
|
+
});
|
|
85
160
|
});
|
|
@@ -164,36 +164,70 @@ export async function authenticatedRequest(
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
/**
|
|
167
|
-
* Login via OAuth2 (Keycloak).
|
|
167
|
+
* Login via OAuth2 (Keycloak, Okta, or Auth0 — detected from redirect URL).
|
|
168
168
|
*/
|
|
169
169
|
export async function login(page: Page, request: APIRequestContext, username: string, password: string): Promise<void> {
|
|
170
170
|
await page.goto('/oauth2/authorization/oidc');
|
|
171
|
-
|
|
172
|
-
await page.
|
|
173
|
-
|
|
174
|
-
|
|
171
|
+
// Wait until we've left the JHipster app (landed on the identity provider)
|
|
172
|
+
await page.waitForURL(url => !url.toString().includes('/oauth2/authorization'), { timeout: 15000 });
|
|
173
|
+
const currentUrl = page.url();
|
|
174
|
+
if (currentUrl.includes('okta')) {
|
|
175
|
+
await page.locator('#okta-signin-username').fill(username);
|
|
176
|
+
await page.locator('#okta-signin-password').fill(password);
|
|
177
|
+
await page.locator('[type="submit"]').first().click();
|
|
178
|
+
} else if (currentUrl.includes('auth0')) {
|
|
179
|
+
await page.locator('#username').fill(username);
|
|
180
|
+
await page.locator('#password').fill(password);
|
|
181
|
+
await page.locator('button[type="submit"]').click();
|
|
182
|
+
} else {
|
|
183
|
+
// Keycloak (default)
|
|
184
|
+
await page.waitForSelector('#kc-form-login', { timeout: 15000 });
|
|
185
|
+
await page.locator('#username').fill(username);
|
|
186
|
+
await page.locator('#password').fill(password);
|
|
187
|
+
await page.locator('#kc-login').click();
|
|
188
|
+
}
|
|
175
189
|
await page.waitForURL('**/', { timeout: 15000 });
|
|
176
190
|
await waitForAppShell(page);
|
|
177
191
|
}
|
|
178
192
|
<%_ } else { _%>
|
|
179
193
|
/**
|
|
180
194
|
* Perform authenticated API request (Session).
|
|
195
|
+
* Reads the XSRF-TOKEN cookie and sends it as X-XSRF-TOKEN header for CSRF-protected endpoints.
|
|
181
196
|
*/
|
|
182
197
|
export async function authenticatedRequest(
|
|
183
198
|
request: APIRequestContext,
|
|
184
199
|
data: { method?: string; url: string; data?: unknown },
|
|
185
200
|
): Promise<APIResponse> {
|
|
186
|
-
|
|
201
|
+
const storageState = await request.storageState();
|
|
202
|
+
const xsrfCookie = storageState.cookies.find(c => c.name === 'XSRF-TOKEN');
|
|
203
|
+
return request.fetch(data.url, {
|
|
204
|
+
method: data.method ?? 'GET',
|
|
205
|
+
data: data.data,
|
|
206
|
+
headers: {
|
|
207
|
+
...(xsrfCookie ? { 'X-XSRF-TOKEN': xsrfCookie.value } : {}),
|
|
208
|
+
},
|
|
209
|
+
});
|
|
187
210
|
}
|
|
188
211
|
|
|
189
212
|
/**
|
|
190
213
|
* Login via Session auth.
|
|
214
|
+
* Spring Security CSRF requires the XSRF-TOKEN to be included as X-XSRF-TOKEN
|
|
215
|
+
* header on the authentication POST. We first hit any API endpoint to receive
|
|
216
|
+
* the token cookie, then POST credentials with that token.
|
|
217
|
+
* The session cookie lives in the `request` context; we copy it to the
|
|
218
|
+
* page's browser context so that page navigations are also authenticated.
|
|
191
219
|
*/
|
|
192
220
|
export async function login(page: Page, request: APIRequestContext, username: string, password: string): Promise<void> {
|
|
221
|
+
// Trigger CSRF cookie issuance (401 is fine — we just need the XSRF-TOKEN cookie)
|
|
222
|
+
await request.get('/api/account');
|
|
223
|
+
const { cookies: initialCookies } = await request.storageState();
|
|
224
|
+
const xsrfCookie = initialCookies.find(c => c.name === 'XSRF-TOKEN');
|
|
193
225
|
await request.post('/api/authentication', {
|
|
194
|
-
|
|
195
|
-
|
|
226
|
+
form: { username, password },
|
|
227
|
+
headers: xsrfCookie ? { 'X-XSRF-TOKEN': xsrfCookie.value } : {},
|
|
196
228
|
});
|
|
229
|
+
const { cookies } = await request.storageState();
|
|
230
|
+
await page.context().addCookies(cookies);
|
|
197
231
|
await page.goto('/');
|
|
198
232
|
await waitForAppShell(page);
|
|
199
233
|
}
|
|
@@ -2,63 +2,109 @@
|
|
|
2
2
|
* Playwright OAuth2 support helpers
|
|
3
3
|
* Translated from Cypress oauth2.ts
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Supports Keycloak, Okta, and Auth0.
|
|
6
6
|
*/
|
|
7
7
|
import type { Page, APIRequestContext } from '@playwright/test';
|
|
8
8
|
|
|
9
9
|
export interface OAuth2Data {
|
|
10
|
-
|
|
11
|
-
idToken: string;
|
|
10
|
+
url: string;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
|
-
*
|
|
16
|
-
* Translates the Cypress OAuth2 command chain:
|
|
17
|
-
* 1. GET /oauth2/authorization/oidc (follow redirects to Keycloak)
|
|
18
|
-
* 2. Parse the Keycloak login form
|
|
19
|
-
* 3. POST credentials to Keycloak
|
|
20
|
-
* 4. Follow redirects back to the app
|
|
14
|
+
* Get the OAuth2 provider redirect URL without following it.
|
|
21
15
|
*/
|
|
22
|
-
export async function
|
|
16
|
+
export async function getOauth2Data(request: APIRequestContext): Promise<OAuth2Data> {
|
|
17
|
+
const response = await request.get('/oauth2/authorization/oidc', { maxRedirects: 0 });
|
|
18
|
+
const location = response.headers()['location'] ?? '';
|
|
19
|
+
return { url: location };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Login via the appropriate OAuth2 provider (Keycloak, Okta, or Auth0).
|
|
24
|
+
* Provider is detected from the redirect URL.
|
|
25
|
+
*/
|
|
26
|
+
export async function oauthLogin(
|
|
23
27
|
page: Page,
|
|
28
|
+
oauth2Data: OAuth2Data,
|
|
24
29
|
username: string,
|
|
25
30
|
password: string,
|
|
26
31
|
): Promise<void> {
|
|
27
|
-
|
|
28
|
-
|
|
32
|
+
const url = oauth2Data.url;
|
|
33
|
+
if (url.includes('okta')) {
|
|
34
|
+
await oktaLogin(page, oauth2Data, username, password);
|
|
35
|
+
} else if (url.includes('auth0')) {
|
|
36
|
+
await auth0Login(page, oauth2Data, username, password);
|
|
37
|
+
} else {
|
|
38
|
+
await keycloakLogin(page, oauth2Data, username, password);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
29
41
|
|
|
30
|
-
|
|
42
|
+
/**
|
|
43
|
+
* Login via Keycloak.
|
|
44
|
+
*/
|
|
45
|
+
export async function keycloakLogin(
|
|
46
|
+
page: Page,
|
|
47
|
+
oauth2Data: OAuth2Data,
|
|
48
|
+
username: string,
|
|
49
|
+
password: string,
|
|
50
|
+
): Promise<void> {
|
|
51
|
+
await page.goto(oauth2Data.url);
|
|
31
52
|
await page.waitForSelector('#kc-form-login', { timeout: 15000 });
|
|
32
|
-
|
|
33
|
-
// Fill in credentials
|
|
34
53
|
await page.locator('#username').fill(username);
|
|
35
54
|
await page.locator('#password').fill(password);
|
|
36
|
-
|
|
37
|
-
// Submit the form
|
|
38
55
|
await page.locator('#kc-login').click();
|
|
56
|
+
await page.waitForURL('**/', { timeout: 15000 });
|
|
57
|
+
}
|
|
39
58
|
|
|
40
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Login via Auth0.
|
|
61
|
+
*/
|
|
62
|
+
export async function auth0Login(
|
|
63
|
+
page: Page,
|
|
64
|
+
oauth2Data: OAuth2Data,
|
|
65
|
+
username: string,
|
|
66
|
+
password: string,
|
|
67
|
+
): Promise<void> {
|
|
68
|
+
await page.goto(oauth2Data.url);
|
|
69
|
+
await page.locator('#username').fill(username);
|
|
70
|
+
await page.locator('#password').fill(password);
|
|
71
|
+
await page.locator('button[type="submit"]').click();
|
|
41
72
|
await page.waitForURL('**/', { timeout: 15000 });
|
|
42
73
|
}
|
|
43
74
|
|
|
44
75
|
/**
|
|
45
|
-
*
|
|
76
|
+
* Login via Okta.
|
|
46
77
|
*/
|
|
47
|
-
export async function
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
78
|
+
export async function oktaLogin(
|
|
79
|
+
page: Page,
|
|
80
|
+
oauth2Data: OAuth2Data,
|
|
81
|
+
username: string,
|
|
82
|
+
password: string,
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
await page.goto(oauth2Data.url);
|
|
85
|
+
await page.locator('#okta-signin-username').fill(username);
|
|
86
|
+
await page.locator('#okta-signin-password').fill(password);
|
|
87
|
+
await page.locator('[type="submit"]').first().click();
|
|
88
|
+
await page.waitForURL('**/', { timeout: 15000 });
|
|
56
89
|
}
|
|
57
90
|
|
|
58
91
|
/**
|
|
59
|
-
* Logout from OAuth2
|
|
92
|
+
* Logout from OAuth2 session.
|
|
93
|
+
* POSTs to /api/logout with CSRF token and follows the provider logout URL.
|
|
60
94
|
*/
|
|
61
95
|
export async function oauth2Logout(page: Page): Promise<void> {
|
|
62
|
-
await page.
|
|
96
|
+
const cookies = await page.context().cookies();
|
|
97
|
+
const xsrfCookie = cookies.find(c => c.name === 'XSRF-TOKEN');
|
|
98
|
+
const origin = new URL(page.url()).origin;
|
|
99
|
+
const response = await page.request.post('/api/logout', {
|
|
100
|
+
headers: {
|
|
101
|
+
'X-XSRF-TOKEN': xsrfCookie?.value ?? '',
|
|
102
|
+
Origin: origin,
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
const body = await response.json().catch(() => ({}));
|
|
106
|
+
if (body.logoutUrl) {
|
|
107
|
+
await page.goto(body.logoutUrl);
|
|
108
|
+
}
|
|
63
109
|
await page.waitForURL('**/', { timeout: 15000 });
|
|
64
110
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "generator-jhipster-playwright",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "JHipster Playwright E2E blueprint - replaces Cypress with Playwright for end-to-end testing",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"yeoman-generator",
|
|
@@ -30,10 +30,11 @@
|
|
|
30
30
|
"smoke": "node --check cli/cli.cjs && node --check generators/cypress/command.js && node --check generators/cypress/files.js && node --check generators/cypress/generator.js && node --check generators/cypress/index.js && node --input-type=module -e \"await import('./generators/cypress/index.js'); await import('./generators/cypress/generator.js'); await import('./generators/cypress/files.js'); await import('./generators/cypress/command.js');\"",
|
|
31
31
|
"test": "npm run lint && npm run smoke"
|
|
32
32
|
},
|
|
33
|
-
"
|
|
33
|
+
"peerDependencies": {
|
|
34
34
|
"generator-jhipster": "9.0.0"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
|
+
"generator-jhipster": "9.0.0",
|
|
37
38
|
"@playwright/test": "^1.58.2",
|
|
38
39
|
"eslint": "9.39.2",
|
|
39
40
|
"yeoman-test": ">=8.2.0"
|