@camunda/e2e-test-suite 0.0.1287 → 0.0.1288

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 (33) hide show
  1. package/dist/pages/8.9/AppsPage.d.ts +9 -3
  2. package/dist/pages/8.9/AppsPage.js +262 -47
  3. package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
  4. package/dist/pages/8.9/ClusterDetailsPage.js +92 -9
  5. package/dist/pages/8.9/ClusterSecretsPage.js +5 -1
  6. package/dist/pages/8.9/ConnectorMarketplacePage.d.ts +1 -0
  7. package/dist/pages/8.9/ConnectorMarketplacePage.js +46 -3
  8. package/dist/pages/8.9/ConsoleOrganizationPage.d.ts +3 -2
  9. package/dist/pages/8.9/ConsoleOrganizationPage.js +47 -22
  10. package/dist/pages/8.9/FormJsPage.d.ts +4 -0
  11. package/dist/pages/8.9/FormJsPage.js +53 -19
  12. package/dist/pages/8.9/IdpCreatePage.js +5 -0
  13. package/dist/pages/8.9/ModelerCreatePage.d.ts +5 -0
  14. package/dist/pages/8.9/ModelerCreatePage.js +153 -21
  15. package/dist/pages/8.9/ModelerHomePage.d.ts +1 -0
  16. package/dist/pages/8.9/ModelerHomePage.js +29 -4
  17. package/dist/pages/8.9/PlayPage.js +11 -1
  18. package/dist/pages/8.9/ProcessApplicationPage.js +48 -4
  19. package/dist/pages/8.9/UtilitiesPage.js +57 -17
  20. package/dist/pages/8.9/WorkspaceMembersPage.d.ts +21 -0
  21. package/dist/pages/8.9/WorkspaceMembersPage.js +264 -0
  22. package/dist/tests/8.9/access-user-role-management.spec.js +111 -26
  23. package/dist/tests/8.9/console-user-flows.spec.js +4 -4
  24. package/dist/tests/8.9/idp-user-flows.spec.js +2 -1
  25. package/dist/tests/8.9/rba-enabled-v1-user-flows.spec.js +13 -4
  26. package/dist/tests/8.9/rba-enabled-v2-user-flows.spec.js +13 -4
  27. package/dist/tests/8.9/test-setup.spec.js +57 -4
  28. package/dist/tests/8.9/utr-enabled-user-flows.spec.js +18 -0
  29. package/dist/tests/8.9/web-modeler-user-flows.spec.js +6 -2
  30. package/dist/utils/consoleApiHelpers.d.ts +2 -0
  31. package/dist/utils/consoleApiHelpers.js +50 -4
  32. package/dist/utils/resetSession.js +6 -1
  33. package/package.json +1 -1
@@ -19,9 +19,13 @@ declare class AppsPage {
19
19
  readonly camundaComponentsButton: Locator;
20
20
  readonly camundaAppsButton: Locator;
21
21
  readonly hubSidebar: Locator;
22
+ private hubReturnUrl?;
22
23
  constructor(page: Page);
23
24
  clickCluster(component: Locator, name: string): Promise<void>;
25
+ private expandHubClusterGroup;
26
+ private showAllWorkspaces;
24
27
  private clickHubClusterApp;
28
+ goBackToHub(): Promise<void>;
25
29
  clickModeler(): Promise<void>;
26
30
  clickTasklist(clusterName: string): Promise<void>;
27
31
  clickAdmin(clusterName: string): Promise<void>;
@@ -35,8 +39,10 @@ declare class AppsPage {
35
39
  clickCamundaApps(maxRetries?: number): Promise<void>;
36
40
  private dismissBlockingOverlays;
37
41
  clickConsoleLink(): Promise<void>;
38
- assertOperatePresent(shouldBeVisible?: boolean): Promise<void>;
39
- assertTasklistPresent(shouldBeVisible?: boolean): Promise<void>;
40
- assertOptimizePresent(shouldBeVisible?: boolean): Promise<void>;
42
+ private hubClusterAppLink;
43
+ private assertHubClusterAppPresent;
44
+ assertOperatePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
45
+ assertTasklistPresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
46
+ assertOptimizePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
41
47
  }
42
48
  export { AppsPage };
@@ -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.9/UtilitiesPage");
7
+ function escapeRegExp(value) {
8
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
9
+ }
7
10
  class AppsPage {
8
11
  page;
9
12
  modelerLink;
@@ -24,9 +27,18 @@ class AppsPage {
24
27
  camundaComponentsButton;
25
28
  camundaAppsButton;
26
29
  hubSidebar;
30
+ hubReturnUrl;
27
31
  constructor(page) {
28
32
  this.page = page;
29
- this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
33
+ // Hub has no "Modeler" switcher entry; Modeler is Hub's home, reached
34
+ // through the header logo link clickModeler() uses
35
+ // (NavShellDs.common.tsx).
36
+ this.modelerLink = page
37
+ .getByRole('link', { name: 'Modeler', exact: true })
38
+ .or(page
39
+ .locator('header[data-slot="app-header"]')
40
+ .getByRole('link', { name: /^Camunda (Modeler|Hub) home$/ }))
41
+ .first();
30
42
  // Camunda Hub's persistent "Main navigation" sidebar lists each cluster
31
43
  // with its own apps (Operate/Tasklist/Optimize/Identity) as child links
32
44
  // -- there is no per-app switcher panel to open on Hub (see
@@ -83,15 +95,186 @@ class AppsPage {
83
95
  }
84
96
  // Camunda Hub's sidebar groups by cluster, not by app: each cluster row
85
97
  // expands to reveal its own per-app child links (Operate/Tasklist/...).
86
- // camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: "the
98
+ // camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: the
87
99
  // design system splits the group row into a label control and a chevron
88
- // control, both labelled by the cluster name; either one opens it" -- and
100
+ // control, both labelled by the cluster name; either one opens it -- and
89
101
  // the child links do not exist in the DOM at all until expanded (not just
90
102
  // hidden), and are not nested under any container matchable by cluster
91
103
  // name once expanded. So: find the toggle by the cluster's own name
92
- // (`.first()` because both controls share that name), click it, then
93
- // look up the app link sidebar-wide rather than scoped to a "group"
94
- // container that doesn't reliably exist.
104
+ // (`.first()` because both controls share that name), click it, then look
105
+ // up the app link sidebar-wide rather than scoped to a "group" container
106
+ // that doesn't reliably exist. Ported back from pages/8.8/AppsPage.ts.
107
+ async expandHubClusterGroup(clusterName) {
108
+ // Anchored at the start: the expanded row's accessible name is its text
109
+ // ("Test Cluster dev" -- name plus environment badge), so a plain
110
+ // substring match would also hit "UI Test Cluster", which this suite
111
+ // creates too and which can precede it in the sidebar.
112
+ const toggle = this.hubSidebar
113
+ .getByRole('button', { name: new RegExp(`^${escapeRegExp(clusterName)}`) })
114
+ .first();
115
+ // The diagram editor (and other full-screen views reached from it, e.g.
116
+ // right after runProcessInstance/deployDiagram) has no "Main navigation"
117
+ // sidebar at all -- confirmed from a nightly trace's DOM snapshot: the
118
+ // page's only <nav> is aria-label="Context navigation" (the breadcrumb),
119
+ // and neither "Operate"/"Tasklist"/"Optimize" nor any app-switcher text
120
+ // appears anywhere on it. clickCamundaApps() is a no-op on Hub (there is
121
+ // no switcher panel to open), so a caller reaching clickOperate/
122
+ // clickTasklist/clickOptimize straight from a diagram was left on a page
123
+ // this sidebar could never resolve on, no matter how the toggle itself
124
+ // is matched. Navigate home first, the same way clickModeler() does,
125
+ // whenever the sidebar isn't already there.
126
+ //
127
+ // A freshly re-authenticated page can also land on a workspace-agnostic
128
+ // route (e.g. bare /workspaces right after loginWithTestUser(), which
129
+ // clickCamundaApps() does nothing to fix -- it is a no-op on Hub) whose
130
+ // sidebar renders but has not resolved this cluster's own group yet.
131
+ // Confirmed from a nightly trace: both the getAttribute and click below
132
+ // timed out (10s each) against this exact toggle while the URL sat on
133
+ // /workspaces, right after a mid-test re-login. Route through the same
134
+ // clickModeler() recovery whenever the toggle itself hasn't shown up
135
+ // either, not only when the whole sidebar is missing.
136
+ // Leave the rail as Hub renders it: collapsed, every group is a visible
137
+ // flyout trigger. Opening the rail here made the group click that follows
138
+ // miss, likely because each label is re-wrapped in a truncation tooltip
139
+ // while the rail widens (8.8 reverted the same step in 3ae39d3).
140
+ const sidebarReady = (await this.hubSidebar.isVisible({ timeout: 5000 }).catch(() => false)) &&
141
+ (await toggle
142
+ .waitFor({ state: 'visible', timeout: 10000 })
143
+ .then(() => true)
144
+ .catch(() => false));
145
+ if (!sidebarReady) {
146
+ await this.clickModeler();
147
+ await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
148
+ // Since camunda-hub #28956 the Workspaces overview defaults to "My
149
+ // workspaces" -- only the ones the current user is an EXPLICIT member
150
+ // of -- and resets to it on every load. The shared workspace is created
151
+ // by whichever setup user won the race, so a role-flow user lands on
152
+ // "No workspaces match this filter / You don't have explicit membership
153
+ // in any workspace in this organization" (confirmed live) and the
154
+ // sidebar never renders this cluster's group. Widening the scope is
155
+ // what makes it appear; without it the wait below can only time out.
156
+ // ModelerHomePage.showAllWorkspaces() covers the project-folder paths,
157
+ // but nothing covered the app-switcher path until here.
158
+ await this.showAllWorkspaces(5000);
159
+ await (0, test_1.expect)(toggle).toBeVisible({ timeout: 30000 });
160
+ }
161
+ // camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
162
+ // shape: "the design system splits the group row into a label control
163
+ // and a chevron control, both labelled by the cluster name; either one
164
+ // opens it" -- there is no separate "Toggle <name>" control, and the
165
+ // child links do not exist in the DOM at all until expanded (not merely
166
+ // hidden), nor are they nested under any container matchable by cluster
167
+ // name once expanded. So: find the toggle by the cluster's own name
168
+ // (`.first()` because both controls share that name), click it, then
169
+ // look up the app link sidebar-wide rather than scoped to a "group"
170
+ // container that doesn't reliably exist.
171
+ // Check THIS cluster's own toggle state, not "is any app link with this
172
+ // name visible anywhere in the sidebar" -- with more than one cluster in
173
+ // the org, a DIFFERENT cluster's group can already be expanded (left over
174
+ // from an earlier action on the same page), and that cluster's own
175
+ // Operate/Tasklist/Optimize link would satisfy an unscoped visibility
176
+ // check, skipping expansion of the requested cluster entirely and then
177
+ // clicking the wrong cluster's link. The child links are not nested under
178
+ // any container matchable by cluster name once expanded (confirmed
179
+ // against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
180
+ // own aria-expanded is the only reliable per-cluster signal available.
181
+ // Not exact: the same suite asserts this row with a substring/regex
182
+ // match (`{name: /Production/}`), not an exact string -- and nightly
183
+ // traces confirm why: the rendered toggle's accessible name is not
184
+ // byte-for-byte the bare cluster name (it carries additional content,
185
+ // e.g. a trailing tag summary), so an exact-match lookup here finds
186
+ // nothing and the click below times out waiting on a locator that can
187
+ // never resolve.
188
+ const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
189
+ if (!alreadyExpanded) {
190
+ // This is the only expansion action in the Hub path -- swallowing its
191
+ // failure made a broken expansion look successful and left the caller
192
+ // to burn the full 30s below waiting on an app link that could never
193
+ // appear, hiding the actionable error (e.g. a toggle that doesn't
194
+ // exist or isn't clickable) behind an unrelated timeout.
195
+ await toggle.click({ timeout: 10000 });
196
+ }
197
+ // An app link resolved after this is sidebar-wide (see callers below), so
198
+ // if an EARLIER call in the same test left a different cluster's group
199
+ // expanded too, two "Operate" (say) links now exist and that becomes a
200
+ // strict-mode violation. Collapse every other expanded group before
201
+ // resolving it -- there is no container to scope the link to instead,
202
+ // since the child links aren't nested under anything matchable by
203
+ // cluster name once expanded.
204
+ // hasNotText filters DOM text content, not the accessible name -- the
205
+ // expanded chevron can be labelled via aria-label with no text content
206
+ // of its own, in which case hasNotText would fail to exclude the toggle
207
+ // we just expanded and this loop would immediately collapse it again.
208
+ // Match by accessible name (same algorithm getByRole's own name option
209
+ // uses) instead, excluding any name CONTAINING clusterName -- not just
210
+ // one exactly equal to it, since (per the toggle locator above) the
211
+ // current cluster's own toggle name is not exactly clusterName either,
212
+ // and an exact-equality exclusion would wrongly treat it as "other" and
213
+ // immediately re-collapse the group this method just expanded.
214
+ // The sidebar's own rail toggle is also a button inside "Main navigation"
215
+ // with aria-expanded="true" once the rail is open ("Collapse sidebar").
216
+ // Without excluding it this loop clicked it right after the group was
217
+ // expanded, collapsing the rail and taking the app links with it -- the
218
+ // 8.8 nightly trace after #3746 shows exactly that sequence (expand
219
+ // "Test Cluster", count 1, click, app link never appears).
220
+ const otherExpandedToggles = this.hubSidebar
221
+ .getByRole('button', {
222
+ expanded: true,
223
+ name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
224
+ })
225
+ .and(this.page.locator(':not([data-slot="app-sidebar-collapse-toggle"])'));
226
+ for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
227
+ // Let a failed collapse throw rather than swallowing it: the app link
228
+ // is intentionally sidebar-wide, so a group left expanded here produces
229
+ // a strict-mode violation (or the wrong cluster's link) below anyway --
230
+ // failing at the actual cause is more useful than at that symptom.
231
+ await otherExpandedToggles.first().click({ timeout: 10000 });
232
+ }
233
+ }
234
+ // Widens Hub's Workspaces scope filter to "All workspaces". Duplicated from
235
+ // ModelerHomePage.showAllWorkspaces() rather than imported: ModelerHomePage
236
+ // constructs page objects of its own, and pulling it in here would make the
237
+ // Apps -> Modeler navigation path circular. The control is absent outside
238
+ // Hub and for non-elevated users, where a membership-scoped list is already
239
+ // correct, so a missing control is a no-op.
240
+ async showAllWorkspaces(probeTimeout) {
241
+ // The empty state offers an explicit "Switch to All workspaces" button
242
+ // next to "You don't have explicit membership in any workspace in this
243
+ // organization" -- prefer it. Driving the combobox alone was not enough:
244
+ // when its accessible name does not match, the probe below simply
245
+ // returns and the caller waits out its whole timeout on a page that is
246
+ // still scoped to "My workspaces" (observed live, twice).
247
+ const switchButton = this.page.getByRole('button', {
248
+ name: 'Switch to All workspaces',
249
+ });
250
+ const scopeFilter = this.page.getByRole('combobox', {
251
+ name: 'Filter workspaces',
252
+ });
253
+ const appeared = await switchButton
254
+ .or(scopeFilter)
255
+ .first()
256
+ .waitFor({ state: 'visible', timeout: probeTimeout })
257
+ .then(() => true)
258
+ .catch(() => false);
259
+ if (!appeared) {
260
+ return;
261
+ }
262
+ if (await switchButton.isVisible().catch(() => false)) {
263
+ await switchButton.click({ timeout: 10000 });
264
+ await (0, test_1.expect)(switchButton).toBeHidden({ timeout: 10000 });
265
+ return;
266
+ }
267
+ if ((await scopeFilter.innerText()).includes('All workspaces')) {
268
+ return;
269
+ }
270
+ await scopeFilter.click({ timeout: 10000 });
271
+ await this.page
272
+ .getByRole('option', { name: 'All workspaces' })
273
+ .click({ timeout: 10000 });
274
+ await (0, test_1.expect)(scopeFilter).toContainText('All workspaces', {
275
+ timeout: 10000,
276
+ });
277
+ }
95
278
  async clickHubClusterApp(clusterName, appName) {
96
279
  // When the sidebar is collapsed (the default on a fresh session), the
97
280
  // group toggle opens a Radix popover -- a role="dialog" labelled with the
@@ -101,39 +284,14 @@ class AppsPage {
101
284
  // Cluster (opens in a new tab)" et al., yet the nav-scoped link never
102
285
  // resolves). Look in that popover first; keep the sidebar-scoped lookup
103
286
  // for the expanded-sidebar layout, where the links render inline.
104
- // Not exact: the rendered accessible name carries more than the bare app
105
- // name (e.g. "Operate in <cluster> (opens in a new tab)").
106
- //
107
- // The sidebar-scoped fallback must also carry clusterName in its name: the
108
- // sidebar lists every Environment as a link named after its cluster, so a
109
- // bare /Admin|Identity/ match resolved to the "Admin Role Cluster"
110
- // environment link -- which precedes the portalled popover in DOM order --
111
- // and navigated to that environment's page instead of Admin (nightly
112
- // trace, UTR Candidate Group).
113
- const appLink = this.page
114
- .getByRole('dialog', { name: clusterName, exact: true })
115
- .getByRole('link', { name: appName })
116
- .or(this.hubSidebar
117
- .getByRole('link', { name: appName })
118
- .and(this.hubSidebar.getByRole('link', { name: ` in ${clusterName}` })))
119
- .first();
120
- // Decide whether to expand from clusterName's OWN toggle state, not from
121
- // whether "some" appLink is already visible sidebar-wide: in a
122
- // multi-cluster org another cluster's group can already be expanded with
123
- // the same app name, which made this check pass while clusterName's own
124
- // group stayed collapsed -- appLink.click() below then fired on the
125
- // wrong cluster's link (see PR #3627 review).
126
- const collapsedToggle = this.hubSidebar
127
- .getByRole('button', { name: clusterName, expanded: false })
128
- .first();
129
- if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
130
- // This is the only expansion action in the Hub path -- swallowing its
131
- // failure made a broken expansion look successful and left the caller
132
- // to burn the full 30s below waiting on an app link that could never
133
- // appear, hiding the actionable error (e.g. a toggle that doesn't
134
- // exist or isn't clickable) behind an unrelated timeout.
135
- await collapsedToggle.click({ timeout: 10000 });
136
- }
287
+ const appLink = this.hubClusterAppLink(clusterName, appName);
288
+ // Expansion, sidebar recovery and collapsing every OTHER expanded group
289
+ // all live in the ported helper. Every caller in this suite reaches here
290
+ // straight from the diagram editor after "Deploy & run", a view with no
291
+ // "Main navigation" sidebar at all -- so the toggle lookup the old inline
292
+ // check did could never resolve there (nightly trace: the page's only
293
+ // chrome is the breadcrumb and the "Instance started!" toast).
294
+ await this.expandHubClusterGroup(clusterName);
137
295
  await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
138
296
  // Hub's cluster-app links carry target="_blank", so clicking them opens
139
297
  // the app in a new tab while every caller keeps asserting on this.page.
@@ -141,11 +299,22 @@ class AppsPage {
141
299
  // navigation the Console app switcher gave these callers.
142
300
  const href = await appLink.getAttribute('href');
143
301
  if (href) {
302
+ this.hubReturnUrl = this.page.url();
144
303
  await this.page.goto(new URL(href, this.page.url()).toString());
145
304
  return;
146
305
  }
147
306
  await appLink.click({ timeout: 10000 });
148
307
  }
308
+ // Returns from an app that clickHubClusterApp opened. page.goBack() alone
309
+ // can stop on one of the app's own login-redirect entries, and the next
310
+ // Hub sidebar lookup then runs against the app instead of Hub.
311
+ async goBackToHub() {
312
+ await this.page.goBack();
313
+ if (this.hubReturnUrl &&
314
+ !new URL(this.page.url()).hostname.startsWith('hub.')) {
315
+ await this.page.goto(this.hubReturnUrl);
316
+ }
317
+ }
149
318
  async clickModeler() {
150
319
  // Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
151
320
  // app switcher. Its home is the Modeler/Workspaces home, but a hostname
@@ -247,10 +416,6 @@ class AppsPage {
247
416
  }
248
417
  async clickAdmin(clusterName) {
249
418
  if (new URL(this.page.url()).hostname.startsWith('hub.')) {
250
- // Non-anchored, matching the substring lookup clickHubClusterApp uses
251
- // for Operate/Tasklist/Optimize -- an anchored pattern fails the same
252
- // way an exact-string match did for those apps if the rendered link's
253
- // accessible name carries any extra content (icon/badge).
254
419
  await this.clickHubClusterApp(clusterName, /Admin|Identity/);
255
420
  return;
256
421
  }
@@ -488,8 +653,14 @@ class AppsPage {
488
653
  // separate app to switch into. Its header banner (homePage.consoleBanner,
489
654
  // via hubHeader) is already visible from any Hub page, so there is no
490
655
  // navigation action needed here -- mirrors the same hostname guard in
491
- // clickModeler().
656
+ // clickModeler(). The diagram editor is the exception: it has no "Main
657
+ // navigation" sidebar, so a caller arriving from Deploy & run would find
658
+ // no Clusters/Environments link. Go home first there.
492
659
  if (new URL(this.page.url()).hostname.startsWith('hub.')) {
660
+ if (!(await this.hubSidebar.isVisible().catch(() => false))) {
661
+ await this.clickModeler();
662
+ await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
663
+ }
493
664
  return;
494
665
  }
495
666
  const maxRetries = 3;
@@ -527,7 +698,41 @@ class AppsPage {
527
698
  }
528
699
  throw new Error(`Failed to click the console link after ${maxRetries} attempts.`);
529
700
  }
530
- async assertOperatePresent(shouldBeVisible = true) {
701
+ // Hub's replacement for the app switcher: a cluster's apps are the child
702
+ // links of its sidebar group. Hub names every app link "<app> in
703
+ // <environment>", plus " (opens in a new tab)" when it leads to the live app
704
+ // (camunda-hub environment-sidebar-rows.ts). Anchor both halves: the
705
+ // flyout's first link is "Details of <environment>", so for "Admin Role
706
+ // Cluster" a bare /Admin|Identity/ picked that link, and inline it picked
707
+ // "Operate in Admin Role Cluster (opens in a new tab)".
708
+ hubClusterAppLink(clusterName, appName) {
709
+ const appPattern = typeof appName === 'string' ? escapeRegExp(appName) : appName.source;
710
+ const cluster = escapeRegExp(clusterName);
711
+ const appLinkName = new RegExp(`^(?:${appPattern}) in ${cluster}(?: \\(opens in a new tab\\))?$`);
712
+ return this.page
713
+ .getByRole('dialog', { name: clusterName, exact: true })
714
+ .getByRole('link', { name: appLinkName })
715
+ .or(this.hubSidebar.getByRole('link', { name: appLinkName }))
716
+ .first();
717
+ }
718
+ async assertHubClusterAppPresent(clusterName, appName, shouldBeVisible) {
719
+ const appLink = this.hubClusterAppLink(clusterName, appName);
720
+ // Not expanded for an absence check: expandHubClusterGroup throws when
721
+ // the user cannot see the environment at all, which is a pass here.
722
+ if (!shouldBeVisible) {
723
+ await (0, test_1.expect)(appLink).toBeHidden({ timeout: 30000 });
724
+ return;
725
+ }
726
+ await this.expandHubClusterGroup(clusterName);
727
+ await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
728
+ }
729
+ // On Hub the cluster name is what scopes the lookup: there is no app
730
+ // switcher, only each cluster's own sidebar group.
731
+ async assertOperatePresent(clusterName, shouldBeVisible = true) {
732
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
733
+ await this.assertHubClusterAppPresent(clusterName, 'Operate', shouldBeVisible);
734
+ return;
735
+ }
531
736
  const maxRetries = 3;
532
737
  for (let retries = 0; retries < maxRetries; retries++) {
533
738
  try {
@@ -556,7 +761,12 @@ class AppsPage {
556
761
  }
557
762
  throw new Error(`Failed to assert the operate link visibility after ${maxRetries} attempts.`);
558
763
  }
559
- async assertTasklistPresent(shouldBeVisible = true) {
764
+ // See assertOperatePresent for the clusterName/Hub note.
765
+ async assertTasklistPresent(clusterName, shouldBeVisible = true) {
766
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
767
+ await this.assertHubClusterAppPresent(clusterName, 'Tasklist', shouldBeVisible);
768
+ return;
769
+ }
560
770
  const maxRetries = 3;
561
771
  for (let retries = 0; retries < maxRetries; retries++) {
562
772
  try {
@@ -585,7 +795,12 @@ class AppsPage {
585
795
  }
586
796
  throw new Error(`Failed to assert the tasklist link visibility after ${maxRetries} attempts.`);
587
797
  }
588
- async assertOptimizePresent(shouldBeVisible = true) {
798
+ // See assertOperatePresent for the clusterName/Hub note.
799
+ async assertOptimizePresent(clusterName, shouldBeVisible = true) {
800
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
801
+ await this.assertHubClusterAppPresent(clusterName, 'Optimize', shouldBeVisible);
802
+ return;
803
+ }
589
804
  const maxRetries = 3;
590
805
  for (let retries = 0; retries < maxRetries; retries++) {
591
806
  try {
@@ -4,6 +4,7 @@ import { McpCredentials } from '../../utils/mcpSecrets';
4
4
  declare class ClusterDetailsPage {
5
5
  private page;
6
6
  private pendingClientScopeCheck;
7
+ private clusterDetailsUrl;
7
8
  readonly apiTab: Locator;
8
9
  readonly createFirstClientButton: Locator;
9
10
  readonly clientNameTextbox: Locator;
@@ -24,6 +25,7 @@ declare class ClusterDetailsPage {
24
25
  readonly clientCredentialsDialog: Locator;
25
26
  readonly clustersLink: Locator;
26
27
  readonly alertsTab: Locator;
28
+ readonly alertsTabPanel: Locator;
27
29
  readonly alertsList: Locator;
28
30
  readonly alertExpandRowButtons: Locator;
29
31
  readonly alertRowActionsButtons: Locator;
@@ -43,6 +45,7 @@ declare class ClusterDetailsPage {
43
45
  readonly expandButton: Locator;
44
46
  readonly rbaEnabledMessage: Locator;
45
47
  readonly authorizationsHeading: Locator;
48
+ readonly authorizationsSwitch: Locator;
46
49
  readonly orchestrationClusterCheckbox: Locator;
47
50
  readonly clientCredentialsLink: (clientCredentials: string) => Locator;
48
51
  readonly clientRow: (name: string) => Locator;
@@ -74,11 +77,13 @@ declare class ClusterDetailsPage {
74
77
  clickCloseModalButton(): Promise<void>;
75
78
  clickSettingsTab(): Promise<void>;
76
79
  private waitForAuthorizationsSection;
80
+ private setAuthorizationsViaSwitch;
77
81
  enableAuthorizations(): Promise<void>;
78
82
  disableAuthorizations(): Promise<void>;
79
83
  searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
80
84
  deleteAPIClientsIfExist(name?: string): Promise<void>;
81
85
  clickAlertsTab(): Promise<void>;
86
+ reopenAlertsTab(): Promise<void>;
82
87
  clickExpandButton(): Promise<void>;
83
88
  clickCreateFirstAlertButton(): Promise<void>;
84
89
  clickCreateNewAlertButton(): Promise<void>;
@@ -6,11 +6,17 @@ const sleep_1 = require("../../utils/sleep");
6
6
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
7
7
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
8
8
  const ClientCredentialsDetailsPage_1 = require("./ClientCredentialsDetailsPage");
9
+ const sessionRecovery_1 = require("../../utils/sessionRecovery");
10
+ // Hub's own /login and /login-callback routes, which render only a spinner
11
+ // while the Auth0 hand-off of a full page load is in flight.
12
+ const HUB_LOGIN_ROUTE = /^\/login(-callback)?\/?$/;
9
13
  class ClusterDetailsPage {
10
14
  page;
11
15
  // Name of a client whose scopes still have to be checked against the
12
16
  // clients list — see createAPIClient() / clickCloseModalButton().
13
17
  pendingClientScopeCheck = null;
18
+ // The cluster page reopenAlertsTab() goes back to -- see there.
19
+ clusterDetailsUrl;
14
20
  apiTab;
15
21
  createFirstClientButton;
16
22
  clientNameTextbox;
@@ -31,6 +37,7 @@ class ClusterDetailsPage {
31
37
  clientCredentialsDialog;
32
38
  clustersLink;
33
39
  alertsTab;
40
+ alertsTabPanel;
34
41
  alertsList;
35
42
  alertExpandRowButtons;
36
43
  alertRowActionsButtons;
@@ -50,6 +57,7 @@ class ClusterDetailsPage {
50
57
  expandButton;
51
58
  rbaEnabledMessage;
52
59
  authorizationsHeading;
60
+ authorizationsSwitch;
53
61
  orchestrationClusterCheckbox;
54
62
  clientCredentialsLink;
55
63
  clientRow;
@@ -134,7 +142,13 @@ class ClusterDetailsPage {
134
142
  .getByRole('banner')
135
143
  .getByRole('link', { name: 'Clusters' });
136
144
  this.alertsTab = page.getByRole('tab', { name: 'Alerts' });
137
- this.alertsList = page
145
+ // Only the selected tab's panel is exposed (the design-system tabs unmount
146
+ // the others, Carbon marks them hidden), and it is named after its tab.
147
+ // Page-wide, getByRole('row') read the Overview "Cluster Details" rows
148
+ // whenever Overview was showing -- the nightly's `Received string:
149
+ // "NameAlert Flow Cluster"` where the Email alert row was expected.
150
+ this.alertsTabPanel = page.getByRole('tabpanel', { name: 'Alerts' });
151
+ this.alertsList = this.alertsTabPanel
138
152
  .getByRole('row')
139
153
  .filter({ hasNotText: 'Delivery method' }); //Filter out header row
140
154
  // Hub's Alerts tab is a design-system data table: one summary row per
@@ -191,6 +205,12 @@ class ClusterDetailsPage {
191
205
  });
192
206
  this.expandButton = page.locator('button[class="cds--table-expand__button"]');
193
207
  this.rbaEnabledMessage = page.getByText('Authorizations enabled');
208
+ // Hub's design-system Settings tab: <button role="switch"
209
+ // aria-label="Switch Enable authorizations" aria-checked="true|false">
210
+ // (c4 C4Toggle). It has no Carbon `.cds--toggle__*` nodes. Same as 8.8.
211
+ this.authorizationsSwitch = this.page.getByRole('switch', {
212
+ name: /Enable authorizations/i,
213
+ });
194
214
  this.authorizationsHeading = this.page.getByRole('heading', {
195
215
  name: 'Authorizations',
196
216
  });
@@ -381,9 +401,40 @@ class ClusterDetailsPage {
381
401
  },
382
402
  });
383
403
  }
404
+ // Drives the design-system Authorizations switch. Returns false when this
405
+ // shell does not render one, so the caller falls through to the legacy
406
+ // Carbon toggle.
407
+ async setAuthorizationsViaSwitch(enabled) {
408
+ const hasSwitch = await this.authorizationsSwitch
409
+ .waitFor({ state: 'visible', timeout: 10000 })
410
+ .then(() => true)
411
+ .catch(() => false);
412
+ if (!hasSwitch) {
413
+ return false;
414
+ }
415
+ const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
416
+ if (isOn === enabled) {
417
+ return true;
418
+ }
419
+ await this.authorizationsSwitch.click({ timeout: 30000 });
420
+ const confirmOpened = await this.dialog
421
+ .waitFor({ state: 'visible', timeout: 15000 })
422
+ .then(() => true)
423
+ .catch(() => false);
424
+ if (confirmOpened) {
425
+ const confirm = enabled ? this.rbaEnableButton : this.rbaDisableButton;
426
+ await confirm.click({ timeout: 30000 });
427
+ await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
428
+ }
429
+ await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
430
+ return true;
431
+ }
384
432
  async enableAuthorizations() {
385
433
  await (0, sleep_1.sleep)(5000);
386
434
  await this.waitForAuthorizationsSection();
435
+ if (await this.setAuthorizationsViaSwitch(true)) {
436
+ return;
437
+ }
387
438
  // Locate all elements with class .cds--toggle__text
388
439
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
389
440
  // Check if toggleTextElements has at least 2 elements
@@ -417,11 +468,18 @@ class ClusterDetailsPage {
417
468
  }
418
469
  }
419
470
  else {
420
- console.log('Toggle text elements not found or less than 2.');
471
+ // Was a console.log that returned as though authorizations had been
472
+ // enabled; under Hub this branch ran every time (8.8 fix 7d771aa).
473
+ throw new Error('Could not enable authorizations: the Settings tab rendered neither ' +
474
+ 'the design-system "Enable authorizations" switch nor a Carbon ' +
475
+ 'toggle pair.');
421
476
  }
422
477
  }
423
478
  async disableAuthorizations() {
424
479
  await this.waitForAuthorizationsSection();
480
+ if (await this.setAuthorizationsViaSwitch(false)) {
481
+ return;
482
+ }
425
483
  // Locate all elements with class .cds--toggle__text
426
484
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
427
485
  // Check if toggleTextElements has at least 2 elements
@@ -456,7 +514,9 @@ class ClusterDetailsPage {
456
514
  }
457
515
  }
458
516
  else {
459
- console.log('Toggle text elements not found or less than 2.');
517
+ throw new Error('Could not disable authorizations: the Settings tab rendered neither ' +
518
+ 'the design-system "Enable authorizations" switch nor a Carbon ' +
519
+ 'toggle pair.');
460
520
  }
461
521
  }
462
522
  async searchAndClickClientCredentialsLink(name) {
@@ -494,10 +554,9 @@ class ClusterDetailsPage {
494
554
  await this.alertsTab.click();
495
555
  // The tab bar remounts while the cluster payload is still arriving, which
496
556
  // discards the click and leaves Overview selected -- and the tab is a
497
- // button, not a route, so nothing later re-selects it. Callers then read
498
- // the Overview "Cluster Details" rows through the page-wide alertsList
499
- // locator and see a row count that never changes. Confirm the panel
500
- // mounted, re-selecting the tab after a reload between attempts.
557
+ // button, not a route, so nothing later re-selects it. Callers would then
558
+ // find no Alerts panel for alertsList to read. Confirm the panel mounted,
559
+ // re-selecting the tab after a reload between attempts.
501
560
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.alertsPanel, {
502
561
  visibilityTimeout: 60000,
503
562
  totalTimeout: 240000,
@@ -509,6 +568,31 @@ class ClusterDetailsPage {
509
568
  },
510
569
  });
511
570
  }
571
+ // Re-enters the cluster page and re-opens the Alerts tab, for callers that
572
+ // poll the alerts table for a server-side change. Hub keeps the selected
573
+ // tab out of the URL (ClusterDetails.saas.tsx drops c4's tabChange), so any
574
+ // full load comes back on Overview and the tab has to be clicked again.
575
+ // A goto to the remembered cluster page rather than a reload: a full load
576
+ // re-runs Hub's Auth0 hand-off, and a reload issued while the page is still
577
+ // parked on /login-callback?code=... replays that single-use code.
578
+ async reopenAlertsTab() {
579
+ if ((0, sessionRecovery_1.isAtIdentityProvider)(this.page)) {
580
+ await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page);
581
+ }
582
+ const current = new URL(this.page.url());
583
+ if (!(0, sessionRecovery_1.isAtIdentityProvider)(this.page) &&
584
+ !HUB_LOGIN_ROUTE.test(current.pathname)) {
585
+ this.clusterDetailsUrl = current.href;
586
+ }
587
+ const target = this.clusterDetailsUrl ?? current.href;
588
+ const host = new URL(target).host;
589
+ await this.page.goto(target, { waitUntil: 'domcontentloaded' });
590
+ // goto() resolves on the first document; the hand-off then bounces
591
+ // through /login and /login-callback (~45s on CI) before it lands back.
592
+ await this.page.waitForURL((url) => url.host === host && !HUB_LOGIN_ROUTE.test(url.pathname), { timeout: 120000 });
593
+ await this.alertsTab.click({ timeout: 60000 });
594
+ await (0, test_1.expect)(this.alertsPanel.first()).toBeVisible({ timeout: 60000 });
595
+ }
512
596
  async clickExpandButton() {
513
597
  await this.expandButton.click({ timeout: 60000 });
514
598
  }
@@ -829,8 +913,7 @@ class ClusterDetailsPage {
829
913
  catch (error) {
830
914
  if (attempt < maxRetries - 1) {
831
915
  console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
832
- await this.page.reload();
833
- await (0, sleep_1.sleep)(20000);
916
+ await this.reopenAlertsTab();
834
917
  }
835
918
  else {
836
919
  throw new Error(`Assertion failed after ${maxRetries} attempts`);