@camunda/e2e-test-suite 0.0.1287 → 0.0.1289

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 (45) hide show
  1. package/dist/pages/8.8/AppsPage.d.ts +7 -3
  2. package/dist/pages/8.8/AppsPage.js +96 -26
  3. package/dist/pages/8.8/ModelerCreatePage.d.ts +3 -0
  4. package/dist/pages/8.8/ModelerCreatePage.js +25 -0
  5. package/dist/pages/8.8/UtilitiesPage.js +34 -17
  6. package/dist/pages/8.8/WorkspaceMembersPage.d.ts +1 -0
  7. package/dist/pages/8.8/WorkspaceMembersPage.js +21 -10
  8. package/dist/pages/8.9/AppsPage.d.ts +9 -3
  9. package/dist/pages/8.9/AppsPage.js +262 -47
  10. package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
  11. package/dist/pages/8.9/ClusterDetailsPage.js +92 -9
  12. package/dist/pages/8.9/ClusterSecretsPage.js +5 -1
  13. package/dist/pages/8.9/ConnectorMarketplacePage.d.ts +1 -0
  14. package/dist/pages/8.9/ConnectorMarketplacePage.js +46 -3
  15. package/dist/pages/8.9/ConsoleOrganizationPage.d.ts +3 -2
  16. package/dist/pages/8.9/ConsoleOrganizationPage.js +47 -22
  17. package/dist/pages/8.9/FormJsPage.d.ts +4 -0
  18. package/dist/pages/8.9/FormJsPage.js +53 -19
  19. package/dist/pages/8.9/IdpCreatePage.js +5 -0
  20. package/dist/pages/8.9/ModelerCreatePage.d.ts +5 -0
  21. package/dist/pages/8.9/ModelerCreatePage.js +153 -21
  22. package/dist/pages/8.9/ModelerHomePage.d.ts +1 -0
  23. package/dist/pages/8.9/ModelerHomePage.js +29 -4
  24. package/dist/pages/8.9/PlayPage.js +11 -1
  25. package/dist/pages/8.9/ProcessApplicationPage.js +48 -4
  26. package/dist/pages/8.9/UtilitiesPage.js +57 -17
  27. package/dist/pages/8.9/WorkspaceMembersPage.d.ts +21 -0
  28. package/dist/pages/8.9/WorkspaceMembersPage.js +264 -0
  29. package/dist/tests/8.8/access-user-role-management.spec.js +57 -29
  30. package/dist/tests/8.8/console-user-flows.spec.js +22 -1
  31. package/dist/tests/8.8/rba-enabled-v1-user-flows.spec.js +13 -4
  32. package/dist/tests/8.8/rba-enabled-v2-user-flows.spec.js +50 -14
  33. package/dist/tests/8.8/test-setup.spec.js +49 -4
  34. package/dist/tests/8.9/access-user-role-management.spec.js +111 -26
  35. package/dist/tests/8.9/console-user-flows.spec.js +4 -4
  36. package/dist/tests/8.9/idp-user-flows.spec.js +2 -1
  37. package/dist/tests/8.9/rba-enabled-v1-user-flows.spec.js +13 -4
  38. package/dist/tests/8.9/rba-enabled-v2-user-flows.spec.js +13 -4
  39. package/dist/tests/8.9/test-setup.spec.js +57 -4
  40. package/dist/tests/8.9/utr-enabled-user-flows.spec.js +18 -0
  41. package/dist/tests/8.9/web-modeler-user-flows.spec.js +6 -2
  42. package/dist/utils/consoleApiHelpers.d.ts +2 -0
  43. package/dist/utils/consoleApiHelpers.js +50 -4
  44. package/dist/utils/resetSession.js +6 -1
  45. package/package.json +1 -1
@@ -19,11 +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>;
24
25
  private expandHubClusterGroup;
25
26
  private showAllWorkspaces;
26
27
  private clickHubClusterApp;
28
+ goBackToHub(): Promise<void>;
27
29
  private isOnHub;
28
30
  clickModeler(): Promise<void>;
29
31
  private clickHubModelerHome;
@@ -54,8 +56,10 @@ declare class AppsPage {
54
56
  clickCamundaApps(maxRetries?: number): Promise<void>;
55
57
  private dismissBlockingModal;
56
58
  clickConsoleLink(): Promise<void>;
57
- assertOperatePresent(shouldBeVisible?: boolean): Promise<void>;
58
- assertTasklistPresent(shouldBeVisible?: boolean): Promise<void>;
59
- assertOptimizePresent(shouldBeVisible?: boolean): Promise<void>;
59
+ private hubClusterAppLink;
60
+ private assertHubClusterAppPresent;
61
+ assertOperatePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
62
+ assertTasklistPresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
63
+ assertOptimizePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
60
64
  }
61
65
  export { AppsPage };
@@ -27,9 +27,19 @@ class AppsPage {
27
27
  camundaComponentsButton;
28
28
  camundaAppsButton;
29
29
  hubSidebar;
30
+ hubReturnUrl;
30
31
  constructor(page) {
31
32
  this.page = page;
32
- 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 clickHubModelerHome() uses
35
+ // (NavShellDs.common.tsx). The final .first() keeps this strict-safe on a
36
+ // page that carries both.
37
+ this.modelerLink = page
38
+ .getByRole('link', { name: 'Modeler', exact: true })
39
+ .or(page
40
+ .locator('header[data-slot="app-header"]')
41
+ .getByRole('link', { name: /^Camunda (Modeler|Hub) home$/ }))
42
+ .first();
33
43
  this.appSwitcherButton = page.getByLabel('App Switcher');
34
44
  this.tasklistLink = page.getByRole('link', { name: 'Tasklist', exact: true });
35
45
  this.identityLink = page.getByRole('link', { name: /^(Admin|Identity)$/ });
@@ -253,14 +263,8 @@ class AppsPage {
253
263
  // cluster name and portalled OUTSIDE the "Main navigation" <nav> -- so a
254
264
  // sidebar-scoped lookup finds nothing there. Look in that popover first;
255
265
  // keep the sidebar-scoped lookup for the expanded-sidebar layout, where
256
- // the links render inline. Not exact: the rendered accessible name
257
- // carries more than the bare app name (e.g. "Operate in <cluster>
258
- // (opens in a new tab)").
259
- const appLink = this.page
260
- .getByRole('dialog', { name: clusterName, exact: true })
261
- .getByRole('link', { name: appName })
262
- .or(this.hubSidebar.getByRole('link', { name: appName }))
263
- .first();
266
+ // the links render inline.
267
+ const appLink = this.hubClusterAppLink(clusterName, appName);
264
268
  // Decide whether to expand from clusterName's OWN toggle state, not from
265
269
  // whether "some" appLink is already visible sidebar-wide: in a
266
270
  // multi-cluster org another cluster's group can already be expanded with
@@ -279,11 +283,22 @@ class AppsPage {
279
283
  // navigation the Console app switcher gave these callers.
280
284
  const href = await appLink.getAttribute('href');
281
285
  if (href) {
286
+ this.hubReturnUrl = this.page.url();
282
287
  await this.page.goto(new URL(href, this.page.url()).toString());
283
288
  return;
284
289
  }
285
290
  await appLink.click({ timeout: 10000 });
286
291
  }
292
+ // Returns from an app that clickHubClusterApp opened. page.goBack() alone
293
+ // can stop on one of the app's own login-redirect history entries, and the
294
+ // next Hub sidebar lookup then runs against the app instead of Hub.
295
+ async goBackToHub() {
296
+ await this.page.goBack();
297
+ if (this.hubReturnUrl &&
298
+ !new URL(this.page.url()).hostname.startsWith('hub.')) {
299
+ await this.page.goto(this.hubReturnUrl);
300
+ }
301
+ }
287
302
  // True once the tab is on a Camunda Hub host, where Modeler home is Hub's
288
303
  // own home rather than a modeler.* hostname.
289
304
  isOnHub() {
@@ -459,10 +474,7 @@ class AppsPage {
459
474
  }
460
475
  async clickIdentity(clusterName) {
461
476
  if (new URL(this.page.url()).hostname.startsWith('hub.')) {
462
- // Non-anchored, matching the substring lookup clickHubClusterApp uses
463
- // for Operate/Tasklist/Optimize -- an anchored pattern fails the same
464
- // way an exact-string match did for those apps if the rendered link's
465
- // accessible name carries any extra content (icon/badge).
477
+ // Identity should be removed once camunda/camunda#46447 is fixed.
466
478
  await this.clickHubClusterApp(clusterName, /Admin|Identity/);
467
479
  return;
468
480
  }
@@ -716,17 +728,26 @@ class AppsPage {
716
728
  .catch(() => { });
717
729
  }
718
730
  async clickConsoleLink() {
719
- // Camunda Hub replaced Console on SaaS: there is no "Console" entry in a
720
- // switcher, and the org-scoped Console area is the sidebar's
721
- // Clusters/Environments section. Going there is what callers assert on
722
- // (HomePage.consoleBanner resolves to Hub's authenticated header).
731
+ // Camunda Hub folded Console into the same persistent shell as
732
+ // Modeler/home: Console's Dashboard/Clusters/Organization links are just
733
+ // another section of the same "Main navigation" sidebar, not a separate
734
+ // app to switch into. Its header banner (homePage.consoleBanner, via
735
+ // hubHeader) is already visible from any Hub page, so there is no
736
+ // navigation action needed here -- mirrors the same hostname guard in
737
+ // clickModeler(). Clicking the sidebar's Clusters/Environments link is
738
+ // not just unnecessary but wrong: Hub shows that section only to org
739
+ // admins, so requiring it broke every RBA step that reaches here as a
740
+ // handed-off non-admin test user. Callers that do need the Clusters page
741
+ // call clickClusters() themselves; the rest assert consoleBanner.
742
+ //
743
+ // The diagram editor is the exception: it has no "Main navigation"
744
+ // sidebar at all, so a caller arriving from "Deploy & run" would find
745
+ // nothing here. Go home first there.
723
746
  if (new URL(this.page.url()).hostname.startsWith('hub.')) {
724
- const consoleArea = this.hubSidebar
725
- .locator('a[href$="/clusters"]')
726
- .or(this.hubSidebar.locator('a[href$="/environments"]'))
727
- .first();
728
- await (0, test_1.expect)(consoleArea).toBeVisible({ timeout: 30000 });
729
- await consoleArea.click({ timeout: 30000 });
747
+ if (!(await this.hubSidebar.isVisible().catch(() => false))) {
748
+ await this.clickModeler();
749
+ await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
750
+ }
730
751
  return;
731
752
  }
732
753
  const maxRetries = 3;
@@ -750,7 +771,46 @@ class AppsPage {
750
771
  }
751
772
  throw new Error(`Failed to click the console link after ${maxRetries} attempts.`);
752
773
  }
753
- async assertOperatePresent(shouldBeVisible = true) {
774
+ // Hub's replacement for the app switcher: a cluster's apps are the child
775
+ // links of its own sidebar group. Hub names every app link "<app> in
776
+ // <environment>", plus " (opens in a new tab)" when it leads to the live app
777
+ // (camunda-hub environment-sidebar-rows.ts) -- the nightly traces carry both
778
+ // shapes ("Operate in Third RBA V2 Cluster", "Admin in Admin Role Cluster
779
+ // (opens in a new tab)"). Anchor both halves rather than matching the bare
780
+ // app name: the flyout's first link is "Details of <environment>", so for
781
+ // "Admin Role Cluster" a bare /Admin|Identity/ resolved to that link, and
782
+ // inline it resolved to "Operate in Admin Role Cluster (opens in a new
783
+ // tab)", which precedes the Admin link. Anchoring on the environment also
784
+ // stops "Test Cluster" matching "UI Test Cluster".
785
+ hubClusterAppLink(clusterName, appName) {
786
+ const appPattern = typeof appName === 'string' ? escapeRegExp(appName) : appName.source;
787
+ const cluster = escapeRegExp(clusterName);
788
+ const appLinkName = new RegExp(`^(?:${appPattern}) in ${cluster}(?: \\(opens in a new tab\\))?$`);
789
+ return this.page
790
+ .getByRole('dialog', { name: clusterName, exact: true })
791
+ .getByRole('link', { name: appLinkName })
792
+ .or(this.hubSidebar.getByRole('link', { name: appLinkName }))
793
+ .first();
794
+ }
795
+ async assertHubClusterAppPresent(clusterName, appName, shouldBeVisible) {
796
+ const appLink = this.hubClusterAppLink(clusterName, appName);
797
+ // Not expanded for an absence check: expandHubClusterGroup throws when
798
+ // the user cannot see the environment at all, which is a pass here.
799
+ if (!shouldBeVisible) {
800
+ await (0, test_1.expect)(appLink).toBeHidden({ timeout: 30000 });
801
+ return;
802
+ }
803
+ await this.expandHubClusterGroup(clusterName);
804
+ await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
805
+ }
806
+ // On Hub the cluster name is what scopes the lookup: there is no app
807
+ // switcher panel listing apps org-wide, only each cluster's own sidebar
808
+ // group.
809
+ async assertOperatePresent(clusterName, shouldBeVisible = true) {
810
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
811
+ await this.assertHubClusterAppPresent(clusterName, 'Operate', shouldBeVisible);
812
+ return;
813
+ }
754
814
  const maxRetries = 3;
755
815
  for (let retries = 0; retries < maxRetries; retries++) {
756
816
  try {
@@ -779,7 +839,12 @@ class AppsPage {
779
839
  }
780
840
  throw new Error(`Failed to assert the operate link visibility after ${maxRetries} attempts.`);
781
841
  }
782
- async assertTasklistPresent(shouldBeVisible = true) {
842
+ // See assertOperatePresent for the clusterName/Hub note.
843
+ async assertTasklistPresent(clusterName, shouldBeVisible = true) {
844
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
845
+ await this.assertHubClusterAppPresent(clusterName, 'Tasklist', shouldBeVisible);
846
+ return;
847
+ }
783
848
  const maxRetries = 3;
784
849
  for (let retries = 0; retries < maxRetries; retries++) {
785
850
  try {
@@ -808,7 +873,12 @@ class AppsPage {
808
873
  }
809
874
  throw new Error(`Failed to assert the tasklist link visibility after ${maxRetries} attempts.`);
810
875
  }
811
- async assertOptimizePresent(shouldBeVisible = true) {
876
+ // See assertOperatePresent for the clusterName/Hub note.
877
+ async assertOptimizePresent(clusterName, shouldBeVisible = true) {
878
+ if (new URL(this.page.url()).hostname.startsWith('hub.')) {
879
+ await this.assertHubClusterAppPresent(clusterName, 'Optimize', shouldBeVisible);
880
+ return;
881
+ }
812
882
  const maxRetries = 3;
813
883
  for (let retries = 0; retries < maxRetries; retries++) {
814
884
  try {
@@ -103,6 +103,8 @@ declare class ModelerCreatePage {
103
103
  readonly timerValue: Locator;
104
104
  readonly eighthPlacedElement: Locator;
105
105
  readonly deployAndRunMainButton: Locator;
106
+ readonly deployRunActions: Locator;
107
+ readonly deployAndRunMenuItem: Locator;
106
108
  readonly deployAndRunSubButton: Locator;
107
109
  readonly dialog: Locator;
108
110
  readonly backupDeployButton: Locator;
@@ -141,6 +143,7 @@ declare class ModelerCreatePage {
141
143
  clickAdditionalActionsButton(): Promise<void>;
142
144
  clickDeployMenuItem(): Promise<void>;
143
145
  deployDiagram(clusterName: string): Promise<void>;
146
+ assertDeployAndRunNotPermitted(): Promise<void>;
144
147
  clickStartInstanceMainButton(): Promise<void>;
145
148
  clickStartInstanceSubButton(): Promise<void>;
146
149
  runProcessInstance(clusterName: string, variables?: string, formVariables?: {
@@ -115,6 +115,8 @@ class ModelerCreatePage {
115
115
  timerValue;
116
116
  eighthPlacedElement;
117
117
  deployAndRunMainButton;
118
+ deployRunActions;
119
+ deployAndRunMenuItem;
118
120
  deployAndRunSubButton;
119
121
  dialog;
120
122
  backupDeployButton;
@@ -160,6 +162,13 @@ class ModelerCreatePage {
160
162
  name: 'Deploy & run',
161
163
  exact: true,
162
164
  });
165
+ // camunda-hub deploy-run-combo-action: both the Carbon ComboButton and
166
+ // the design-system SplitButton carry this test id.
167
+ this.deployRunActions = page.getByTestId('deploy-run-actions');
168
+ this.deployAndRunMenuItem = page.getByRole('menuitem', {
169
+ name: 'Deploy & run',
170
+ exact: true,
171
+ });
163
172
  this.deployAndRunSubButton = this.dialog.getByRole('button', {
164
173
  name: 'Deploy & run',
165
174
  exact: true,
@@ -674,6 +683,22 @@ class ModelerCreatePage {
674
683
  await (0, test_1.expect)(this.viewProcessLink).toBeVisible({ timeout: 30000 });
675
684
  await (0, test_1.expect)(this.viewProcessLink).toBeHidden({ timeout: 30000 });
676
685
  }
686
+ // For a user whose org role lacks instance/workflow create permissions,
687
+ // Hub's checkIfAllowedToRun is false: the split button's primary action
688
+ // becomes "Deploy" and "Deploy & run" moves into its menu, disabled
689
+ // (camunda-hub BPMNDeployAndRunButtonsDs.tsx, deploy-run-permission.saas.ts).
690
+ // The Console-era expectation of a deploy that is attempted and then fails
691
+ // cannot happen, so assert the gate itself.
692
+ async assertDeployAndRunNotPermitted() {
693
+ await (0, test_1.expect)(this.deployRunActions.getByRole('button', { name: 'Deploy', exact: true })).toBeVisible({ timeout: 60000 });
694
+ await (0, test_1.expect)(this.deployAndRunMainButton).toHaveCount(0);
695
+ await this.deployRunActions
696
+ .getByRole('button', { name: 'Additional actions' })
697
+ .click({ timeout: 30000 });
698
+ await (0, test_1.expect)(this.deployAndRunMenuItem).toBeVisible({ timeout: 30000 });
699
+ await (0, test_1.expect)(this.deployAndRunMenuItem).toBeDisabled();
700
+ await this.page.keyboard.press('Escape');
701
+ }
677
702
  async clickStartInstanceMainButton() {
678
703
  // ensureStageCluster runs immediately before this on every runProcessInstance
679
704
  // attempt and navigates twice, so the diagram's action bar may belong to a
@@ -83,8 +83,13 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
83
83
  // for the tab's whole lifetime; without clearing it here alongside
84
84
  // cookies/storage, a flow that logs a different user into the same
85
85
  // page could have getUserToken() return the PREVIOUS user's token.
86
- (0, consoleApiHelpers_1.resetHubBearerToken)(page);
86
+ // Listen from before Hub loads: an API helper called later on an idle
87
+ // Hub page would otherwise find no request carrying the token.
88
+ (0, consoleApiHelpers_1.observeBearerTokens)(page);
87
89
  await page.goto('about:blank');
90
+ // After about:blank: until then the previous user's Hub page can still
91
+ // send a request, and its token must not count for the new session.
92
+ (0, consoleApiHelpers_1.resetHubBearerToken)(page);
88
93
  await page
89
94
  .evaluate(() => {
90
95
  try {
@@ -409,24 +414,36 @@ async function enableAuthorizations(clusterName, homePage, clusterPage, clusterD
409
414
  }
410
415
  exports.enableAuthorizations = enableAuthorizations;
411
416
  async function assertLatestAlertEmail(id, mailSlurp, processName, alertText) {
412
- const maxRetries = 3;
413
- for (let retries = 0; retries < maxRetries; retries++) {
414
- try {
415
- const email = await mailSlurp.waitForLatestEmail(id, 120000);
416
- if (email && email.body) {
417
- (0, test_1.expect)(email.body).toContain(processName);
418
- (0, test_1.expect)(email.body).toContain(alertText);
419
- return email.body;
420
- }
421
- else {
422
- throw new Error('Email or email body is null or undefined.');
417
+ // Search the whole inbox for an email carrying alertText rather than asking
418
+ // for the "latest" one. waitForLatestEmail is documented as returning the
419
+ // last received email immediately when the inbox is NOT empty ("If you need
420
+ // to wait for an email for a non-empty inbox see the other receive methods"
421
+ // -- mailslurp-client's own JSDoc), and the alert flow calls this twice on
422
+ // the same inbox: the second call kept resolving to the first alert's mail
423
+ // ("New Incidents: 1") and failed its "New Incidents: 2" assertion on every
424
+ // retry, since retrying re-read the same already-delivered email. Matching
425
+ // on content also tolerates the alert sending more mails than the two this
426
+ // flow asserts on, and ordering between them.
427
+ let body;
428
+ await (0, test_1.expect)(async () => {
429
+ // minCount/retryTimeout make the server itself hold the request open until
430
+ // the inbox has grown, so this does not busy-poll between attempts.
431
+ const previews = await mailSlurp.getEmails(id, {
432
+ minCount: 1,
433
+ retryTimeout: 60000,
434
+ });
435
+ for (const preview of previews) {
436
+ const email = await mailSlurp.getEmail(preview.id);
437
+ if (email?.body?.includes(processName) &&
438
+ email.body.includes(alertText)) {
439
+ body = email.body;
440
+ return;
423
441
  }
424
442
  }
425
- catch (error) {
426
- console.warn(`Click attempt ${retries + 1} failed: ${error}`);
427
- }
428
- }
429
- throw new Error(`Failed to get latest alert (${processName}) email for ${id} after ${maxRetries} attempts.`);
443
+ throw new Error(`No alert email for "${processName}" containing "${alertText}" in ` +
444
+ `inbox ${id} yet (${previews.length} mail(s) present)`);
445
+ }).toPass({ timeout: 360000, intervals: [5000, 10000, 20000] });
446
+ return body;
430
447
  }
431
448
  exports.assertLatestAlertEmail = assertLatestAlertEmail;
432
449
  async function clickInvitationLinkInEmail(page, id, mailSlurp) {
@@ -10,6 +10,7 @@ declare class WorkspaceMembersPage {
10
10
  constructor(page: Page);
11
11
  showAllWorkspaces(): Promise<void>;
12
12
  openManageMembers(workspaceName: string): Promise<void>;
13
+ workspaceId(): string;
13
14
  private memberRow;
14
15
  isMember(email: string): Promise<boolean>;
15
16
  addMember(email: string, role: WorkspaceRole): Promise<void>;
@@ -106,6 +106,14 @@ class WorkspaceMembersPage {
106
106
  await this.membersTab.click({ timeout: 30000 });
107
107
  }
108
108
  }
109
+ // Valid once openManageMembers has landed on /workspaces/<id>/manage.
110
+ workspaceId() {
111
+ const id = this.page.url().match(/\/workspaces\/([^/?#]+)\/manage/)?.[1];
112
+ if (!id) {
113
+ throw new Error(`Not on a workspace Manage page, cannot read its id: ${this.page.url()}`);
114
+ }
115
+ return decodeURIComponent(id);
116
+ }
109
117
  memberRow(email) {
110
118
  return this.page.getByRole('row').filter({ hasText: email }).first();
111
119
  }
@@ -229,12 +237,11 @@ class WorkspaceMembersPage {
229
237
  return;
230
238
  }
231
239
  const row = this.memberRow(email);
232
- // The destructive action lives behind the row's own overflow menu; its
233
- // trigger is an icon button, so it is matched on its accessible name
234
- // rather than on text.
240
+ // camunda-hub MembersTable.tsx: the row's icon trigger is
241
+ // aria-label="Actions for <name>", data-test="member-actions-trigger".
235
242
  await row
236
- .getByRole('button', { name: /(more|actions|options|menu)/i })
237
- .or(row.locator('[data-test="member-row-actions"]'))
243
+ .locator('[data-test="member-actions-trigger"]')
244
+ .or(row.getByRole('button', { name: /^Actions for / }))
238
245
  .first()
239
246
  .click({ timeout: 30000 });
240
247
  await this.page
@@ -242,11 +249,15 @@ class WorkspaceMembersPage {
242
249
  .or(this.page.getByRole('option', { name: /^Remove( member)?$/i }))
243
250
  .first()
244
251
  .click({ timeout: 30000 });
245
- // Removal is confirmed in a dialog. Scoped to it so the match cannot
246
- // resolve back to the menu item that opened it.
247
- const dialog = this.page.getByRole('dialog').first();
248
- await dialog
249
- .getByRole('button', { name: /^(Remove|Confirm|Delete)$/i })
252
+ // RemoveMemberDialog.tsx is a Radix AlertDialog, i.e. role="alertdialog",
253
+ // which getByRole('dialog') does NOT match -- the confirm button was
254
+ // unreachable. Prefer its own test id and keep the role lookup as a
255
+ // fallback.
256
+ await this.page
257
+ .locator('[data-test="remove-member-confirm"]')
258
+ .or(this.page
259
+ .getByRole('alertdialog', { name: 'Remove member' })
260
+ .getByRole('button', { name: 'Remove', exact: true }))
250
261
  .first()
251
262
  .click({ timeout: 30000 });
252
263
  await (0, test_1.expect)(row).toBeHidden({ timeout: 60000 });
@@ -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 };