@camunda/e2e-test-suite 0.0.1208 → 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
- await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.createNewProjectButton, {
260
- preAction: async () => {
261
- // Modeler logs the SPA out on its own when a backend call fails, and
262
- // every later attempt then waits for this button on the Auth0 form
263
- // instead of on Modeler. Get the session back before looking for it.
264
- await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page);
265
- await this.dismissOverlays();
266
- },
267
- totalTimeout: 120000,
268
- visibilityTimeout: 10000,
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
- totalTimeout: 360000,
625
- maxRetries: 5,
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 (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page)) {
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)
@@ -59,7 +59,7 @@ SM_8_9_1.test.describe('MCP Gateway - Cluster Tools @tasklistV2', () => {
59
59
  await operateHomePage.clickProcessesTab();
60
60
  await operateProcessesPage.clickProcessCompletedCheckbox();
61
61
  await operateProcessesPage.applyMoreFilters('Process Instance Key(s)', String(instanceKey));
62
- await operateProcessesPage.clickProcessInstanceLink('MCP Cluster tools');
62
+ await operateProcessesPage.clickProcessInstanceLink('MCP Cluster tools', 'completed');
63
63
  await operateProcessInstancePage.assertProcessCompleteStatusWithRetry();
64
64
  });
65
65
  for (const assertion of testCase.assertions) {
@@ -88,7 +88,7 @@ SM_8_9_1.test.describe('MCP Gateway - Process Definition Tools @tasklistV2', ()
88
88
  await operateHomePage.clickProcessesTab();
89
89
  await operateProcessesPage.clickProcessCompletedCheckbox();
90
90
  await operateProcessesPage.applyMoreFilters('Process Instance Key(s)', String(toolsInstanceKey));
91
- await operateProcessesPage.clickProcessInstanceLink('MCP Process Definition Tools');
91
+ await operateProcessesPage.clickProcessInstanceLink('MCP Process Definition Tools', 'completed');
92
92
  await operateProcessInstancePage.assertProcessCompleteStatusWithRetry();
93
93
  });
94
94
  for (const assertion of testCase.assertions) {
@@ -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 atIdentityProvider = /:\/\/(weblogin|login)\./.test(page.url());
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1208",
3
+ "version": "0.0.1210",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",