@camunda/e2e-test-suite 0.0.944 → 0.0.945

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.
Files changed (38) hide show
  1. package/dist/pages/8.10/AppsPage.d.ts +5 -0
  2. package/dist/pages/8.10/AppsPage.js +152 -0
  3. package/dist/pages/8.10/ModelerHomePage.d.ts +1 -0
  4. package/dist/pages/8.10/ModelerHomePage.js +52 -5
  5. package/dist/pages/8.10/OperateHomePage.js +23 -2
  6. package/dist/pages/8.10/OperateProcessInstancePage.d.ts +17 -3
  7. package/dist/pages/8.10/OperateProcessInstancePage.js +718 -16
  8. package/dist/pages/8.10/OperateProcessesPage.d.ts +1 -0
  9. package/dist/pages/8.10/OperateProcessesPage.js +38 -2
  10. package/dist/pages/8.10/OptimizeHomePage.d.ts +5 -0
  11. package/dist/pages/8.10/OptimizeHomePage.js +138 -0
  12. package/dist/pages/8.10/OptimizeReportPage.d.ts +1 -0
  13. package/dist/pages/8.10/OptimizeReportPage.js +25 -0
  14. package/dist/pages/8.10/TaskPanelPage.d.ts +2 -1
  15. package/dist/pages/8.10/TaskPanelPage.js +37 -9
  16. package/dist/pages/8.10/UtilitiesPage.d.ts +2 -1
  17. package/dist/pages/8.10/UtilitiesPage.js +155 -23
  18. package/dist/pages/SM-8.10/KeycloakAdminPage.js +1 -1
  19. package/dist/pages/SM-8.10/KeycloakLoginPage.d.ts +1 -0
  20. package/dist/pages/SM-8.10/KeycloakLoginPage.js +8 -1
  21. package/dist/pages/SM-8.10/LoginPage.d.ts +1 -1
  22. package/dist/pages/SM-8.10/LoginPage.js +11 -2
  23. package/dist/pages/SM-8.10/NavigationPage.d.ts +1 -0
  24. package/dist/pages/SM-8.10/NavigationPage.js +86 -8
  25. package/dist/pages/SM-8.10/OCIdentityHomePage.js +12 -2
  26. package/dist/pages/SM-8.10/OCIdentityRolesPage.d.ts +3 -0
  27. package/dist/pages/SM-8.10/OCIdentityRolesPage.js +110 -4
  28. package/dist/pages/SM-8.10/OperateHomePage.js +13 -1
  29. package/dist/pages/SM-8.10/OperateProcessesPage.d.ts +1 -0
  30. package/dist/pages/SM-8.10/OperateProcessesPage.js +37 -18
  31. package/dist/pages/SM-8.10/OptimizeReportPage.js +21 -1
  32. package/dist/pages/SM-8.10/TaskDetailsPage.js +7 -0
  33. package/dist/pages/SM-8.10/UtilitiesPage.js +15 -4
  34. package/dist/pages/SM-8.10/optimizeReportUtils.js +30 -4
  35. package/dist/tests/8.10/smoke-tests.spec.js +182 -44
  36. package/dist/utils/constants.d.ts +1 -0
  37. package/dist/utils/constants.js +4 -0
  38. package/package.json +1 -1
@@ -16,6 +16,6 @@ declare class LoginPage {
16
16
  clickUsername(): Promise<void>;
17
17
  fillPassword(password: string): Promise<void>;
18
18
  clickLoginButton(): Promise<void>;
19
- login(username: string, password: string): Promise<void>;
19
+ login(username: string, password: string): Promise<boolean>;
20
20
  }
21
21
  export { LoginPage };
@@ -79,7 +79,11 @@ class LoginPage {
79
79
  }
80
80
  async clickLoginButton() {
81
81
  if (await this.page.locator('button[type="submit"]').isVisible()) {
82
- await this.page.click('button[type="submit"]');
82
+ // The Keycloak submit path was the only click here without an explicit
83
+ // timeout, so it inherited the 10s actionTimeout while page.click waits
84
+ // out the OAuth redirect chain it triggers. Match the 60s budget the
85
+ // sibling branches already use.
86
+ await this.page.click('button[type="submit"]', { timeout: 60000 });
83
87
  }
84
88
  else {
85
89
  await (this.useCore
@@ -87,17 +91,22 @@ class LoginPage {
87
91
  : this.loginButton.click({ timeout: 60000 }));
88
92
  }
89
93
  }
94
+ // Resolves to whether credentials were actually submitted. A missing form is
95
+ // not an error -- the caller may already hold a session, in which case there
96
+ // is nothing to fill -- but the caller has to be able to tell that apart from
97
+ // a submitted form, because only the latter leaves a page transition pending.
90
98
  async login(username, password) {
91
99
  try {
92
100
  await this.detectLoginForm();
93
101
  }
94
102
  catch (error) {
95
- return;
103
+ return false;
96
104
  }
97
105
  await this.clickUsername();
98
106
  await this.fillUsername(username);
99
107
  await this.fillPassword(password);
100
108
  await this.clickLoginButton();
109
+ return true;
101
110
  }
102
111
  }
103
112
  exports.LoginPage = LoginPage;
@@ -10,6 +10,7 @@ declare class NavigationPage {
10
10
  readonly managementIdentityPageBanner: Locator;
11
11
  constructor(page: Page);
12
12
  private isGatewayErrorPage;
13
+ private waitForBanner;
13
14
  goTo(url: string, banner: Locator, sleepTimeout?: number, { username, password, }?: {
14
15
  username?: string;
15
16
  password?: string;
@@ -58,6 +58,52 @@ class NavigationPage {
58
58
  .isVisible()
59
59
  .catch(() => false);
60
60
  }
61
+ // Waits for the app shell's banner, but treats the two states that cannot
62
+ // resolve on their own as a reason to act rather than to keep waiting.
63
+ //
64
+ // The passive `expect(banner).toBeVisible({timeout: 60000})` this replaces
65
+ // could not tell a shell that is merely slow (worth waiting for) from one
66
+ // where nothing is coming at all: `login` swallowed a missing form and
67
+ // submitted nothing, so no transition is pending, or the ingress served a
68
+ // 50x page for the OAuth callback, which never becomes the app shell. Both
69
+ // spend the whole 60s and lose the attempt, and the only surviving evidence
70
+ // is "toBeVisible ... <element(s) not found>" -- repeated once per attempt,
71
+ // that is exactly the reported "Login (/modeler) failed after 8 attempts".
72
+ // The 60s budget is unchanged: a genuinely slow shell still gets all of it,
73
+ // and a sustained outage still fails.
74
+ async waitForBanner(url, banner, submitted) {
75
+ const totalTimeout = constants_1._1_SECOND_IN_MS * 60;
76
+ const deadline = Date.now() + totalTimeout;
77
+ let lastState = 'no banner';
78
+ while (Date.now() < deadline) {
79
+ const round = Math.min(constants_1._1_SECOND_IN_MS * 20, deadline - Date.now());
80
+ try {
81
+ await (0, test_1.expect)(banner).toBeVisible({ timeout: round });
82
+ return;
83
+ }
84
+ catch {
85
+ lastState = `no banner on ${this.page.url()}`;
86
+ }
87
+ if (!submitted) {
88
+ // Nothing was submitted and no banner rendered, so there is no pending
89
+ // transition for the rest of the budget to wait on. Hand back to the
90
+ // caller's retry loop now — it re-navigates and logs in again, which
91
+ // is the only thing that can change this state.
92
+ throw new Error(`${url} rendered no banner and no credentials were submitted: ${lastState}`);
93
+ }
94
+ if (await this.isGatewayErrorPage()) {
95
+ lastState = `a 50x gateway page on ${this.page.url()}`;
96
+ if (Date.now() >= deadline) {
97
+ break;
98
+ }
99
+ await this.page.goto(url, {
100
+ timeout: 60000,
101
+ waitUntil: 'domcontentloaded',
102
+ });
103
+ }
104
+ }
105
+ throw new Error(`${url} did not render its banner within ${totalTimeout}ms: ${lastState}`);
106
+ }
61
107
  async goTo(url, banner, sleepTimeout, { username = IDENTITY_FIRSTUSER_USERNAME, password = IDENTITY_FIRSTUSER_PASSWORD, } = {}, maxRetries = 8) {
62
108
  const startTime = Date.now();
63
109
  let timeout = constants_1._1_SECOND_IN_MS * 10;
@@ -77,8 +123,15 @@ class NavigationPage {
77
123
  // serves an nginx 50x gateway page in place of the app. Reload with a
78
124
  // bounded budget so a short blip self-heals; a sustained outage
79
125
  // exhausts the budget and still surfaces as a failed navigation.
126
+ // The bound is wall clock as well as count: 8 reloads that each wait
127
+ // out the 60s goto budget are ~520s of the 720s test timeout inside a
128
+ // single attempt, which starves the retry loop below of the attempts
129
+ // it needs to ride the outage out.
130
+ const gatewayDeadline = Date.now() + constants_1._1_SECOND_IN_MS * 60;
80
131
  let gatewayReloads = 0;
81
- while (gatewayReloads < 8 && (await this.isGatewayErrorPage())) {
132
+ while (gatewayReloads < 8 &&
133
+ Date.now() < gatewayDeadline &&
134
+ (await this.isGatewayErrorPage())) {
82
135
  await (0, sleep_1.sleep)(constants_1._1_SECOND_IN_MS * 5);
83
136
  await this.page.goto(url, {
84
137
  timeout: 60000,
@@ -100,17 +153,27 @@ class NavigationPage {
100
153
  // authenticated — wait for the real login form to appear.
101
154
  const currentUrl = this.page.url();
102
155
  const onLoginPath = currentUrl.includes('/login') || currentUrl.includes('/auth/');
156
+ let submitted = false;
103
157
  if (isBannerVisible && !onLoginPath) {
104
- const loginVisible = await loginPage.usernameInput.isVisible();
158
+ // Probe with the same #username signal detectLoginForm uses: the
159
+ // KC26 default theme does not expose the 'Username or email' label,
160
+ // so the old locator reported "no form" on a rendered Keycloak login
161
+ // page and left the attempt unauthenticated.
162
+ const loginVisible = await this.page
163
+ .locator('#username')
164
+ .or(loginPage.usernameInput)
165
+ .first()
166
+ .isVisible()
167
+ .catch(() => false);
105
168
  if (loginVisible) {
106
- await loginPage.login(username, password);
169
+ submitted = await loginPage.login(username, password);
107
170
  }
108
171
  // If login selector not visible, banner is real - navigation succeeded
109
172
  }
110
173
  else {
111
- await loginPage.login(username, password);
174
+ submitted = await loginPage.login(username, password);
112
175
  }
113
- await (0, test_1.expect)(banner).toBeVisible({ timeout: 60000 });
176
+ await this.waitForBanner(url, banner, submitted);
114
177
  return;
115
178
  }
116
179
  catch (error) {
@@ -142,8 +205,16 @@ class NavigationPage {
142
205
  }
143
206
  }
144
207
  else {
145
- console.error(`[${now.toISOString()}] (+${elapsed}ms) [NavigationPage] Login (${url}) failed after ${maxRetries} attempts`);
146
- throw new Error(`Login (${url}) failed after ${maxRetries} attempts`);
208
+ console.error(`[${now.toISOString()}] (+${elapsed}ms) [NavigationPage] Login (${url}) failed after ${maxRetries} attempts: ${error}`);
209
+ // Carry the final attempt's error into the thrown message. Without
210
+ // it the only surviving signal is the attempt count, which says
211
+ // nothing about WHY all 8 attempts failed -- a Keycloak 5xx, a
212
+ // gateway page, a missing banner and a closed page all collapse to
213
+ // the same string, so triage of this signature has to guess. Every
214
+ // sibling helper already does this (goToKeycloak below rethrows
215
+ // lastError; KeycloakUtils and pages/8.10/UtilitiesPage interpolate
216
+ // the cause).
217
+ throw new Error(`Login (${url}) failed after ${maxRetries} attempts: ${error}`);
147
218
  }
148
219
  }
149
220
  }
@@ -204,7 +275,14 @@ class NavigationPage {
204
275
  : new Error(`goToKeycloak failed after ${maxRetries} attempts`);
205
276
  }
206
277
  async goToOCAdmin(sleepTimeout, credentials, maxRetries) {
207
- await this.goTo(`${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/admin`, this.identityPageBanner, sleepTimeout, credentials, maxRetries ?? (env_1.isOpenSearch ? 10 : 5));
278
+ await this.goTo(`${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/admin`, this.identityPageBanner, sleepTimeout, credentials,
279
+ // 8, not 5, to match goTo's own default: the comment there records that
280
+ // ~20s of backoff (all 5 attempts buy) was not consistently enough to
281
+ // outlast Keycloak's warm-up, and /admin additionally serves a
282
+ // forbidden redirect while the user's admin authorization propagates.
283
+ // Call sites that wrap goToOCAdmin in their own toPass loop still pass
284
+ // maxRetries explicitly (see document-handling-user-flows).
285
+ maxRetries ?? (env_1.isOpenSearch ? 10 : 8));
208
286
  }
209
287
  async goToClusters(sleepTimeout, credentials) {
210
288
  await this.goTo(`${MODELER_CONTEXT_PATH}/clusters`, this.modelerPageBanner, sleepTimeout, credentials);
@@ -67,8 +67,18 @@ class OCIdentityHomePage {
67
67
  await this.authorizationsTab.click();
68
68
  }
69
69
  async clickRolesTab() {
70
- await (0, test_1.expect)(this.rolesTab).toBeVisible({ timeout: 30000 });
71
- await this.rolesTab.click();
70
+ // Reload between attempts: the Admin shell can render its branding link
71
+ // (which is all goToOCAdmin waits for) before the nav hydrates, and /admin
72
+ // serves a no-permission view while the user's admin authorization is
73
+ // still propagating. A flat wait on the same DOM can recover from neither.
74
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.rolesTab, {
75
+ visibilityTimeout: 30000,
76
+ totalTimeout: 120000,
77
+ postAction: async () => {
78
+ await this.page.reload();
79
+ },
80
+ });
81
+ await this.rolesTab.click({ timeout: 30000 });
72
82
  }
73
83
  async clickGroupsTab() {
74
84
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.groupsTab);
@@ -21,6 +21,7 @@ declare class OCIdentityRolesPage {
21
21
  readonly deleteRoleModalCancelButton: Locator;
22
22
  readonly deleteRoleModalDeleteButton: Locator;
23
23
  readonly adminRole: Locator;
24
+ readonly rolesNavTab: Locator;
24
25
  readonly assignUserButton: Locator;
25
26
  readonly usernameTextBox: Locator;
26
27
  readonly assignUserSubButton: Locator;
@@ -40,6 +41,8 @@ declare class OCIdentityRolesPage {
40
41
  deleteRole(name: string, options?: {
41
42
  throwError?: boolean;
42
43
  }): Promise<void>;
44
+ private isRolesListOpen;
45
+ private openRolesList;
43
46
  clickAdminRole(): Promise<void>;
44
47
  assignUserToRole(userId: string): Promise<void>;
45
48
  clickAssignUserButton(): Promise<void>;
@@ -4,6 +4,7 @@ exports.OCIdentityRolesPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const UtilitiesPage_1 = require("../SM-8.10/UtilitiesPage");
6
6
  const expectLocatorWithPagination_1 = require("../../utils/assertionHelpers/expectLocatorWithPagination");
7
+ const findLocatorWithPagination_1 = require("../../utils/assertionHelpers/findLocatorWithPagination");
7
8
  const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
8
9
  const sleep_1 = require("../../utils/sleep");
9
10
  class OCIdentityRolesPage {
@@ -28,6 +29,7 @@ class OCIdentityRolesPage {
28
29
  deleteRoleModalCancelButton;
29
30
  deleteRoleModalDeleteButton;
30
31
  adminRole;
32
+ rolesNavTab;
31
33
  assignUserButton;
32
34
  usernameTextBox;
33
35
  assignUserSubButton;
@@ -84,6 +86,11 @@ class OCIdentityRolesPage {
84
86
  name: /delete role/i,
85
87
  });
86
88
  this.adminRole = page.getByRole('cell', { name: 'admin', exact: true });
89
+ this.rolesNavTab = page
90
+ .getByRole('banner')
91
+ .locator('a')
92
+ .filter({ hasText: /^Roles$/ })
93
+ .first();
87
94
  this.assignUserButton = page.getByRole('button', { name: 'Assign user' });
88
95
  this.usernameTextBox = page.getByRole('textbox', { name: 'Username' });
89
96
  this.assignUserSubButton = page
@@ -181,8 +188,97 @@ class OCIdentityRolesPage {
181
188
  }
182
189
  }
183
190
  }
191
+ async isRolesListOpen(timeout) {
192
+ return this.createRoleButton
193
+ .first()
194
+ .waitFor({ state: 'visible', timeout })
195
+ .then(() => true)
196
+ .catch(() => false);
197
+ }
198
+ // Re-enters the roles list through the in-app nav rather than reloading.
199
+ // `page.reload()` re-runs the OIDC round-trip and its post-login redirect
200
+ // drops the deep path, so a reload-based retry re-ran against whatever the
201
+ // Admin root rendered: a view with no table at all, or the wrong table.
202
+ // Clicking the banner's Roles link is a client-side route change, so it
203
+ // cannot lose the session; the reload is kept only for the case where the
204
+ // app shell itself is gone.
205
+ async openRolesList(timeout) {
206
+ if (await this.isRolesListOpen(Math.min(timeout, 15000))) {
207
+ return;
208
+ }
209
+ const shellVisible = await this.rolesNavTab
210
+ .waitFor({ state: 'visible', timeout: 15000 })
211
+ .then(() => true)
212
+ .catch(() => false);
213
+ if (!shellVisible) {
214
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
215
+ await (0, test_1.expect)(this.rolesNavTab).toBeVisible({ timeout });
216
+ }
217
+ await this.rolesNavTab.click({ timeout });
218
+ await (0, test_1.expect)(this.createRoleButton.first()).toBeVisible({ timeout });
219
+ }
184
220
  async clickAdminRole() {
185
- await this.adminRole.click();
221
+ // The roles table is fetched after the Roles tab click, and other specs in
222
+ // the nightly add roles that can push `admin` onto a later page, so a bare
223
+ // click had only the 10s actionTimeout to cover both the load and the
224
+ // lookup. Use the same pagination-aware lookup createRole/clickRole use.
225
+ //
226
+ // Bounded overall: this runs in the smoke suite's beforeEach, whose time is
227
+ // charged to the test, and three attempts of the previous per-step budgets
228
+ // could occupy 540s of the 720s test timeout on their own — which is why a
229
+ // stuck recovery here surfaced as a timeout in the test body rather than as
230
+ // this method's own error.
231
+ const deadline = Date.now() + 300000;
232
+ const budget = (max) => Math.max(5000, Math.min(max, deadline - Date.now()));
233
+ const adminCell = this.adminRole.first();
234
+ const maxRetries = 3;
235
+ let lastError;
236
+ for (let attempt = 0; attempt < maxRetries && Date.now() < deadline; attempt++) {
237
+ try {
238
+ // Every attempt starts from a confirmed roles list, so a swallowed nav
239
+ // click or a redirect back to the Admin root is recovered instead of
240
+ // being scanned again.
241
+ await this.openRolesList(budget(60000));
242
+ // Wait for the admin row itself before scanning: the scan gives each
243
+ // page only 5s, so against a table that is still fetching it reports
244
+ // the row missing and — with no next page to walk to — gives up in 5s.
245
+ // That is the shape of the run log's 52 "No more pages available after
246
+ // page 1" against 600 finds on page 1.
247
+ const onCurrentPage = await adminCell
248
+ .waitFor({ state: 'visible', timeout: budget(30000) })
249
+ .then(() => true)
250
+ .catch(() => false);
251
+ if (!onCurrentPage) {
252
+ // Scanned directly rather than through expectLocatorWithPagination:
253
+ // that wrapper hard-codes the scan to 30s (it forwards no options),
254
+ // which is ~5 pages, and the roles table reached 5 pages in the same
255
+ // run. Its retry also reloads, which is the recovery openRolesList
256
+ // exists to avoid.
257
+ const found = await (0, findLocatorWithPagination_1.findLocatorWithPagination)(this.page, adminCell, {
258
+ timeout: budget(90000),
259
+ });
260
+ if (!found) {
261
+ throw new Error('admin role row not found on any page of the roles table');
262
+ }
263
+ }
264
+ await adminCell.click({ timeout: budget(30000) });
265
+ // The row click is the only thing that navigates to the role detail
266
+ // page, and nothing confirmed it landed — a click the table swallowed
267
+ // mid-render left the caller waiting out assignUserButton's whole
268
+ // budget on a page that never had one.
269
+ await (0, test_1.expect)(this.assignUserButton).toBeVisible({
270
+ timeout: budget(30000),
271
+ });
272
+ return;
273
+ }
274
+ catch (error) {
275
+ lastError = error;
276
+ if (attempt < maxRetries - 1) {
277
+ console.warn(`Attempt ${attempt + 1}: admin role detail page did not open, retrying...`);
278
+ }
279
+ }
280
+ }
281
+ throw new Error(`Admin role detail page did not open after ${maxRetries} attempts: ${String(lastError)}`);
186
282
  }
187
283
  async assignUserToRole(userId) {
188
284
  await this.clickAssignUserButton();
@@ -191,16 +287,26 @@ class OCIdentityRolesPage {
191
287
  await this.clickAssignUserSubButton();
192
288
  }
193
289
  async clickAssignUserButton() {
194
- await this.assignUserButton.click();
290
+ // Rendered only once the role detail page has loaded its users tab, which
291
+ // outlasts the 10s actionTimeout a bare click inherits. Same budget the
292
+ // SaaS 8.10 doAssignUserToRole already gives this button.
293
+ await (0, test_1.expect)(this.assignUserButton).toBeVisible({ timeout: 60000 });
294
+ await this.assignUserButton.click({ timeout: 60000 });
195
295
  }
196
296
  async clickUsernameTextbox() {
197
- await this.usernameTextBox.click();
297
+ // Waiting for the textbox is what confirms the modal finished opening.
298
+ await (0, test_1.expect)(this.usernameTextBox).toBeVisible({ timeout: 60000 });
299
+ await this.usernameTextBox.click({ timeout: 60000 });
198
300
  }
199
301
  async fillUsernameTextbox(userId) {
200
302
  await this.usernameTextBox.fill(userId);
201
303
  }
202
304
  async clickAssignUserSubButton() {
203
- await this.assignUserSubButton.click();
305
+ // The modal's submit button stays disabled until the username field
306
+ // validates, so wait for it to be actionable rather than racing the 10s
307
+ // actionTimeout.
308
+ await (0, test_1.expect)(this.assignUserSubButton).toBeEnabled({ timeout: 60000 });
309
+ await this.assignUserSubButton.click({ timeout: 60000 });
204
310
  }
205
311
  async clickClientsTab() {
206
312
  await this.clientsTab.click();
@@ -49,10 +49,22 @@ class OperateHomePage {
49
49
  .getByLabel('Open');
50
50
  }
51
51
  async clickProcessesTab() {
52
+ // Called right after navigating into Operate, a Vite SPA whose nav bar can
53
+ // take longer than clickLocatorWithRetry's default 10s visibilityTimeout to
54
+ // render on a loaded cluster — so all 3 attempts could burn inside 30s and
55
+ // throw while the link was still on its way. Give each attempt the 30s the
56
+ // callers that pre-wait this same link already assume.
52
57
  await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.processesTab, {
53
- totalTimeout: 60000,
58
+ totalTimeout: 120000,
59
+ visibilityTimeout: 30000,
54
60
  });
55
61
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.processPageHeading, {
62
+ // Defaults (10s per attempt, 60s total) gave the Processes panel less
63
+ // budget than the tab click above already assumes it needs on a loaded
64
+ // cluster, so all three attempts could expire while the view was still
65
+ // rendering. Match the 30s/120s the click uses.
66
+ visibilityTimeout: 30000,
67
+ totalTimeout: 120000,
56
68
  preAction: async () => {
57
69
  await (0, test_1.expect)(this.processesTab).toBeVisible({
58
70
  timeout: constants_1._1_SECOND_IN_MS * 4,
@@ -23,6 +23,7 @@ declare class OperateProcessesPage {
23
23
  private toggleActiveCheckboxOn;
24
24
  private toggleCompletedCheckbox;
25
25
  private applyProcessStateFilter;
26
+ private dismissWhatsNewPopUp;
26
27
  private checkTableForProcess;
27
28
  clickProcessActiveCheckbox(): Promise<void>;
28
29
  clickProcessCompletedCheckbox(): Promise<void>;
@@ -98,7 +98,10 @@ class OperateProcessesPage {
98
98
  }
99
99
  async toggleActiveCheckboxOn() {
100
100
  await this.uncheckCompletedCheckbox();
101
- await this.clickProcessIncidentsCheckbox();
101
+ // Set — never toggle — the Incidents filter: applyProcessStateFilter is
102
+ // re-applied after every reload in clickProcessInstanceLink, so a blind
103
+ // flip oscillates the filter between passes and the lookup never converges.
104
+ await this.checkCheckbox(this.processIncidentsCheckbox);
102
105
  await this.processActiveCheckbox.waitFor({
103
106
  state: 'attached',
104
107
  timeout: constants_1._1_SECOND_IN_MS * 10,
@@ -114,7 +117,7 @@ class OperateProcessesPage {
114
117
  }
115
118
  async toggleCompletedCheckbox() {
116
119
  await this.uncheckActiveCheckbox();
117
- await this.clickProcessIncidentsCheckbox();
120
+ await this.checkCheckbox(this.processIncidentsCheckbox);
118
121
  await this.processCompletedCheckbox.waitFor({
119
122
  state: 'attached',
120
123
  timeout: constants_1._1_SECOND_IN_MS * 10,
@@ -137,6 +140,31 @@ class OperateProcessesPage {
137
140
  await this.toggleCompletedCheckbox();
138
141
  }
139
142
  }
143
+ // Operate raises the "Here's what moved in Operate" modal on a first page
144
+ // load, and every test here logs in as a freshly created user, so it is up on
145
+ // the Processes view. Its Carbon overlay swallows clicks on the Processes tab
146
+ // link and on the filter checkbox labels, which is why a reload-recovery pass
147
+ // could make no progress at all. Dismiss it before interacting with the view,
148
+ // not only after an instance page has opened.
149
+ async dismissWhatsNewPopUp() {
150
+ // isVisible() returns immediately — Playwright ignores its `timeout`
151
+ // option — so this probe got none of the 2s grace it reads as having. Right
152
+ // after a reload Operate has usually not mounted the modal yet, the probe
153
+ // reported "not open", and the Carbon overlay then intercepted every click
154
+ // in the recovery loop below until the 720s test timeout. waitFor spends
155
+ // the 2s the probe was written to spend.
156
+ const isOpen = await this.whatsNewPopUp
157
+ .waitFor({ state: 'visible', timeout: constants_1._1_SECOND_IN_MS * 2 })
158
+ .then(() => true)
159
+ .catch(() => false);
160
+ if (!isOpen) {
161
+ return;
162
+ }
163
+ await this.gotItButton.click({ timeout: constants_1._1_SECOND_IN_MS * 10 });
164
+ await (0, test_1.expect)(this.whatsNewPopUp).toBeHidden({
165
+ timeout: constants_1._1_SECOND_IN_MS * 10,
166
+ });
167
+ }
140
168
  async checkTableForProcess(processName) {
141
169
  try {
142
170
  const processTable = this.page.locator('[aria-label="Process Instances Panel"]');
@@ -149,9 +177,7 @@ class OperateProcessesPage {
149
177
  await link.scrollIntoViewIfNeeded();
150
178
  await link.click({ timeout: constants_1._1_SECOND_IN_MS * 2 });
151
179
  await (0, test_1.expect)(this.page.getByText('Instance History').first()).toBeVisible({ timeout: constants_1._1_SECOND_IN_MS * 10 });
152
- if (await this.whatsNewPopUp.isVisible()) {
153
- await this.gotItButton.click();
154
- }
180
+ await this.dismissWhatsNewPopUp();
155
181
  return true;
156
182
  }
157
183
  catch (err) {
@@ -190,19 +216,7 @@ class OperateProcessesPage {
190
216
  timeout: constants_1._1_SECOND_IN_MS * 10,
191
217
  });
192
218
  const wasChecked = await this.processIncidentsCheckbox.isChecked();
193
- if (wasChecked) {
194
- await this.processIncidentsCheckbox.uncheck({
195
- force: true,
196
- timeout: 90000,
197
- });
198
- }
199
- else {
200
- await this.processIncidentsCheckbox.check({ force: true, timeout: 90000 });
201
- }
202
- await (0, test_1.expect)(this.processIncidentsCheckbox).toBeChecked({
203
- checked: !wasChecked,
204
- timeout: constants_1._1_SECOND_IN_MS * 5,
205
- });
219
+ await this.setCheckboxState(this.processIncidentsCheckbox, !wasChecked);
206
220
  }
207
221
  async clickRunningProcessInstancesCheckbox() {
208
222
  await this.processRunningInstancesCheckbox.waitFor({
@@ -240,6 +254,7 @@ class OperateProcessesPage {
240
254
  });
241
255
  }
242
256
  async clickProcessInstanceLink(processName, type = 'active') {
257
+ await this.dismissWhatsNewPopUp();
243
258
  await this.applyProcessStateFilter(type);
244
259
  if (await this.checkTableForProcess(processName)) {
245
260
  return;
@@ -278,6 +293,10 @@ class OperateProcessesPage {
278
293
  .waitForLoadState('networkidle', { timeout: 30000 })
279
294
  .catch(() => console.log('waitForLoadState after Operate navigation timed out, continuing...'));
280
295
  }
296
+ // The reload re-raises the what's-new modal, and its overlay would
297
+ // otherwise intercept both the Processes tab click and the checkbox
298
+ // labels below, so no attempt in this loop could ever converge.
299
+ await this.dismissWhatsNewPopUp();
281
300
  // Operate may land on the Dashboard after reload even when the captured
282
301
  // URL pointed at the Processes view. The filter checkboxes only exist on
283
302
  // the Processes tab, so navigate there explicitly before applying filters.
@@ -67,7 +67,27 @@ class OptimizeReportPage {
67
67
  await this.processSelectionButton.click({ timeout: 180000 });
68
68
  }
69
69
  async clickUserTaskProcess(processName) {
70
- await this.page.getByText(processName, { exact: true }).first().click();
70
+ const processOption = this.page
71
+ .getByText(processName, { exact: true })
72
+ .first();
73
+ const maxRetries = 3;
74
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
75
+ await (0, test_1.expect)(processOption).toBeVisible({ timeout: 30000 });
76
+ await processOption.click();
77
+ // Optimize keeps refetching the definition list while the modal is open.
78
+ // A refetch that lands right after the click remounts the version
79
+ // list-box and drops the selection with it, so Version stays disabled and
80
+ // the caller's toBeEnabled assertion times out. Confirm the selection
81
+ // committed and re-pick when a remount swallowed it. Deliberately no
82
+ // throw here: the caller's assertion stays the gate on this state.
83
+ try {
84
+ await (0, test_1.expect)(this.versionSelection).toBeEnabled({ timeout: 15000 });
85
+ return;
86
+ }
87
+ catch (error) {
88
+ console.warn(`Attempt ${attempt} to select process ${processName} left the version selection disabled, retrying...`, error instanceof Error ? error.message : error);
89
+ }
90
+ }
71
91
  }
72
92
  async clickVersionSelection() {
73
93
  await this.versionSelection.click();
@@ -105,6 +105,13 @@ class TaskDetailsPage {
105
105
  await (0, test_1.expect)(this.assignToMeButton).toBeVisible({ timeout: 60000 });
106
106
  await this.assignToMeButton.click({ timeout: 60000 });
107
107
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.assignedToMeText, {
108
+ // Assignment is a round-trip through the Zeebe user-task backend, which
109
+ // on a loaded cluster outlasts the default 10s per attempt. Every failure
110
+ // here aborts the caller's whole task-completion attempt, so it retries
111
+ // the search, the click and the panel load as well — the run log shows
112
+ // that churn repeating up to attempt 7.
113
+ visibilityTimeout: 30000,
114
+ totalTimeout: 60000,
108
115
  postAction: async () => {
109
116
  await this.page.reload();
110
117
  },
@@ -82,7 +82,12 @@ async function completeTaskWithRetry(page, taskPanelPage, taskDetailsPage, taskN
82
82
  }
83
83
  for (let attempt = 0; attempt < maxRetries; attempt++) {
84
84
  try {
85
- await page.waitForLoadState('networkidle');
85
+ // Tasklist keeps polling for task updates, so 'networkidle' never
86
+ // settles here. This wait carried no timeout, so it consumed whatever
87
+ // was left of the 12-minute test budget and the run died inside
88
+ // waitForLoadState. The task panel rendering is the precondition this
89
+ // step actually needs, so wait for that instead.
90
+ await (0, test_1.expect)(taskPanelPage.availableTasks).toBeVisible({ timeout: 60000 });
86
91
  const taskLocator = taskPanelPage.availableTasks
87
92
  .getByText(taskName, { exact: true })
88
93
  .first();
@@ -150,7 +155,8 @@ async function completeTaskWithRetry(page, taskPanelPage, taskDetailsPage, taskN
150
155
  catch {
151
156
  await page.reload();
152
157
  }
153
- await page.waitForLoadState('networkidle');
158
+ // No settle wait here either — the goto already waited for
159
+ // domcontentloaded and the next attempt waits for the task panel.
154
160
  }
155
161
  else {
156
162
  console.error(`All ${maxRetries} attempts failed for task ${taskName}`);
@@ -343,7 +349,12 @@ async function assertLocatorVisibleWithRetry(page, locator, text, timeout = 3000
343
349
  console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
344
350
  }
345
351
  else {
346
- throw new Error(`Assertion failed after ${maxRetries} attempts`);
352
+ // Keep the cause. Callers branch on the original error to tell a real
353
+ // failure apart from the test timeout aborting an in-flight wait (see
354
+ // assertReportWithRefreshes), and a bare attempt count told them
355
+ // nothing — so they retried into an already-dead test budget and
356
+ // reported their own generic "after N attempts" instead.
357
+ throw new Error(`Assertion failed after ${maxRetries} attempts: ${String(error)}`);
347
358
  }
348
359
  }
349
360
  }
@@ -371,7 +382,7 @@ async function assertPageTextWithRetry(page, text, notVisible, timeout = 30000,
371
382
  console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
372
383
  }
373
384
  else {
374
- throw new Error(`Assertion failed after ${maxRetries} attempts`);
385
+ throw new Error(`Assertion failed after ${maxRetries} attempts: ${String(error)}`);
375
386
  }
376
387
  }
377
388
  }