@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
- await this.createAuthorizationSubmitButton.click();
109
- await (0, test_1.expect)(this.createAuthorizationModal).not.toBeVisible({
110
- timeout: 30000,
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
- totalTimeout: 240000,
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
@@ -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 === 'Sep' ? 'Sept?' : 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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1056",
3
+ "version": "0.0.1058",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",