@camunda/e2e-test-suite 0.0.1276 → 0.0.1278

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.
@@ -21,8 +21,12 @@ declare class AppsPage {
21
21
  readonly hubSidebar: Locator;
22
22
  constructor(page: Page);
23
23
  clickCluster(component: Locator, name: string): Promise<void>;
24
+ private expandHubClusterGroup;
25
+ private showAllWorkspaces;
24
26
  private clickHubClusterApp;
27
+ private isOnHub;
25
28
  clickModeler(): Promise<void>;
29
+ private clickHubModelerHome;
26
30
  /**
27
31
  * Waits for the Modeler app to actually render, and recovers the one landing
28
32
  * state a hostname check cannot see: Modeler's OAuth `/login-callback`
@@ -4,6 +4,9 @@ exports.AppsPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const UtilitiesPage_1 = require("../8.8/UtilitiesPage");
7
+ function escapeRegExp(value) {
8
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+ }
7
10
  class AppsPage {
8
11
  page;
9
12
  modelerLink;
@@ -85,6 +88,165 @@ class AppsPage {
85
88
  // up the app link sidebar-wide rather than scoped to a "group" container
86
89
  // that doesn't reliably exist. Ported from pages/8.9/AppsPage.ts, where
87
90
  // this shape is already nightly-proven.
91
+ async expandHubClusterGroup(clusterName) {
92
+ const toggle = this.hubSidebar
93
+ .getByRole('button', { name: clusterName })
94
+ .first();
95
+ // The diagram editor (and other full-screen views reached from it, e.g.
96
+ // right after runProcessInstance/deployDiagram) has no "Main navigation"
97
+ // sidebar at all -- confirmed from a nightly trace's DOM snapshot: the
98
+ // page's only <nav> is aria-label="Context navigation" (the breadcrumb),
99
+ // and neither "Operate"/"Tasklist"/"Optimize" nor any app-switcher text
100
+ // appears anywhere on it. clickCamundaApps() is a no-op on Hub (there is
101
+ // no switcher panel to open), so a caller reaching clickOperate/
102
+ // clickTasklist/clickOptimize straight from a diagram was left on a page
103
+ // this sidebar could never resolve on, no matter how the toggle itself
104
+ // is matched. Navigate home first, the same way clickModeler() does,
105
+ // whenever the sidebar isn't already there.
106
+ //
107
+ // A freshly re-authenticated page can also land on a workspace-agnostic
108
+ // route (e.g. bare /workspaces right after loginWithTestUser(), which
109
+ // clickCamundaApps() does nothing to fix -- it is a no-op on Hub) whose
110
+ // sidebar renders but has not resolved this cluster's own group yet.
111
+ // Confirmed from a nightly trace: both the getAttribute and click below
112
+ // timed out (10s each) against this exact toggle while the URL sat on
113
+ // /workspaces, right after a mid-test re-login. Route through the same
114
+ // clickModeler() recovery whenever the toggle itself hasn't shown up
115
+ // either, not only when the whole sidebar is missing.
116
+ const sidebarReady = (await this.hubSidebar.isVisible({ timeout: 5000 }).catch(() => false)) &&
117
+ (await toggle
118
+ .waitFor({ state: 'visible', timeout: 10000 })
119
+ .then(() => true)
120
+ .catch(() => false));
121
+ if (!sidebarReady) {
122
+ await this.clickModeler();
123
+ await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
124
+ // Hub's Workspaces overview defaults to "My workspaces" -- only the
125
+ // ones the current user is an EXPLICIT member of -- and resets to it on
126
+ // every load (camunda-hub #28956). That default is what a real user
127
+ // sees, so leave it alone and widen only when this cluster's group is
128
+ // genuinely not in it: the shared workspace is created by whichever
129
+ // setup user won the race, so a role-flow user otherwise lands on "You
130
+ // don't have explicit membership in any workspace in this
131
+ // organization" and the group never renders.
132
+ const groupUnderDefaultScope = await toggle
133
+ .waitFor({ state: 'visible', timeout: 10000 })
134
+ .then(() => true)
135
+ .catch(() => false);
136
+ if (!groupUnderDefaultScope) {
137
+ await this.showAllWorkspaces(5000);
138
+ }
139
+ await (0, test_1.expect)(toggle).toBeVisible({ timeout: 30000 });
140
+ }
141
+ // camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
142
+ // shape: "the design system splits the group row into a label control
143
+ // and a chevron control, both labelled by the cluster name; either one
144
+ // opens it" -- there is no separate "Toggle <name>" control, and the
145
+ // child links do not exist in the DOM at all until expanded (not merely
146
+ // hidden), nor are they nested under any container matchable by cluster
147
+ // name once expanded. So: find the toggle by the cluster's own name
148
+ // (`.first()` because both controls share that name), click it, then
149
+ // look up the app link sidebar-wide rather than scoped to a "group"
150
+ // container that doesn't reliably exist.
151
+ // Check THIS cluster's own toggle state, not "is any app link with this
152
+ // name visible anywhere in the sidebar" -- with more than one cluster in
153
+ // the org, a DIFFERENT cluster's group can already be expanded (left over
154
+ // from an earlier action on the same page), and that cluster's own
155
+ // Operate/Tasklist/Optimize link would satisfy an unscoped visibility
156
+ // check, skipping expansion of the requested cluster entirely and then
157
+ // clicking the wrong cluster's link. The child links are not nested under
158
+ // any container matchable by cluster name once expanded (confirmed
159
+ // against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
160
+ // own aria-expanded is the only reliable per-cluster signal available.
161
+ // Not exact: the same suite asserts this row with a substring/regex
162
+ // match (`{name: /Production/}`), not an exact string -- and nightly
163
+ // traces confirm why: the rendered toggle's accessible name is not
164
+ // byte-for-byte the bare cluster name (it carries additional content,
165
+ // e.g. a trailing tag summary), so an exact-match lookup here finds
166
+ // nothing and the click below times out waiting on a locator that can
167
+ // never resolve.
168
+ const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
169
+ if (!alreadyExpanded) {
170
+ // This is the only expansion action in the Hub path -- swallowing its
171
+ // failure made a broken expansion look successful and left the caller
172
+ // to burn the full 30s below waiting on an app link that could never
173
+ // appear, hiding the actionable error (e.g. a toggle that doesn't
174
+ // exist or isn't clickable) behind an unrelated timeout.
175
+ await toggle.click({ timeout: 10000 });
176
+ }
177
+ // An app link resolved after this is sidebar-wide (see callers below), so
178
+ // if an EARLIER call in the same test left a different cluster's group
179
+ // expanded too, two "Operate" (say) links now exist and that becomes a
180
+ // strict-mode violation. Collapse every other expanded group before
181
+ // resolving it -- there is no container to scope the link to instead,
182
+ // since the child links aren't nested under anything matchable by
183
+ // cluster name once expanded.
184
+ // hasNotText filters DOM text content, not the accessible name -- the
185
+ // expanded chevron can be labelled via aria-label with no text content
186
+ // of its own, in which case hasNotText would fail to exclude the toggle
187
+ // we just expanded and this loop would immediately collapse it again.
188
+ // Match by accessible name (same algorithm getByRole's own name option
189
+ // uses) instead, excluding any name CONTAINING clusterName -- not just
190
+ // one exactly equal to it, since (per the toggle locator above) the
191
+ // current cluster's own toggle name is not exactly clusterName either,
192
+ // and an exact-equality exclusion would wrongly treat it as "other" and
193
+ // immediately re-collapse the group this method just expanded.
194
+ const otherExpandedToggles = this.hubSidebar.getByRole('button', {
195
+ expanded: true,
196
+ name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
197
+ });
198
+ for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
199
+ // Let a failed collapse throw rather than swallowing it: the app link
200
+ // is intentionally sidebar-wide, so a group left expanded here produces
201
+ // a strict-mode violation (or the wrong cluster's link) below anyway --
202
+ // failing at the actual cause is more useful than at that symptom.
203
+ await otherExpandedToggles.first().click({ timeout: 10000 });
204
+ }
205
+ }
206
+ // Widens Hub's Workspaces scope filter to "All workspaces". Duplicated from
207
+ // ModelerHomePage.showAllWorkspaces() rather than imported: ModelerHomePage
208
+ // constructs page objects of its own, and pulling it in here would make the
209
+ // Apps -> Modeler navigation path circular. The control is absent outside
210
+ // Hub and for non-elevated users, where a membership-scoped list is already
211
+ // correct, so a missing control is a no-op.
212
+ async showAllWorkspaces(probeTimeout) {
213
+ // The empty state offers an explicit "Switch to All workspaces" button
214
+ // next to "You don't have explicit membership in any workspace in this
215
+ // organization" -- prefer it. Driving the combobox alone was not enough:
216
+ // when its accessible name does not match, the probe below simply
217
+ // returns and the caller waits out its whole timeout on a page that is
218
+ // still scoped to "My workspaces" (observed live, twice).
219
+ const switchButton = this.page.getByRole('button', {
220
+ name: 'Switch to All workspaces',
221
+ });
222
+ const scopeFilter = this.page.getByRole('combobox', {
223
+ name: 'Filter workspaces',
224
+ });
225
+ const appeared = await switchButton
226
+ .or(scopeFilter)
227
+ .first()
228
+ .waitFor({ state: 'visible', timeout: probeTimeout })
229
+ .then(() => true)
230
+ .catch(() => false);
231
+ if (!appeared) {
232
+ return;
233
+ }
234
+ if (await switchButton.isVisible().catch(() => false)) {
235
+ await switchButton.click({ timeout: 10000 });
236
+ await (0, test_1.expect)(switchButton).toBeHidden({ timeout: 10000 });
237
+ return;
238
+ }
239
+ if ((await scopeFilter.innerText()).includes('All workspaces')) {
240
+ return;
241
+ }
242
+ await scopeFilter.click({ timeout: 10000 });
243
+ await this.page
244
+ .getByRole('option', { name: 'All workspaces' })
245
+ .click({ timeout: 10000 });
246
+ await (0, test_1.expect)(scopeFilter).toContainText('All workspaces', {
247
+ timeout: 10000,
248
+ });
249
+ }
88
250
  async clickHubClusterApp(clusterName, appName) {
89
251
  // When the sidebar is collapsed (the default on a fresh session), the
90
252
  // group toggle opens a Radix popover -- a role="dialog" labelled with the
@@ -104,12 +266,12 @@ class AppsPage {
104
266
  // multi-cluster org another cluster's group can already be expanded with
105
267
  // the same app name, and the click would then fire on the wrong
106
268
  // cluster's link.
107
- const collapsedToggle = this.hubSidebar
108
- .getByRole('button', { name: clusterName, expanded: false })
109
- .first();
110
- if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
111
- await collapsedToggle.click({ timeout: 10000 });
112
- }
269
+ // Expansion, sidebar recovery and collapsing every OTHER expanded group
270
+ // all live in the ported helper: with more than one cluster in the org
271
+ // (this suite creates Test Cluster and AWS Cluster), a sidebar-wide app
272
+ // link can otherwise resolve to a different cluster's row, or become a
273
+ // strict-mode violation once two groups are open.
274
+ await this.expandHubClusterGroup(clusterName);
113
275
  await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
114
276
  // Hub's cluster-app links carry target="_blank", so clicking them opens
115
277
  // the app in a new tab while every caller keeps asserting on this.page.
@@ -122,6 +284,11 @@ class AppsPage {
122
284
  }
123
285
  await appLink.click({ timeout: 10000 });
124
286
  }
287
+ // True once the tab is on a Camunda Hub host, where Modeler home is Hub's
288
+ // own home rather than a modeler.* hostname.
289
+ isOnHub() {
290
+ return new URL(this.page.url()).hostname.startsWith('hub.');
291
+ }
125
292
  async clickModeler() {
126
293
  // Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
127
294
  // app switcher. Its home is the Modeler/Workspaces home, but a hostname
@@ -132,22 +299,7 @@ class AppsPage {
132
299
  // Navigate home through the header logo link instead, same as
133
300
  // ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
134
301
  // Modeler home is a harmless in-SPA no-op.
135
- if (new URL(this.page.url()).hostname.startsWith('hub.')) {
136
- const header = this.page.locator('header[data-slot="app-header"]');
137
- // The href stays literally "/login" until the client's auth state
138
- // catches up just after login (camunda-hub NavShellDs.common.tsx) --
139
- // clicking it in that window would log the test out instead of
140
- // navigating home, so exclude that href and wait for the authenticated
141
- // one rather than racing it.
142
- const homeLink = header
143
- .getByRole('link', { name: 'Camunda Modeler home' })
144
- .and(this.page.locator(':not([href$="/login"])'))
145
- .or(header
146
- .getByRole('link', { name: 'Camunda Hub home' })
147
- .and(this.page.locator(':not([href$="/login"])')))
148
- .first();
149
- await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
150
- await homeLink.click({ timeout: 30000 });
302
+ if (await this.clickHubModelerHome()) {
151
303
  return;
152
304
  }
153
305
  const maxRetries = 5;
@@ -160,6 +312,17 @@ class AppsPage {
160
312
  }
161
313
  for (let retries = 0; retries < maxRetries; retries++) {
162
314
  try {
315
+ // Re-checked every iteration, not once before the loop. A caller can
316
+ // reach here from a cluster app (Operate/Tasklist/Identity), which is
317
+ // NOT a hub.* host, so the pre-loop check above takes the legacy
318
+ // branch -- but the reload in the catch below lands back on Hub, and
319
+ // the loop's own success signal (a modeler.* hostname) can never come
320
+ // true there. A nightly trace shows exactly that: every retry
321
+ // snapshot sits on hub.ultrawombat.com/workspaces, which IS Modeler
322
+ // home, while the loop kept failing until it ran out of attempts.
323
+ if (await this.clickHubModelerHome()) {
324
+ return;
325
+ }
163
326
  await this.clickCamundaApps();
164
327
  await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
165
328
  await this.modelerLink.click({ timeout: 30000 });
@@ -194,6 +357,30 @@ class AppsPage {
194
357
  }
195
358
  throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
196
359
  }
360
+ // Navigates to Hub's Modeler home through the header logo link. Returns
361
+ // false when this tab is not on a Hub host, so the caller falls through to
362
+ // the legacy Console app-switcher flow.
363
+ async clickHubModelerHome() {
364
+ if (!this.isOnHub()) {
365
+ return false;
366
+ }
367
+ const header = this.page.locator('header[data-slot="app-header"]');
368
+ // The href stays literally "/login" until the client's auth state
369
+ // catches up just after login (camunda-hub NavShellDs.common.tsx) --
370
+ // clicking it in that window would log the test out instead of
371
+ // navigating home, so exclude that href and wait for the authenticated
372
+ // one rather than racing it.
373
+ const homeLink = header
374
+ .getByRole('link', { name: 'Camunda Modeler home' })
375
+ .and(this.page.locator(':not([href$="/login"])'))
376
+ .or(header
377
+ .getByRole('link', { name: 'Camunda Hub home' })
378
+ .and(this.page.locator(':not([href$="/login"])')))
379
+ .first();
380
+ await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
381
+ await homeLink.click({ timeout: 30000 });
382
+ return true;
383
+ }
197
384
  /**
198
385
  * Waits for the Modeler app to actually render, and recovers the one landing
199
386
  * state a hostname check cannot see: Modeler's OAuth `/login-callback`
@@ -22,8 +22,13 @@ declare class ClusterDetailsPage {
22
22
  readonly clientCredentialsDialog: Locator;
23
23
  readonly clustersLink: Locator;
24
24
  readonly alertsTab: Locator;
25
+ readonly alertsPanel: Locator;
25
26
  readonly activeTabPanel: Locator;
26
27
  readonly alertsList: Locator;
28
+ readonly alertExpandRowButtons: Locator;
29
+ readonly alertRowActionsButtons: Locator;
30
+ readonly deleteAlertMenuItem: Locator;
31
+ readonly deleteAlertConfirmButton: Locator;
27
32
  readonly createFirstAlertButton: Locator;
28
33
  readonly createNewAlertButton: Locator;
29
34
  readonly emailRadioButton: Locator;
@@ -37,6 +42,7 @@ declare class ClusterDetailsPage {
37
42
  readonly expandButton: Locator;
38
43
  readonly rbaEnabledMessage: Locator;
39
44
  readonly authorizationsHeading: Locator;
45
+ readonly authorizationsSwitch: Locator;
40
46
  readonly orchestrationClusterCheckbox: Locator;
41
47
  readonly clientCredentialsLink: (clientCredentials: string) => Locator;
42
48
  readonly clientRow: (name: string) => Locator;
@@ -64,6 +70,7 @@ declare class ClusterDetailsPage {
64
70
  clickCloseModalButton(): Promise<void>;
65
71
  clickSettingsTab(): Promise<void>;
66
72
  private waitForAuthorizationsSection;
73
+ private setAuthorizationsViaSwitch;
67
74
  enableAuthorizations(): Promise<void>;
68
75
  disableAuthorizations(): Promise<void>;
69
76
  searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
@@ -76,6 +83,8 @@ declare class ClusterDetailsPage {
76
83
  selectEmailRadioButton(): Promise<void>;
77
84
  clickCreateAlertButton(): Promise<void>;
78
85
  deleteAlerts(): Promise<void>;
86
+ ensureAlertsTabSelected(): Promise<void>;
87
+ expandAlerts(): Promise<void>;
79
88
  private confirmDelete;
80
89
  private dismissDeleteDialog;
81
90
  private doDelete;
@@ -28,8 +28,13 @@ class ClusterDetailsPage {
28
28
  clientCredentialsDialog;
29
29
  clustersLink;
30
30
  alertsTab;
31
+ alertsPanel;
31
32
  activeTabPanel;
32
33
  alertsList;
34
+ alertExpandRowButtons;
35
+ alertRowActionsButtons;
36
+ deleteAlertMenuItem;
37
+ deleteAlertConfirmButton;
33
38
  createFirstAlertButton;
34
39
  createNewAlertButton;
35
40
  emailRadioButton;
@@ -43,6 +48,7 @@ class ClusterDetailsPage {
43
48
  expandButton;
44
49
  rbaEnabledMessage;
45
50
  authorizationsHeading;
51
+ authorizationsSwitch;
46
52
  orchestrationClusterCheckbox;
47
53
  clientCredentialsLink;
48
54
  clientRow;
@@ -123,12 +129,39 @@ class ClusterDetailsPage {
123
129
  this.alertsList = this.activeTabPanel
124
130
  .getByRole('row')
125
131
  .filter({ hasNotText: 'Delivery method' }); //Filter out header row
132
+ // Hub's Alerts tab is a design-system data table: one summary row per
133
+ // alert, its detail fields in a sibling expansion row that only exists
134
+ // once the row's "Expand row" toggle is clicked, and Delete behind the
135
+ // row's "Row actions" kebab -> "Delete" -> "Delete alert" dialog. A
136
+ // nightly trace's DOM carries aria-label="Row actions" and no row-level
137
+ // "Delete" button at all, which is why doDelete() below saw "no items"
138
+ // and silently deleted nothing. Same shape as pages/8.9, scoped to the
139
+ // visible tab panel.
140
+ this.alertExpandRowButtons = this.activeTabPanel.getByRole('button', {
141
+ name: 'Expand row',
142
+ exact: true,
143
+ });
144
+ this.alertRowActionsButtons = this.activeTabPanel.getByRole('button', {
145
+ name: 'Row actions',
146
+ exact: true,
147
+ });
148
+ this.deleteAlertMenuItem = page.getByRole('menuitem', {
149
+ name: 'Delete',
150
+ exact: true,
151
+ });
152
+ this.deleteAlertConfirmButton = page
153
+ .getByRole('dialog', { name: 'Delete alert' })
154
+ .getByRole('button', { name: 'Delete', exact: true });
126
155
  this.createFirstAlertButton = page.getByRole('button', {
127
156
  name: 'Create an alert',
128
157
  });
129
158
  this.createNewAlertButton = page.getByRole('button', {
130
159
  name: 'Create new alert',
131
160
  });
161
+ // The Alerts panel offers "Create an alert" while empty and "Create new
162
+ // alert" once it holds one, so either proves the panel itself mounted.
163
+ // Ported from pages/8.9.
164
+ this.alertsPanel = this.createFirstAlertButton.or(this.createNewAlertButton);
132
165
  this.emailRadioButton = page.getByRole('radio', {
133
166
  name: 'Email',
134
167
  });
@@ -155,6 +188,14 @@ class ClusterDetailsPage {
155
188
  });
156
189
  this.expandButton = page.locator('button[class="cds--table-expand__button"]');
157
190
  this.rbaEnabledMessage = page.getByText('Authorizations enabled');
191
+ // Hub's design-system Settings tab renders this as
192
+ // <button role="switch" data-slot="switch" aria-label="Switch Enable
193
+ // authorizations" aria-checked="true|false">, confirmed from a live
194
+ // trace. The Carbon `.cds--toggle__*` nodes the old code indexed into do
195
+ // not exist there at all.
196
+ this.authorizationsSwitch = this.page.getByRole('switch', {
197
+ name: /Enable authorizations/i,
198
+ });
158
199
  this.authorizationsHeading = this.page.getByRole('heading', {
159
200
  name: 'Authorizations',
160
201
  });
@@ -280,9 +321,34 @@ class ClusterDetailsPage {
280
321
  },
281
322
  });
282
323
  }
324
+ // Drives the design-system Authorizations switch. Returns false when this
325
+ // shell does not render one, so the caller can fall through to the legacy
326
+ // Carbon toggle.
327
+ async setAuthorizationsViaSwitch(enabled) {
328
+ if (!(await this.authorizationsSwitch.isVisible().catch(() => false))) {
329
+ return false;
330
+ }
331
+ const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
332
+ if (isOn === enabled) {
333
+ return true;
334
+ }
335
+ await this.authorizationsSwitch.click({ timeout: 30000 });
336
+ if (await this.dialog.isVisible({ timeout: 15000 }).catch(() => false)) {
337
+ const confirm = enabled ? this.rbaEnableButton : this.rbaDisableButton;
338
+ if (await confirm.isVisible().catch(() => false)) {
339
+ await confirm.click({ timeout: 30000 });
340
+ }
341
+ await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
342
+ }
343
+ await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
344
+ return true;
345
+ }
283
346
  async enableAuthorizations() {
284
347
  await (0, sleep_1.sleep)(5000);
285
348
  await this.waitForAuthorizationsSection();
349
+ if (await this.setAuthorizationsViaSwitch(true)) {
350
+ return;
351
+ }
286
352
  // Locate all elements with class .cds--toggle__text
287
353
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
288
354
  // Check if toggleTextElements has at least 2 elements
@@ -316,11 +382,22 @@ class ClusterDetailsPage {
316
382
  }
317
383
  }
318
384
  else {
319
- console.log('Toggle text elements not found or less than 2.');
385
+ // Previously a console.log that returned as though the work were done.
386
+ // Under Hub neither `.cds--toggle__text` nor `.cds--toggle__switch`
387
+ // exists, so this branch was taken on every run: authorizations were
388
+ // never enabled, the cluster kept its "Authorizations disabled" badge,
389
+ // and nothing downstream noticed until a permission assertion failed
390
+ // somewhere far away.
391
+ throw new Error('Could not enable authorizations: the Settings tab rendered neither ' +
392
+ 'the design-system "Enable authorizations" switch nor a Carbon ' +
393
+ 'toggle pair.');
320
394
  }
321
395
  }
322
396
  async disableAuthorizations() {
323
397
  await this.waitForAuthorizationsSection();
398
+ if (await this.setAuthorizationsViaSwitch(false)) {
399
+ return;
400
+ }
324
401
  // Locate all elements with class .cds--toggle__text
325
402
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
326
403
  // Check if toggleTextElements has at least 2 elements
@@ -354,7 +431,11 @@ class ClusterDetailsPage {
354
431
  }
355
432
  }
356
433
  else {
357
- console.log('Toggle text elements not found or less than 2.');
434
+ // Same silent no-op as enableAuthorizations had: under Hub no Carbon
435
+ // toggle exists, so this returned as though it had disabled them.
436
+ throw new Error('Could not disable authorizations: the Settings tab rendered neither ' +
437
+ 'the design-system "Enable authorizations" switch nor a Carbon ' +
438
+ 'toggle pair.');
358
439
  }
359
440
  }
360
441
  async searchAndClickClientCredentialsLink(name) {
@@ -448,7 +529,65 @@ class ClusterDetailsPage {
448
529
  });
449
530
  }
450
531
  async deleteAlerts() {
451
- await this.doDelete(this.dialog, 'Alerts');
532
+ const hasRowActions = await this.alertRowActionsButtons
533
+ .first()
534
+ .waitFor({ state: 'visible', timeout: 10000 })
535
+ .then(() => true)
536
+ .catch(() => false);
537
+ if (!hasRowActions) {
538
+ await this.doDelete(this.dialog, 'Alerts');
539
+ return;
540
+ }
541
+ let remaining = await this.alertRowActionsButtons.count();
542
+ while (remaining > 0) {
543
+ // A re-render can detach the menu between opening it and the click, so
544
+ // open-menu-and-choose-Delete is retried as one unit.
545
+ await (0, test_1.expect)(async () => {
546
+ await this.alertRowActionsButtons.first().click({ timeout: 10000 });
547
+ await this.deleteAlertMenuItem.click({ timeout: 10000 });
548
+ await (0, test_1.expect)(this.deleteAlertConfirmButton).toBeVisible({
549
+ timeout: 10000,
550
+ });
551
+ }).toPass({ timeout: 60000 });
552
+ await this.deleteAlertConfirmButton.click({ timeout: 30000 });
553
+ await (0, test_1.expect)(this.alertRowActionsButtons).toHaveCount(remaining - 1, {
554
+ timeout: 60000,
555
+ });
556
+ remaining = await this.alertRowActionsButtons.count();
557
+ }
558
+ }
559
+ // Reveals each alert's detail row, where the design-system table keeps the
560
+ // recipient, creation date and trigger count the callers assert on. A no-op
561
+ // on the legacy table, whose detail rows are always rendered.
562
+ // Re-selects the Alerts tab when it is no longer the showing one. A
563
+ // page.reload() drops the tab selection and the cluster page comes back on
564
+ // Overview, whose Environments table supplies the very first row -- which
565
+ // is how `alertsList.nth(0)` came to read "NameAlert Flow Cluster" instead
566
+ // of "Email" (console-user-flows.spec.ts reloads and then reads the list
567
+ // straight away). `alertsList` is scoped to whichever tabpanel is visible,
568
+ // so restoring the tab is what makes those reads address the alerts table.
569
+ async ensureAlertsTabSelected() {
570
+ const selected = await this.alertsTab
571
+ .getAttribute('aria-selected')
572
+ .catch(() => null);
573
+ if (selected === 'true') {
574
+ return;
575
+ }
576
+ await this.clickAlertsTab();
577
+ }
578
+ async expandAlerts() {
579
+ await this.ensureAlertsTabSelected();
580
+ for (let i = 0; i < 10; i++) {
581
+ const toggle = this.alertExpandRowButtons.first();
582
+ if (!(await toggle.isVisible().catch(() => false))) {
583
+ return;
584
+ }
585
+ const before = await this.alertExpandRowButtons.count();
586
+ await toggle.click({ timeout: 10000 });
587
+ await (0, test_1.expect)(this.alertExpandRowButtons).toHaveCount(before - 1, {
588
+ timeout: 10000,
589
+ });
590
+ }
452
591
  }
453
592
  // The confirmation modal only closes once the DELETE request resolves. When
454
593
  // that call fails the modal stays open on an inline "We're having trouble
@@ -701,6 +840,18 @@ class ClusterDetailsPage {
701
840
  console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
702
841
  await this.page.reload();
703
842
  await (0, sleep_1.sleep)(20000);
843
+ // A reload drops the tab selection, so the Alerts panel is no
844
+ // longer the visible one -- `alertsList` is scoped to whichever
845
+ // tabpanel is showing, which after a reload is Overview, whose
846
+ // first non-header row is the cluster-details "Name" row. That is
847
+ // exactly what the nightly reported: `Received string:
848
+ // "NameAlert Flow Cluster"`, nine times over, while the Email alert
849
+ // sat unread on a tab nobody had re-opened. Re-select the tab, wait
850
+ // for the panel to mount, and re-expand the detail rows the
851
+ // assertions read from.
852
+ await this.alertsTab.click({ timeout: 60000 });
853
+ await (0, test_1.expect)(this.alertsPanel).toBeVisible({ timeout: 60000 });
854
+ await this.expandAlerts();
704
855
  }
705
856
  else {
706
857
  throw new Error(`Assertion failed after ${maxRetries} attempts`);
@@ -47,6 +47,7 @@ declare class ClusterPage {
47
47
  private extractClusterIdFromUrl;
48
48
  private logClusterCreationInfo;
49
49
  createCluster(name: string, region?: string, tasklistV1Api?: boolean): Promise<void>;
50
+ clickProviderOption(region: string): Promise<void>;
50
51
  selectSubRegion(region: string): Promise<void>;
51
52
  determineTasklistAPI(tasklistV1Api?: boolean): Promise<void>;
52
53
  clickClusterType(): Promise<void>;