@camunda/e2e-test-suite 0.0.1056 → 0.0.1058
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.
|
@@ -105,10 +105,21 @@ class Authorization {
|
|
|
105
105
|
force: true,
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
108
|
+
// Submitting POSTs to the orchestration cluster's Identity API, which
|
|
109
|
+
// answers 503 while the cluster is still settling. Carbon then leaves the
|
|
110
|
+
// modal open with the form still filled, so the only thing the old
|
|
111
|
+
// single-shot submit could do was wait out its 30s and fail. Re-submitting
|
|
112
|
+
// replays the same request from the same form rather than building a
|
|
113
|
+
// second authorization; the guard keeps a slow-but-successful close from
|
|
114
|
+
// clicking a button that is already gone.
|
|
115
|
+
await (0, test_1.expect)(async () => {
|
|
116
|
+
if (await this.createAuthorizationModal.isVisible()) {
|
|
117
|
+
await this.createAuthorizationSubmitButton.click();
|
|
118
|
+
}
|
|
119
|
+
await (0, test_1.expect)(this.createAuthorizationModal).not.toBeVisible({
|
|
120
|
+
timeout: 30000,
|
|
121
|
+
});
|
|
122
|
+
}).toPass({ timeout: 150000, intervals: [5000, 10000, 15000] });
|
|
112
123
|
await this.selectResourceTypeTab(authorization.resourceType).click();
|
|
113
124
|
const item = this.page.getByRole('row').filter({
|
|
114
125
|
hasText: `${authorization.ownerId}${authorization.resourceId}`,
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
6
7
|
class LoginPage {
|
|
7
8
|
page;
|
|
8
9
|
usernameInput;
|
|
@@ -55,6 +56,7 @@ class LoginPage {
|
|
|
55
56
|
}
|
|
56
57
|
async login(credentials = {}) {
|
|
57
58
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
59
|
+
(0, sessionRecovery_1.trackSignedInUser)(this.page, username);
|
|
58
60
|
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 200000 });
|
|
59
61
|
await this.fillUsername(username);
|
|
60
62
|
await this.clickContinueButton();
|
|
@@ -65,6 +67,7 @@ class LoginPage {
|
|
|
65
67
|
}
|
|
66
68
|
async loginWithTestUser(credentials = {}) {
|
|
67
69
|
const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
|
|
70
|
+
(0, sessionRecovery_1.trackSignedInUser)(this.page, username);
|
|
68
71
|
// SSO re-auth may silently log us back in as the previous user after logout.
|
|
69
72
|
// Detect Console (settings button visible) and retry logout until Auth0 form appears.
|
|
70
73
|
const settingsBtn = this.page.getByLabel('Open Settings');
|
|
@@ -114,6 +117,7 @@ class LoginPage {
|
|
|
114
117
|
}
|
|
115
118
|
async loginWithoutOrgAssertion(credentials = {}) {
|
|
116
119
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
120
|
+
(0, sessionRecovery_1.trackSignedInUser)(this.page, username);
|
|
117
121
|
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 180000 });
|
|
118
122
|
await this.fillUsername(username);
|
|
119
123
|
await this.clickContinueButton();
|
|
@@ -4,6 +4,7 @@ exports.ModelerHomePage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
|
|
7
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
7
8
|
const randomName_1 = require("../../utils/randomName");
|
|
8
9
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
9
10
|
class ModelerHomePage {
|
|
@@ -172,6 +173,10 @@ class ModelerHomePage {
|
|
|
172
173
|
async clickCreateNewProjectButton() {
|
|
173
174
|
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.createNewProjectButton, {
|
|
174
175
|
preAction: async () => {
|
|
176
|
+
// Modeler logs the SPA out on its own when a backend call fails, and
|
|
177
|
+
// every later attempt then waits for this button on the Auth0 form
|
|
178
|
+
// instead of on Modeler. Get the session back before looking for it.
|
|
179
|
+
await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page);
|
|
175
180
|
await this.dismissOverlays();
|
|
176
181
|
},
|
|
177
182
|
totalTimeout: 120000,
|
|
@@ -204,6 +209,16 @@ class ModelerHomePage {
|
|
|
204
209
|
// editor unmounts, and the click below then waits out its whole timeout
|
|
205
210
|
// on an element that never comes back.
|
|
206
211
|
await this.dismissOverlays();
|
|
212
|
+
// Modeler can answer the create-project POST with a 5xx. The click that
|
|
213
|
+
// got us here still "succeeded", but no project exists and the browser
|
|
214
|
+
// stayed on the projects list, so the inline editor never mounts and
|
|
215
|
+
// every retry below re-clicks a control that is not there until toPass
|
|
216
|
+
// gives up. Re-issue the creation when that is what happened -- a rename
|
|
217
|
+
// needs a project to rename.
|
|
218
|
+
if (!(await this.becameVisible(this.projectNameInput.first(), 5000)) &&
|
|
219
|
+
(await this.becameVisible(this.createNewProjectButton, 5000))) {
|
|
220
|
+
await this.createNewProjectButton.click({ timeout: 15000 });
|
|
221
|
+
}
|
|
207
222
|
await this.projectNameInput.first().click({ timeout: 15000 });
|
|
208
223
|
await this.projectNameInput.first().fill(name);
|
|
209
224
|
// Confirm the value actually landed in the edit field before committing;
|
|
@@ -514,7 +529,14 @@ class ModelerHomePage {
|
|
|
514
529
|
// Widened from 180s: dismissing the announcement dialog costs up to ~25s
|
|
515
530
|
// of settle-and-probe per attempt, which at 180s left room for barely one
|
|
516
531
|
// retry after an intercepted click had already burnt its 60s.
|
|
517
|
-
|
|
532
|
+
//
|
|
533
|
+
// Widened again from 240s. When Modeler answers `/api/internal/login`
|
|
534
|
+
// with a 5xx the app hangs on its bootstrap spinner, and then
|
|
535
|
+
// dismissBlockingDialog's networkidle wait never settles either -- an
|
|
536
|
+
// attempt costs ~80s instead of ~25s, so the 240s budget ran out after
|
|
537
|
+
// three of the five retries configured below. The 2026-09-01 nightly
|
|
538
|
+
// took two such 5xx to bury a test that had three unused attempts left.
|
|
539
|
+
totalTimeout: 360000,
|
|
518
540
|
maxRetries: 5,
|
|
519
541
|
preAction: async () => {
|
|
520
542
|
// Dismiss the dialog BEFORE the banner: the Modeler home page can
|
|
@@ -527,6 +549,15 @@ class ModelerHomePage {
|
|
|
527
549
|
await this.clickMessageBanner();
|
|
528
550
|
},
|
|
529
551
|
postAction: async () => {
|
|
552
|
+
// Before anything else: a 5xx from Modeler's own /api/internal/login
|
|
553
|
+
// makes the SPA log itself out, and from then on the reload below only
|
|
554
|
+
// re-renders the Auth0 form -- the tile can never come back, so every
|
|
555
|
+
// remaining attempt burns its visibility timeout for nothing. Recover
|
|
556
|
+
// the session as the current test user and let the next attempt probe
|
|
557
|
+
// the tile again.
|
|
558
|
+
if (await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page)) {
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
530
561
|
// The project tile only renders on the Modeler home page. A failure
|
|
531
562
|
// that leaves the browser inside the project -- createForm's recovery
|
|
532
563
|
// path below reloads and calls straight back in here -- makes the tile
|
package/dist/utils/formatDate.js
CHANGED
|
@@ -5,8 +5,14 @@ function formatDate(date = new Date()) {
|
|
|
5
5
|
const day = date.getDate();
|
|
6
6
|
const year = date.getFullYear();
|
|
7
7
|
const dayPattern = `0?${day}`;
|
|
8
|
+
// September is the one month whose short form differs between locales/ICU
|
|
9
|
+
// builds: en-GB renders "Sept" while Console renders the en-US "Sep". The
|
|
10
|
+
// `=== 'Sep'` test this replaces could never fire, because en-GB had already
|
|
11
|
+
// produced "Sept" -- so every September the expectation was /Sept 0?1, .../
|
|
12
|
+
// against a UI showing "Sep 1", and the assertion failed on calendar date
|
|
13
|
+
// alone. Match on the shared prefix so either rendering is accepted.
|
|
8
14
|
const month = date.toLocaleString('en-GB', { month: 'short' });
|
|
9
|
-
const monthPattern = month
|
|
15
|
+
const monthPattern = month.startsWith('Sep') ? 'Sept?' : month;
|
|
10
16
|
return new RegExp(`${monthPattern} ${dayPattern}, ${year}`);
|
|
11
17
|
}
|
|
12
18
|
exports.formatDate = formatDate;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Page } from '@playwright/test';
|
|
2
|
+
/**
|
|
3
|
+
* Records which user this page is currently signed in as, so a later recovery
|
|
4
|
+
* re-authenticates as that same user instead of falling back to a default.
|
|
5
|
+
* Getting this wrong would silently change what an RBA/permission spec
|
|
6
|
+
* verifies, so recovery is skipped entirely when nothing was recorded.
|
|
7
|
+
*/
|
|
8
|
+
export declare function trackSignedInUser(page: Page, username: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Re-authenticates when the page has been bounced to the identity provider
|
|
11
|
+
* mid-test, and reports whether it had to.
|
|
12
|
+
*
|
|
13
|
+
* Web Modeler drops the session on its own when a backend call fails: a 5xx
|
|
14
|
+
* from `/api/internal/login` (or `/api/internal/self`) makes the SPA call Auth0
|
|
15
|
+
* logout, and from that point every navigation lands on the universal login
|
|
16
|
+
* form. Retry loops that recover by reloading then reload that form until they
|
|
17
|
+
* run out of attempts, which is why a single Modeler auth blip on the
|
|
18
|
+
* 2026-09-01 nightly turned into 26 failures whose only visible symptom was a
|
|
19
|
+
* Modeler locator that "never appeared".
|
|
20
|
+
*
|
|
21
|
+
* Auth0 sends us back to whichever app triggered the logout, so re-entering the
|
|
22
|
+
* credentials on the form returns the browser to that app -- no navigation
|
|
23
|
+
* needed here.
|
|
24
|
+
*/
|
|
25
|
+
export declare function restoreSessionIfLoggedOut(page: Page): Promise<boolean>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.restoreSessionIfLoggedOut = exports.trackSignedInUser = void 0;
|
|
4
|
+
// Only the username is kept -- no password, no cookie, no token. Keyed by the
|
|
5
|
+
// Playwright Page because the object that logs in (LoginPage) and the objects
|
|
6
|
+
// that later have to recover a dropped session (ModelerHomePage) are separate
|
|
7
|
+
// instances, and several specs build their own `new LoginPage(page)` rather
|
|
8
|
+
// than taking the fixture's, so instance-scoped state would silently miss them.
|
|
9
|
+
const signedInUser = new WeakMap();
|
|
10
|
+
/**
|
|
11
|
+
* Records which user this page is currently signed in as, so a later recovery
|
|
12
|
+
* re-authenticates as that same user instead of falling back to a default.
|
|
13
|
+
* Getting this wrong would silently change what an RBA/permission spec
|
|
14
|
+
* verifies, so recovery is skipped entirely when nothing was recorded.
|
|
15
|
+
*/
|
|
16
|
+
function trackSignedInUser(page, username) {
|
|
17
|
+
signedInUser.set(page, username);
|
|
18
|
+
}
|
|
19
|
+
exports.trackSignedInUser = trackSignedInUser;
|
|
20
|
+
// Read back out of the environment at the moment it is needed, from the
|
|
21
|
+
// `C8_USERNAME*` / `C8_PASSWORD*` pair that defines this user -- the same
|
|
22
|
+
// source LoginPage reads its own defaults from. Nothing secret is carried
|
|
23
|
+
// between the login and the recovery.
|
|
24
|
+
function resolvePassword(username) {
|
|
25
|
+
const prefix = 'C8_USERNAME';
|
|
26
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
27
|
+
if (!key.startsWith(prefix) || value !== username) {
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const password = process.env[`C8_PASSWORD${key.slice(prefix.length)}`];
|
|
31
|
+
if (password) {
|
|
32
|
+
return password;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const emailInput = (page) => page
|
|
38
|
+
.getByLabel('Email address')
|
|
39
|
+
.and(page.locator(':not([id="c4-invite-email"])'));
|
|
40
|
+
const passwordInput = (page) => page
|
|
41
|
+
.getByLabel(/^Password\s*\*?$/i)
|
|
42
|
+
.or(page.locator('input[type="password"]'))
|
|
43
|
+
.first();
|
|
44
|
+
const continueButton = (page) => page.getByRole('button', { name: 'Continue', exact: true });
|
|
45
|
+
const passwordHeading = (page) => page.getByRole('heading', { name: 'Enter Your Password' });
|
|
46
|
+
const isVisibleWithin = (locator, timeout) => locator
|
|
47
|
+
.waitFor({ state: 'visible', timeout })
|
|
48
|
+
.then(() => true)
|
|
49
|
+
.catch(() => false);
|
|
50
|
+
// Which identity the password step is about to authenticate. Auth0 carries it
|
|
51
|
+
// in the username field it keeps on that step, and renders it as text as well;
|
|
52
|
+
// read the field first because the text can also contain the app's own address.
|
|
53
|
+
async function offeredIdentity(page) {
|
|
54
|
+
const field = page
|
|
55
|
+
.locator('input[name="username"], input[type="email"]')
|
|
56
|
+
.first();
|
|
57
|
+
const value = await field.inputValue({ timeout: 5000 }).catch(() => undefined);
|
|
58
|
+
if (value?.includes('@')) {
|
|
59
|
+
return value.trim();
|
|
60
|
+
}
|
|
61
|
+
const rendered = await page
|
|
62
|
+
.locator('form')
|
|
63
|
+
.first()
|
|
64
|
+
.innerText({ timeout: 5000 })
|
|
65
|
+
.catch(() => undefined);
|
|
66
|
+
return rendered?.match(/[^\s<>]+@[^\s<>]+\.[^\s<>]+/)?.[0];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Re-authenticates when the page has been bounced to the identity provider
|
|
70
|
+
* mid-test, and reports whether it had to.
|
|
71
|
+
*
|
|
72
|
+
* Web Modeler drops the session on its own when a backend call fails: a 5xx
|
|
73
|
+
* from `/api/internal/login` (or `/api/internal/self`) makes the SPA call Auth0
|
|
74
|
+
* logout, and from that point every navigation lands on the universal login
|
|
75
|
+
* form. Retry loops that recover by reloading then reload that form until they
|
|
76
|
+
* run out of attempts, which is why a single Modeler auth blip on the
|
|
77
|
+
* 2026-09-01 nightly turned into 26 failures whose only visible symptom was a
|
|
78
|
+
* Modeler locator that "never appeared".
|
|
79
|
+
*
|
|
80
|
+
* Auth0 sends us back to whichever app triggered the logout, so re-entering the
|
|
81
|
+
* credentials on the form returns the browser to that app -- no navigation
|
|
82
|
+
* needed here.
|
|
83
|
+
*/
|
|
84
|
+
async function restoreSessionIfLoggedOut(page) {
|
|
85
|
+
const username = signedInUser.get(page);
|
|
86
|
+
if (username === undefined) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
// Cheap first: the identity provider is a different host, so the URL settles
|
|
90
|
+
// this without touching the DOM. Only pages that are NOT obviously on it pay
|
|
91
|
+
// for a probe, and that probe has to be a real wait -- isVisible() does not
|
|
92
|
+
// wait, and the logout redirect is often still in flight when we look.
|
|
93
|
+
const atIdentityProvider = /:\/\/(weblogin|login)\./.test(page.url());
|
|
94
|
+
const probeTimeout = atIdentityProvider ? 30000 : 2000;
|
|
95
|
+
const onEmailStep = await isVisibleWithin(emailInput(page), probeTimeout);
|
|
96
|
+
const onPasswordStep = !onEmailStep && (await isVisibleWithin(passwordHeading(page), 1000));
|
|
97
|
+
if (!onEmailStep && !onPasswordStep) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
const password = resolvePassword(username);
|
|
101
|
+
if (password === undefined) {
|
|
102
|
+
console.warn(`restoreSessionIfLoggedOut: session was dropped at ${page.url()}, but no ` +
|
|
103
|
+
`C8_PASSWORD* variable pairs with ${username} -- not guessing one.`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
// Landing straight on the password step means Auth0 is offering an identity
|
|
107
|
+
// we did not pick, and every SaaS test user shares one password -- so typing
|
|
108
|
+
// it here would authenticate as whoever Auth0 remembered and silently change
|
|
109
|
+
// which user an RBA spec is exercising. Only continue when the form is
|
|
110
|
+
// offering the user this page was actually signed in as. Coming through the
|
|
111
|
+
// email step is safe by construction: we type the username ourselves.
|
|
112
|
+
if (onPasswordStep) {
|
|
113
|
+
const offered = await offeredIdentity(page);
|
|
114
|
+
if (offered !== username) {
|
|
115
|
+
const shown = offered ?? 'an identity it does not disclose';
|
|
116
|
+
console.warn(`restoreSessionIfLoggedOut: identity provider is offering ${shown}, ` +
|
|
117
|
+
`but this page was signed in as ${username} -- refusing to ` +
|
|
118
|
+
're-authenticate.');
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
console.warn(`restoreSessionIfLoggedOut: session was dropped at ${page.url()}, ` +
|
|
123
|
+
`re-authenticating as ${username}`);
|
|
124
|
+
if (onEmailStep) {
|
|
125
|
+
await emailInput(page).fill(username);
|
|
126
|
+
await continueButton(page).click({ timeout: 30000 });
|
|
127
|
+
}
|
|
128
|
+
await passwordInput(page).waitFor({ state: 'visible', timeout: 60000 });
|
|
129
|
+
await passwordInput(page).fill(password);
|
|
130
|
+
await continueButton(page).click({ timeout: 60000 });
|
|
131
|
+
await page.waitForLoadState('domcontentloaded', { timeout: 60000 });
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
exports.restoreSessionIfLoggedOut = restoreSessionIfLoggedOut;
|