@camunda/e2e-test-suite 0.0.1209 → 0.0.1210
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.
|
@@ -39,6 +39,9 @@ declare class ModelerHomePage {
|
|
|
39
39
|
dismissBlockingDialog(): Promise<void>;
|
|
40
40
|
private dismissUncataloguedDialog;
|
|
41
41
|
dismissOverlays(): Promise<void>;
|
|
42
|
+
private consecutiveSessionRecoveries;
|
|
43
|
+
private recoverModelerSession;
|
|
44
|
+
private sessionDropError;
|
|
42
45
|
clickCreateNewProjectButton(): Promise<void>;
|
|
43
46
|
enterNewProjectName(name: string): Promise<void>;
|
|
44
47
|
enterIdpApplicationName(name: string): Promise<void>;
|
|
@@ -65,6 +68,7 @@ declare class ModelerHomePage {
|
|
|
65
68
|
createForm(formName: string): Promise<void>;
|
|
66
69
|
clickCreateIdpApplicationButton(): Promise<void>;
|
|
67
70
|
clickCrossComponentProjectFolder(): Promise<void>;
|
|
71
|
+
private clickCrossComponentProjectFolderWithRetry;
|
|
68
72
|
createCrossComponentProjectFolder(): Promise<void>;
|
|
69
73
|
clickManageButton(retries?: number): Promise<void>;
|
|
70
74
|
clickUploadFilesButton(): Promise<void>;
|
|
@@ -180,6 +180,14 @@ class ModelerHomePage {
|
|
|
180
180
|
// the What's New modal and the migration notice, and closing one reveals the
|
|
181
181
|
// next.
|
|
182
182
|
async dismissBlockingDialog() {
|
|
183
|
+
// Nothing Modeler owns can be open on the identity provider's login form,
|
|
184
|
+
// and the probes below cost ~27s there for certain (a networkidle wait that
|
|
185
|
+
// does not settle plus a 10s label lookup). When Modeler has dropped the
|
|
186
|
+
// session that is the single largest slice of every retry attempt, spent
|
|
187
|
+
// looking for a dialog that cannot exist. Caller recovers the session; this
|
|
188
|
+
// just refuses to pay for the search in the meantime.
|
|
189
|
+
if ((0, sessionRecovery_1.isAtIdentityProvider)(this.page))
|
|
190
|
+
return;
|
|
183
191
|
await this.page
|
|
184
192
|
.waitForLoadState('networkidle', { timeout: 15000 })
|
|
185
193
|
.catch(() => false);
|
|
@@ -244,6 +252,8 @@ class ModelerHomePage {
|
|
|
244
252
|
// `.or(...).first()` unions in DOM order, where the top banner comes first,
|
|
245
253
|
// so on their own they close the banner and leave the dialog open.
|
|
246
254
|
await this.dismissBlockingDialog();
|
|
255
|
+
if ((0, sessionRecovery_1.isAtIdentityProvider)(this.page))
|
|
256
|
+
return;
|
|
247
257
|
for (const locator of [
|
|
248
258
|
this.closeButton,
|
|
249
259
|
this.page.getByRole('button', { name: 'Close' }).first(),
|
|
@@ -255,18 +265,70 @@ class ModelerHomePage {
|
|
|
255
265
|
.catch(() => { });
|
|
256
266
|
}
|
|
257
267
|
}
|
|
268
|
+
// How many times in a row a retry attempt has found the page bounced back to
|
|
269
|
+
// the identity provider. Reset as soon as one finds it still signed in.
|
|
270
|
+
consecutiveSessionRecoveries = 0;
|
|
271
|
+
// Modeler drops the SPA session when one of its own backend calls 5xx's, and
|
|
272
|
+
// the call it drops on -- POST /api/internal/login -- is the very call the
|
|
273
|
+
// next sign-in has to make on the way back in. Auth0 hands the browser back
|
|
274
|
+
// within a couple of seconds, but that call then runs for up to 30s before
|
|
275
|
+
// failing and logging the SPA straight out again, so returning as soon as the
|
|
276
|
+
// credentials are accepted hands the caller a page that is about to bounce --
|
|
277
|
+
// and the attempt is spent probing Modeler UI on the login form.
|
|
278
|
+
//
|
|
279
|
+
// Wait for Modeler's own shell instead. It is the one signal that says the
|
|
280
|
+
// backend accepted the session, it returns the instant the backend comes back
|
|
281
|
+
// (rather than at the next attempt boundary, which is how a recovery that
|
|
282
|
+
// lands mid-outage used to lose the rest of its budget), and when the backend
|
|
283
|
+
// stays down the wait is spent on the thing the test is actually waiting for.
|
|
284
|
+
async recoverModelerSession() {
|
|
285
|
+
if (!(await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page))) {
|
|
286
|
+
this.consecutiveSessionRecoveries = 0;
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const shellVisible = await this.becameVisible(this.modelerPageBanner, 120000);
|
|
290
|
+
if (shellVisible) {
|
|
291
|
+
this.consecutiveSessionRecoveries = 0;
|
|
292
|
+
}
|
|
293
|
+
return shellVisible;
|
|
294
|
+
}
|
|
295
|
+
// Reported instead of the retry helper's "Failed to click locator ..." when
|
|
296
|
+
// the reason the locator never appeared is that Modeler kept logging the
|
|
297
|
+
// session out. The 2026-09-21 nightly dispatched 27 separate "locator never
|
|
298
|
+
// appeared" failures that were one Modeler auth outage; naming the cause here
|
|
299
|
+
// is what makes that legible from the report alone.
|
|
300
|
+
sessionDropError(action, cause) {
|
|
301
|
+
return new Error(`${action}: Modeler dropped the session ` +
|
|
302
|
+
`${this.consecutiveSessionRecoveries} time(s) and re-authentication ` +
|
|
303
|
+
'never got the app back -- its own POST /api/internal/login is ' +
|
|
304
|
+
`failing. Underlying error: ${cause}`);
|
|
305
|
+
}
|
|
258
306
|
async clickCreateNewProjectButton() {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
307
|
+
try {
|
|
308
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.createNewProjectButton, {
|
|
309
|
+
preAction: async () => {
|
|
310
|
+
// Modeler logs the SPA out on its own when a backend call fails, and
|
|
311
|
+
// every later attempt then waits for this button on the Auth0 form
|
|
312
|
+
// instead of on Modeler. Get the session back before looking for it.
|
|
313
|
+
await this.recoverModelerSession();
|
|
314
|
+
await this.dismissOverlays();
|
|
315
|
+
},
|
|
316
|
+
// Raised from 120s. This is the same dropped-session failure the sibling
|
|
317
|
+
// clickCrossComponentProjectFolder budget was twice widened for, and it
|
|
318
|
+
// was carrying a third of that budget: on the 2026-09-21 nightly all
|
|
319
|
+
// three attempts were spent inside the first two minutes of a Modeler
|
|
320
|
+
// auth outage, so the call gave up with the outage still running.
|
|
321
|
+
totalTimeout: 360000,
|
|
322
|
+
maxRetries: 5,
|
|
323
|
+
visibilityTimeout: 10000,
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
if (this.consecutiveSessionRecoveries > 0) {
|
|
328
|
+
throw this.sessionDropError('clickCreateNewProjectButton', error);
|
|
329
|
+
}
|
|
330
|
+
throw error;
|
|
331
|
+
}
|
|
270
332
|
}
|
|
271
333
|
async enterNewProjectName(name) {
|
|
272
334
|
// The inline rename silently drops the value when the editable title field
|
|
@@ -609,6 +671,17 @@ class ModelerHomePage {
|
|
|
609
671
|
await this.createIdpApplicationButton.click();
|
|
610
672
|
}
|
|
611
673
|
async clickCrossComponentProjectFolder() {
|
|
674
|
+
try {
|
|
675
|
+
await this.clickCrossComponentProjectFolderWithRetry();
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
if (this.consecutiveSessionRecoveries > 0) {
|
|
679
|
+
throw this.sessionDropError('clickCrossComponentProjectFolder', error);
|
|
680
|
+
}
|
|
681
|
+
throw error;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async clickCrossComponentProjectFolderWithRetry() {
|
|
612
685
|
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.crossComponentProjectFolder, {
|
|
613
686
|
visibilityTimeout: 60000,
|
|
614
687
|
// Widened from 180s: dismissing the announcement dialog costs up to ~25s
|
|
@@ -621,9 +694,21 @@ class ModelerHomePage {
|
|
|
621
694
|
// attempt costs ~80s instead of ~25s, so the 240s budget ran out after
|
|
622
695
|
// three of the five retries configured below. The 2026-09-01 nightly
|
|
623
696
|
// took two such 5xx to bury a test that had three unused attempts left.
|
|
624
|
-
|
|
625
|
-
|
|
697
|
+
//
|
|
698
|
+
// Widened once more, from 360s. On the 2026-09-21 nightly Modeler's
|
|
699
|
+
// /api/internal/login 5xx'd for ~27 minutes and the retries that stood
|
|
700
|
+
// the best chance -- the ones that started late enough to overlap its
|
|
701
|
+
// recovery -- ran out of budget minutes before it. 480s still leaves room
|
|
702
|
+
// under the 720s test timeout for the rest of a spec's work.
|
|
703
|
+
totalTimeout: 480000,
|
|
704
|
+
maxRetries: 6,
|
|
626
705
|
preAction: async () => {
|
|
706
|
+
// Before the overlay passes, not only after a failed attempt: a dropped
|
|
707
|
+
// session leaves the browser on the Auth0 form, where every dismissal
|
|
708
|
+
// probe below is a guaranteed miss and the tile can never render. The
|
|
709
|
+
// postAction recovery only runs once an attempt has already burnt its
|
|
710
|
+
// 60s visibility timeout there.
|
|
711
|
+
await this.recoverModelerSession();
|
|
627
712
|
// Dismiss the dialog BEFORE the banner: the Modeler home page can
|
|
628
713
|
// stack both at once, and clickMessageBanner resolves an
|
|
629
714
|
// `.or(...).first()` union in DOM order — the top banner comes first,
|
|
@@ -640,7 +725,7 @@ class ModelerHomePage {
|
|
|
640
725
|
// remaining attempt burns its visibility timeout for nothing. Recover
|
|
641
726
|
// the session as the current test user and let the next attempt probe
|
|
642
727
|
// the tile again.
|
|
643
|
-
if (await
|
|
728
|
+
if (await this.recoverModelerSession()) {
|
|
644
729
|
return;
|
|
645
730
|
}
|
|
646
731
|
// The project tile only renders on the Modeler home page. A failure
|
|
@@ -695,6 +780,12 @@ class ModelerHomePage {
|
|
|
695
780
|
await this.uploadFilesButton.click({ timeout: 60000 });
|
|
696
781
|
}
|
|
697
782
|
async clickMessageBanner() {
|
|
783
|
+
// Same reasoning as dismissBlockingDialog: Modeler's banner cannot be on
|
|
784
|
+
// the identity provider's login form, so the 15s probe below is a
|
|
785
|
+
// guaranteed miss there and only shortens the budget left for the session
|
|
786
|
+
// to come back.
|
|
787
|
+
if ((0, sessionRecovery_1.isAtIdentityProvider)(this.page))
|
|
788
|
+
return;
|
|
698
789
|
try {
|
|
699
790
|
const button = this.messageBanner
|
|
700
791
|
.or(this.closeButton)
|
|
@@ -6,6 +6,14 @@ import type { Page } from '@playwright/test';
|
|
|
6
6
|
* verifies, so recovery is skipped entirely when nothing was recorded.
|
|
7
7
|
*/
|
|
8
8
|
export declare function trackSignedInUser(page: Page, username: string): void;
|
|
9
|
+
/**
|
|
10
|
+
* Whether the browser is currently parked on the identity provider rather than
|
|
11
|
+
* on the application. The provider is a different host, so this settles it
|
|
12
|
+
* without touching the DOM -- which is what makes it cheap enough to gate the
|
|
13
|
+
* application-side waits (overlay probes, locator visibility) that cannot
|
|
14
|
+
* possibly succeed there.
|
|
15
|
+
*/
|
|
16
|
+
export declare function isAtIdentityProvider(page: Page): boolean;
|
|
9
17
|
/**
|
|
10
18
|
* Re-authenticates when the page has been bounced to the identity provider
|
|
11
19
|
* mid-test, and reports whether it had to.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.restoreSessionIfLoggedOut = exports.trackSignedInUser = void 0;
|
|
3
|
+
exports.restoreSessionIfLoggedOut = exports.isAtIdentityProvider = exports.trackSignedInUser = void 0;
|
|
4
4
|
// Only the username is kept -- no password, no cookie, no token. Keyed by the
|
|
5
5
|
// Playwright Page because the object that logs in (LoginPage) and the objects
|
|
6
6
|
// that later have to recover a dropped session (ModelerHomePage) are separate
|
|
@@ -43,6 +43,17 @@ const passwordInput = (page) => page
|
|
|
43
43
|
.first();
|
|
44
44
|
const continueButton = (page) => page.getByRole('button', { name: 'Continue', exact: true });
|
|
45
45
|
const passwordHeading = (page) => page.getByRole('heading', { name: 'Enter Your Password' });
|
|
46
|
+
/**
|
|
47
|
+
* Whether the browser is currently parked on the identity provider rather than
|
|
48
|
+
* on the application. The provider is a different host, so this settles it
|
|
49
|
+
* without touching the DOM -- which is what makes it cheap enough to gate the
|
|
50
|
+
* application-side waits (overlay probes, locator visibility) that cannot
|
|
51
|
+
* possibly succeed there.
|
|
52
|
+
*/
|
|
53
|
+
function isAtIdentityProvider(page) {
|
|
54
|
+
return /:\/\/(weblogin|login)\./.test(page.url());
|
|
55
|
+
}
|
|
56
|
+
exports.isAtIdentityProvider = isAtIdentityProvider;
|
|
46
57
|
const isVisibleWithin = (locator, timeout) => locator
|
|
47
58
|
.waitFor({ state: 'visible', timeout })
|
|
48
59
|
.then(() => true)
|
|
@@ -90,8 +101,7 @@ async function restoreSessionIfLoggedOut(page) {
|
|
|
90
101
|
// this without touching the DOM. Only pages that are NOT obviously on it pay
|
|
91
102
|
// for a probe, and that probe has to be a real wait -- isVisible() does not
|
|
92
103
|
// wait, and the logout redirect is often still in flight when we look.
|
|
93
|
-
const
|
|
94
|
-
const probeTimeout = atIdentityProvider ? 30000 : 2000;
|
|
104
|
+
const probeTimeout = isAtIdentityProvider(page) ? 30000 : 2000;
|
|
95
105
|
const onEmailStep = await isVisibleWithin(emailInput(page), probeTimeout);
|
|
96
106
|
const onPasswordStep = !onEmailStep && (await isVisibleWithin(passwordHeading(page), 1000));
|
|
97
107
|
if (!onEmailStep && !onPasswordStep) {
|