@camunda/e2e-test-suite 0.0.1288 → 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.
@@ -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 });
@@ -7,6 +7,7 @@ const sleep_1 = require("../../utils/sleep");
7
7
  const resetSession_1 = require("../../utils/resetSession");
8
8
  const LoginPage_1 = require("../../pages/8.8/LoginPage");
9
9
  const WorkspaceMembersPage_1 = require("../../pages/8.8/WorkspaceMembersPage");
10
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
10
11
  const test_1 = require("@playwright/test");
11
12
  const users_1 = require("../../utils/users");
12
13
  const testUser = (0, users_1.getTestUser)('mainUser');
@@ -47,18 +48,20 @@ _8_8_1.test.describe('User Roles User Flow', () => {
47
48
  // Editor and its own "not reachable without workspace access" assertion
48
49
  // would fail on the second attempt; the grant would also leak into every
49
50
  // later nightly run against the same org.
50
- _8_8_1.test.afterEach(async ({ page, browser }, testInfo) => {
51
+ _8_8_1.test.afterEach(async ({ page, loginPage }, testInfo) => {
51
52
  if (!testInfo.title.startsWith('Analyst Role User Flow')) {
52
53
  return;
53
54
  }
54
55
  if (!analystWorkspaceGrantMade) {
55
56
  return;
56
57
  }
57
- await (0, resetSession_1.resetSession)(browser, page);
58
- await new LoginPage_1.LoginPage(page).loginWithTestUser({
59
- username: process.env.C8_USERNAME,
60
- password: process.env.C8_PASSWORD,
61
- });
58
+ // Leave Hub before the analyst's session is dropped. A Hub deep link
59
+ // reloaded signed out is persisted as the next login's destination
60
+ // (camunda-hub auth-service.saas.ts persistReturnTo), so the admin landed
61
+ // on the analyst's diagram, whose sidebar has no org link for
62
+ // assertTestUsesCorrectOrganization to find.
63
+ await page.goto('about:blank');
64
+ await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, testUser, (testInfo.workerIndex + 1) * 1000);
62
65
  const workspaceMembersPage = new WorkspaceMembersPage_1.WorkspaceMembersPage(page);
63
66
  await workspaceMembersPage.openManageMembers(analystWorkspaceName);
64
67
  await workspaceMembersPage.removeMember(process.env.C8_USERNAME_TEST_9);
@@ -98,9 +101,9 @@ _8_8_1.test.describe('User Roles User Flow', () => {
98
101
  timeout: 60000,
99
102
  });
100
103
  await appsPage.clickCamundaApps();
101
- await appsPage.assertOptimizePresent();
102
- await appsPage.assertOperatePresent();
103
- await appsPage.assertTasklistPresent();
104
+ await appsPage.assertOptimizePresent(clusterName);
105
+ await appsPage.assertOperatePresent(clusterName);
106
+ await appsPage.assertTasklistPresent(clusterName);
104
107
  await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
105
108
  await appsPage.clickCamundaApps();
106
109
  });
@@ -112,7 +115,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
112
115
  });
113
116
  });
114
117
  await _8_8_1.test.step('Navigate to Tasklist and Ensure Tasklist is not Accessible', async () => {
115
- await page.goBack();
118
+ await appsPage.goBackToHub();
116
119
  await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
117
120
  await appsPage.clickCamundaApps();
118
121
  await appsPage.clickTasklist(clusterName);
@@ -121,7 +124,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
121
124
  });
122
125
  });
123
126
  await _8_8_1.test.step('Navigate to Identity and Ensure Identity is not Accessible', async () => {
124
- await page.goBack();
127
+ await appsPage.goBackToHub();
125
128
  await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
126
129
  await appsPage.clickCamundaApps();
127
130
  await appsPage.clickIdentity(clusterName);
@@ -130,7 +133,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
130
133
  });
131
134
  });
132
135
  await _8_8_1.test.step('Navigate to Optimize and Ensure Optimize is Not Accessible', async () => {
133
- await page.goBack();
136
+ await appsPage.goBackToHub();
134
137
  await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
135
138
  await appsPage.clickCamundaApps();
136
139
  await appsPage.clickOptimize(clusterName);
@@ -139,7 +142,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
139
142
  });
140
143
  });
141
144
  await _8_8_1.test.step('Navigate to Modeler and Ensure Diagram Failed To Deploy', async () => {
142
- await page.goBack();
145
+ await appsPage.goBackToHub();
143
146
  await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
144
147
  await appsPage.clickCamundaApps();
145
148
  await appsPage.clickModeler();
@@ -166,6 +169,19 @@ _8_8_1.test.describe('User Roles User Flow', () => {
166
169
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
167
170
  const processName = 'User Roles User Flow' + randomString;
168
171
  const clusterName = clusterNames['Analyst Role User Flow'];
172
+ // A retry runs in a fresh worker, where analystWorkspaceGrantMade starts
173
+ // false even when the previous attempt's teardown failed to hand the grant
174
+ // back -- which is exactly what happens when that teardown dies on its
175
+ // admin re-login. The "not reachable without workspace access" step below
176
+ // would then always fail, because the analyst is still an Editor.
177
+ if (_8_8_1.test.info().retry > 0) {
178
+ await _8_8_1.test.step('Hand back a workspace grant left by the previous attempt', async () => {
179
+ await appsPage.clickModeler();
180
+ const workspaceMembersPage = new WorkspaceMembersPage_1.WorkspaceMembersPage(page);
181
+ await workspaceMembersPage.openManageMembers(analystWorkspaceName);
182
+ await workspaceMembersPage.removeMember(process.env.C8_USERNAME_TEST_9);
183
+ });
184
+ }
169
185
  await _8_8_1.test.step('Navigate to Console and Enable RBA', async () => {
170
186
  await (0, UtilitiesPage_1.enableAuthorizations)(clusterName, homePage, clusterPage, clusterDetailsPage, appsPage);
171
187
  await (0, sleep_1.sleep)(15000);
@@ -225,6 +241,9 @@ _8_8_1.test.describe('User Roles User Flow', () => {
225
241
  const hubSidebar = page.getByRole('navigation', {
226
242
  name: 'Main navigation',
227
243
  });
244
+ // The negative counts below mean nothing before the shell has rendered:
245
+ // toHaveCount(0) passes vacuously on a page that has not painted yet.
246
+ await (0, test_1.expect)(hubSidebar).toBeVisible({ timeout: 30000 });
228
247
  await (0, test_1.expect)(hubSidebar.getByRole('button', { name: clusterName })).toHaveCount(0, { timeout: 30000 });
229
248
  await (0, test_1.expect)(hubSidebar.getByRole('link', { name: /Optimize/ })).toHaveCount(0, { timeout: 30000 });
230
249
  await (0, test_1.expect)(optimizeHomePage.optimizeBanner).not.toBeVisible({
@@ -247,6 +266,14 @@ _8_8_1.test.describe('User Roles User Flow', () => {
247
266
  // clean that up too.
248
267
  analystWorkspaceGrantMade = true;
249
268
  await workspaceMembersPage.setMemberRole(process.env.C8_USERNAME_TEST_9, 'Editor');
269
+ // Editorship alone surfaces nothing: an environment is reachable only
270
+ // when it is assigned to a workspace the user is Editor or Workspace
271
+ // Admin on, and nothing else in this flow assigns this cluster's
272
+ // environment to the shared workspace. Without it the analyst holds the
273
+ // membership but the sidebar still renders no group for the cluster,
274
+ // which is where the nightly timed out (expandHubClusterGroup waiting on
275
+ // button "Analyst Role Cluster").
276
+ await (0, consoleApiHelpers_1.linkClusterToWorkspaceViaApi)(page, workspaceMembersPage.workspaceId(), clusterName);
250
277
  await (0, resetSession_1.resetSession)(browser, page);
251
278
  await new LoginPage_1.LoginPage(page).loginWithTestUser({
252
279
  username: process.env.C8_USERNAME_TEST_9,
@@ -287,9 +314,9 @@ _8_8_1.test.describe('User Roles User Flow', () => {
287
314
  timeout: 60000,
288
315
  });
289
316
  await appsPage.clickCamundaApps();
290
- await appsPage.assertOptimizePresent();
291
- await appsPage.assertOperatePresent();
292
- await appsPage.assertTasklistPresent();
317
+ await appsPage.assertOptimizePresent(clusterName);
318
+ await appsPage.assertOperatePresent(clusterName);
319
+ await appsPage.assertTasklistPresent(clusterName);
293
320
  await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
294
321
  await appsPage.clickCamundaApps();
295
322
  });
@@ -299,7 +326,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
299
326
  await (0, UtilitiesPage_1.assertLocatorVisibleWithRetry)(page, operateHomePage.noPermissionsMessage, 'Operate No Permission message');
300
327
  });
301
328
  await _8_8_1.test.step('Navigate to Tasklist and Ensure Tasklist is not Accessible', async () => {
302
- await page.goBack();
329
+ await appsPage.goBackToHub();
303
330
  await appsPage.clickCamundaApps();
304
331
  await appsPage.clickTasklist(clusterName);
305
332
  await (0, test_1.expect)(taskPanelPage.noPermissionsMessage).toBeVisible({
@@ -307,7 +334,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
307
334
  });
308
335
  });
309
336
  await _8_8_1.test.step('Navigate to Identity and Ensure Identity is not Accessible', async () => {
310
- await page.goBack();
337
+ await appsPage.goBackToHub();
311
338
  await appsPage.clickCamundaApps();
312
339
  await appsPage.clickIdentity(clusterName);
313
340
  await (0, test_1.expect)(ocIdentityHomePage.noPermissionsMessage).toBeVisible({
@@ -315,14 +342,14 @@ _8_8_1.test.describe('User Roles User Flow', () => {
315
342
  });
316
343
  });
317
344
  await _8_8_1.test.step('Navigate to Optimize and Ensure Optimize is Accessible', async () => {
318
- await page.goBack();
345
+ await appsPage.goBackToHub();
319
346
  await appsPage.clickCamundaApps();
320
347
  await appsPage.clickOptimize(clusterName);
321
348
  await (0, test_1.expect)(optimizeHomePage.optimizeBanner).toBeVisible({
322
349
  timeout: 60000,
323
350
  });
324
351
  });
325
- await _8_8_1.test.step('Navigate to Modeler and Ensure Diagram Failed To Deploy', async () => {
352
+ await _8_8_1.test.step('Navigate to Modeler and Ensure Diagram Cannot Be Deployed & Run', async () => {
326
353
  await appsPage.clickCamundaApps();
327
354
  await appsPage.clickModeler();
328
355
  await homePage.clickSkipCustomization();
@@ -339,9 +366,10 @@ _8_8_1.test.describe('User Roles User Flow', () => {
339
366
  await modelerCreatePage.clickAppendElementButton();
340
367
  await modelerCreatePage.clickAppendEndEventButton();
341
368
  await (0, sleep_1.sleep)(20000);
342
- await modelerCreatePage.runProcessInstance(clusterName, '', {}, '', true);
343
- await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).toBeVisible({ timeout: 60000 });
344
- await modelerCreatePage.clickCloseButton();
369
+ // Hub gates this in the UI: an Analyst holds no instance or workflow
370
+ // create permission, so "Deploy & run" is offered only disabled, where
371
+ // Console let the attempt through and reported a failed deployment.
372
+ await modelerCreatePage.assertDeployAndRunNotPermitted();
345
373
  });
346
374
  });
347
375
  (0, _8_8_1.test)('Admin Role User Flow', async ({ page, modelerHomePage, appsPage, homePage, clusterPage, clusterDetailsPage, browser, consoleOrganizationsPage, settingsPage, operateHomePage, ocIdentityHomePage, taskPanelPage, optimizeHomePage, modelerCreatePage, }) => {
@@ -403,9 +431,9 @@ _8_8_1.test.describe('User Roles User Flow', () => {
403
431
  timeout: 60000,
404
432
  });
405
433
  await appsPage.clickCamundaApps();
406
- await appsPage.assertOptimizePresent();
407
- await appsPage.assertOperatePresent();
408
- await appsPage.assertTasklistPresent();
434
+ await appsPage.assertOptimizePresent(clusterName);
435
+ await appsPage.assertOperatePresent(clusterName);
436
+ await appsPage.assertTasklistPresent(clusterName);
409
437
  await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
410
438
  await appsPage.clickCamundaApps();
411
439
  });
@@ -417,7 +445,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
417
445
  });
418
446
  });
419
447
  await _8_8_1.test.step('Navigate to Tasklist and Ensure Tasklist is Accessible', async () => {
420
- await page.goBack();
448
+ await appsPage.goBackToHub();
421
449
  await appsPage.clickCamundaApps();
422
450
  await appsPage.clickTasklist(clusterName);
423
451
  await (0, test_1.expect)(taskPanelPage.taskListPageBanner).toBeVisible({
@@ -425,7 +453,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
425
453
  });
426
454
  });
427
455
  await _8_8_1.test.step('Navigate to Identity and Ensure Identity is Accessible', async () => {
428
- await page.goBack();
456
+ await appsPage.goBackToHub();
429
457
  await appsPage.clickCamundaApps();
430
458
  await appsPage.clickIdentity(clusterName);
431
459
  await (0, test_1.expect)(ocIdentityHomePage.identityBanner).toBeVisible({
@@ -436,7 +464,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
436
464
  });
437
465
  });
438
466
  await _8_8_1.test.step('Navigate to Optimize and Ensure Optimize is Accessible', async () => {
439
- await page.goBack();
467
+ await appsPage.goBackToHub();
440
468
  await appsPage.clickCamundaApps();
441
469
  await appsPage.clickOptimize(clusterName);
442
470
  await (0, test_1.expect)(optimizeHomePage.optimizeBanner).toBeVisible({
@@ -192,7 +192,28 @@ _8_8_1.test.describe('Console User Flow Tests @tasklistV2', () => {
192
192
  await modelerHomePage.clickCrossComponentProjectFolder();
193
193
  await modelerHomePage.clickProcessDiagram(processName);
194
194
  await modelerCreatePage.runProcessInstance(alertFlowClusterName);
195
- await (0, sleep_1.sleep)(10000);
195
+ // Straight into the next run, no wait. The email assertion below expects
196
+ // ONE mail reporting "New Incidents: 2", which only happens when both
197
+ // incidents fall inside the same alert evaluation cycle; the 10s sleep
198
+ // that used to be here pushed them into separate cycles and Console sent
199
+ // a "New Incidents: 1" mail for each (observed live: three mails at
200
+ // 15:09:20, 15:11:51 and 15:13:02, all reporting 1).
201
+ //
202
+ // Close the first run's toast before clicking again: camunda-hub 6813a7e
203
+ // made the "Instance started!" toast persistent whenever it carries the
204
+ // Operate action, so it stays up and its action button covers
205
+ // "Deploy & run", swallowing the second click ("<button
206
+ // data-action=\"true\"> ... intercepts pointer events", observed live
207
+ // once the sleep was gone).
208
+ const instanceStartedToast = page
209
+ .locator('[data-sonner-toast]')
210
+ .filter({ has: modelerCreatePage.viewProcessInstanceLink });
211
+ await instanceStartedToast
212
+ .locator('[data-close-button="true"]')
213
+ .click({ timeout: 30000 });
214
+ await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeHidden({
215
+ timeout: 30000,
216
+ });
196
217
  await modelerCreatePage.runProcessInstance(alertFlowClusterName);
197
218
  });
198
219
  await _8_8_1.test.step('Navigate to Cluster Details', async () => {
@@ -9,6 +9,7 @@ const roleAuthorizations_1 = require("../../utils/roleAuthorizations");
9
9
  const expectTextWithRetry_1 = require("../../utils/assertionHelpers/expectTextWithRetry");
10
10
  const expectTextWithPagination_1 = require("../../utils/assertionHelpers/expectTextWithPagination");
11
11
  const users_1 = require("../../utils/users");
12
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
12
13
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
13
14
  const mainUser = (0, users_1.getTestUser)('twentyFifthUser');
14
15
  _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV1', () => {
@@ -19,19 +20,30 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV1', () => {
19
20
  'RBA On User Flow - Permission for Selected Processes Only': 'Fourth RBA V1 Cluster',
20
21
  };
21
22
  _8_8_1.test.beforeEach(async ({ page, homePage, clusterPage, loginPage, clusterDetailsPage, appsPage }, testInfo) => {
23
+ // Claimed here, not in each body: creating the cluster and waiting for
24
+ // it to come back Healthy after the authorizations toggle can outlast
25
+ // the default per-test timeout before the body starts.
26
+ _8_8_1.test.slow();
22
27
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, mainUser, (testInfo.workerIndex + 1) * 1000);
23
28
  await homePage.clickClusters();
24
29
  const clusterName = clusterNames[testInfo.title];
25
30
  (0, test_1.expect)(clusterName, `No cluster mapping for test title: "${testInfo.title}")`).toBeTruthy();
26
31
  await clusterPage.createClusterIfNotExists(clusterName);
27
32
  await (0, UtilitiesPage_1.disableRBA)(clusterName, homePage, clusterPage, clusterDetailsPage, appsPage);
33
+ // The flows below hand off to test users with no elevated or DevOps org
34
+ // role, and Hub only lets those roles assign an environment to a
35
+ // workspace (camunda-hub#28955). Without this, the test user's deploy
36
+ // dialog never offers this cluster and every runProcessInstance retry
37
+ // fails until the test times out. Assign it now, while the elevated
38
+ // main user's session is active. test-setup creates the test users'
39
+ // workspaces up front so that they exist at this point.
40
+ await (0, consoleApiHelpers_1.linkClusterToWorkspacesNamedViaApi)(page, clusterName);
28
41
  });
29
42
  _8_8_1.test.afterEach(async ({ page }, testInfo) => {
30
43
  await (0, _setup_1.captureScreenshot)(page, testInfo);
31
44
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
32
45
  });
33
46
  (0, _8_8_1.test)('RBA On User Flow - No User Permission', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, ocIdentityHomePage, ocIdentityAuthorizationsPage, ocIdentityRolesPage, loginPage, settingsPage, }, testInfo) => {
34
- _8_8_1.test.slow();
35
47
  const clusterName = clusterNames[testInfo.title];
36
48
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
37
49
  const processName = 'RBA_NO_PERM' + randomString;
@@ -153,7 +165,6 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV1', () => {
153
165
  });
154
166
  });
155
167
  (0, _8_8_1.test)('RBA On User Flow - Permission for One Process', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, loginPage, ocIdentityHomePage, ocIdentityAuthorizationsPage, ocIdentityRolesPage, settingsPage, }, testInfo) => {
156
- _8_8_1.test.slow();
157
168
  const clusterName = clusterNames[testInfo.title];
158
169
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
159
170
  const processName = 'RBA_ONE_PERM' + randomString;
@@ -305,7 +316,6 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV1', () => {
305
316
  });
306
317
  });
307
318
  (0, _8_8_1.test)('RBA On User Flow - Permission for All Processes with Full Permissions', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, ocIdentityHomePage, ocIdentityAuthorizationsPage, }, testInfo) => {
308
- _8_8_1.test.slow();
309
319
  const clusterName = clusterNames[testInfo.title];
310
320
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
311
321
  const processName = 'AFTER_RBA' + randomString;
@@ -375,7 +385,6 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV1', () => {
375
385
  });
376
386
  });
377
387
  (0, _8_8_1.test)('RBA On User Flow - Permission for Selected Processes Only', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, ocIdentityHomePage, ocIdentityAuthorizationsPage, ocIdentityRolesPage, loginPage, settingsPage, }, testInfo) => {
378
- _8_8_1.test.slow();
379
388
  const clusterName = clusterNames[testInfo.title];
380
389
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
381
390
  const processName = 'RBA_SELECTED_PERM' + randomString;
@@ -9,6 +9,7 @@ const roleAuthorizations_1 = require("../../utils/roleAuthorizations");
9
9
  const expectTextWithRetry_1 = require("../../utils/assertionHelpers/expectTextWithRetry");
10
10
  const expectTextWithPagination_1 = require("../../utils/assertionHelpers/expectTextWithPagination");
11
11
  const users_1 = require("../../utils/users");
12
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
12
13
  const mainUser = (0, users_1.getTestUser)('twentySixthUser');
13
14
  _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
14
15
  const clusterNames = {
@@ -18,6 +19,10 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
18
19
  'RBA On User Flow - Permission for Selected Processes Only': 'Fourth RBA V2 Cluster',
19
20
  };
20
21
  _8_8_1.test.beforeEach(async ({ page, homePage, clusterPage, loginPage, clusterDetailsPage, appsPage }, testInfo) => {
22
+ // Claimed here, not in each body: creating the cluster and waiting for
23
+ // it to come back Healthy after the authorizations toggle can outlast
24
+ // the default per-test timeout before the body starts.
25
+ _8_8_1.test.slow();
21
26
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, mainUser, (testInfo.workerIndex + 1) * 1000);
22
27
  await (0, test_1.expect)(homePage.clusterTab).toBeVisible({ timeout: 120000 });
23
28
  await homePage.clickClusters();
@@ -25,13 +30,20 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
25
30
  (0, test_1.expect)(clusterName, `No cluster mapping for test title: "${testInfo.title}")`).toBeTruthy();
26
31
  await clusterPage.createClusterIfNotExists(clusterName);
27
32
  await (0, UtilitiesPage_1.disableRBA)(clusterName, homePage, clusterPage, clusterDetailsPage, appsPage);
33
+ // The flows below hand off to test users with no elevated or DevOps org
34
+ // role, and Hub only lets those roles assign an environment to a
35
+ // workspace (camunda-hub#28955). Without this, the test user's deploy
36
+ // dialog never offers this cluster and every runProcessInstance retry
37
+ // fails until the test times out. Assign it now, while the elevated
38
+ // main user's session is active. test-setup creates the test users'
39
+ // workspaces up front so that they exist at this point.
40
+ await (0, consoleApiHelpers_1.linkClusterToWorkspacesNamedViaApi)(page, clusterName);
28
41
  });
29
42
  _8_8_1.test.afterEach(async ({ page }, testInfo) => {
30
43
  await (0, _setup_1.captureScreenshot)(page, testInfo);
31
44
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
32
45
  });
33
46
  (0, _8_8_1.test)('RBA On User Flow - No User Permission', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, ocIdentityHomePage, ocIdentityAuthorizationsPage, ocIdentityRolesPage, loginPage, settingsPage, }, testInfo) => {
34
- _8_8_1.test.slow();
35
47
  const clusterName = clusterNames[testInfo.title];
36
48
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
37
49
  const processName = 'RBA_NO_PERM' + randomString;
@@ -68,10 +80,19 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
68
80
  await ocIdentityAuthorizationsPage.createAuthorization((0, roleAuthorizations_1.authorizationAllPermissions)(role));
69
81
  });
70
82
  await _8_8_1.test.step('Clear cookies and reset session', async () => {
83
+ // Reset the session directly rather than driving Hub's user-menu
84
+ // logout: that menu is not reachable from every page this step can be
85
+ // reached from, and clickOpenSettingsButton then burns all three of its
86
+ // retries (nightly: "Failed to click button after 3 retries"). Clearing
87
+ // cookies and storage is what the step is actually for, and is what
88
+ // pages/8.9's copy of this spec does.
89
+ await page.context().clearCookies();
90
+ await page.evaluate(() => {
91
+ localStorage.clear();
92
+ sessionStorage.clear();
93
+ });
71
94
  await page.goto('/');
72
- await settingsPage.clickOpenSettingsButton();
73
- await settingsPage.clickLogoutButton();
74
- await loginPage.loginWithTestUser(testUser);
95
+ await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, testUser, 1000);
75
96
  });
76
97
  await _8_8_1.test.step('Navigate to Web Modeler', async () => {
77
98
  await appsPage.clickCamundaApps();
@@ -131,7 +152,6 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
131
152
  });
132
153
  });
133
154
  (0, _8_8_1.test)('RBA On User Flow - Permission for One Process', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, loginPage, ocIdentityHomePage, ocIdentityAuthorizationsPage, ocIdentityRolesPage, settingsPage, }, testInfo) => {
134
- _8_8_1.test.slow();
135
155
  const clusterName = clusterNames[testInfo.title];
136
156
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
137
157
  const processName = 'RBA_ONE_PERM' + randomString;
@@ -175,12 +195,21 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
175
195
  await ocIdentityAuthorizationsPage.createAuthorization((0, roleAuthorizations_1.roleAllAuthorization)(role1));
176
196
  });
177
197
  await _8_8_1.test.step('Clear cookies and reset session', async () => {
198
+ // Reset the session directly rather than driving Hub's user-menu
199
+ // logout: that menu is not reachable from every page this step can be
200
+ // reached from, and clickOpenSettingsButton then burns all three of its
201
+ // retries (nightly: "Failed to click button after 3 retries"). Clearing
202
+ // cookies and storage is what the step is actually for, and is what
203
+ // pages/8.9's copy of this spec does.
204
+ await page.context().clearCookies();
205
+ await page.evaluate(() => {
206
+ localStorage.clear();
207
+ sessionStorage.clear();
208
+ });
178
209
  await page.goto('/');
179
- await settingsPage.clickOpenSettingsButton();
180
- await settingsPage.clickLogoutButton();
181
210
  });
182
211
  await _8_8_1.test.step('Login as Test User', async () => {
183
- await loginPage.loginWithTestUser(testUser);
212
+ await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, testUser, 1000);
184
213
  });
185
214
  await _8_8_1.test.step('Navigate to Web Modeler', async () => {
186
215
  await appsPage.clickCamundaApps();
@@ -280,7 +309,6 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
280
309
  });
281
310
  });
282
311
  (0, _8_8_1.test)('RBA On User Flow - Permission for All Processes with Full Permissions', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, ocIdentityHomePage, ocIdentityAuthorizationsPage, }, testInfo) => {
283
- _8_8_1.test.slow();
284
312
  const clusterName = clusterNames[testInfo.title];
285
313
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
286
314
  const processName = 'AFTER_RBA' + randomString;
@@ -352,8 +380,7 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
352
380
  await (0, expectTextWithRetry_1.expectTextWithRetry)(page, process2);
353
381
  });
354
382
  });
355
- (0, _8_8_1.test)('RBA On User Flow - Permission for Selected Processes Only', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, ocIdentityHomePage, ocIdentityAuthorizationsPage, ocIdentityRolesPage, loginPage, settingsPage, }, testInfo) => {
356
- _8_8_1.test.slow();
383
+ (0, _8_8_1.test)('RBA On User Flow - Permission for Selected Processes Only', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, taskPanelPage, taskProcessesPage, operateHomePage, ocIdentityHomePage, ocIdentityAuthorizationsPage, ocIdentityRolesPage, loginPage, }, testInfo) => {
357
384
  const clusterName = clusterNames[testInfo.title];
358
385
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
359
386
  const processName = 'RBA_SELECTED_PERM' + randomString;
@@ -392,10 +419,19 @@ _8_8_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () => {
392
419
  await (0, sleep_1.sleep)(20000);
393
420
  });
394
421
  await _8_8_1.test.step('Clear cookies and reset session', async () => {
422
+ // Reset the session directly rather than driving Hub's user-menu
423
+ // logout: that menu is not reachable from every page this step can be
424
+ // reached from, and clickOpenSettingsButton then burns all three of its
425
+ // retries (nightly: "Failed to click button after 3 retries"). Clearing
426
+ // cookies and storage is what the step is actually for, and is what
427
+ // pages/8.9's copy of this spec does.
428
+ await page.context().clearCookies();
429
+ await page.evaluate(() => {
430
+ localStorage.clear();
431
+ sessionStorage.clear();
432
+ });
395
433
  await page.goto('/');
396
- await settingsPage.clickOpenSettingsButton();
397
- await settingsPage.clickLogoutButton();
398
- await loginPage.loginWithTestUser(testUser);
434
+ await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, testUser, 1000);
399
435
  });
400
436
  await _8_8_1.test.step('Navigate to Web Modeler', async () => {
401
437
  await appsPage.clickCamundaApps();
@@ -118,11 +118,56 @@ _8_8_1.test.describe('Cluster Setup Tests', () => {
118
118
  await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.connectorSecretsValues);
119
119
  }
120
120
  });
121
- for (const [index, user] of (0, users_1.getTestUsers)().entries()) {
122
- (0, _8_8_1.test)(`Create Project Folder for User ${index + 1}`, async ({ page, loginPage, homePage, appsPage, modelerHomePage, }, testInfo) => {
123
- await _8_8_1.test.step('Log in', async () => {
124
- await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
121
+ // The RBA specs hand off to these developer-role users, who cannot assign an
122
+ // environment to a workspace (camunda-hub#28955). Their workspaces must exist
123
+ // before the RBA beforeEach links its cluster to them as the admin main user.
124
+ const rbaHandoffUsers = process.env.IS_AG === 'true' && process.env.RUN_FULL_SUITE !== 'true'
125
+ ? []
126
+ : ['4', '5', '6']
127
+ .map((n) => ({
128
+ username: process.env[`C8_USERNAME_TEST_${n}`] ?? '',
129
+ password: process.env[`C8_PASSWORD_TEST_${n}`] ?? '',
130
+ title: `Create Project Folder for RBA hand-off user TEST_${n}`,
131
+ }))
132
+ .filter((user) => user.username !== '' && user.password !== '');
133
+ for (const [index, user] of [
134
+ ...(0, users_1.getTestUsers)().map((u, i) => ({
135
+ ...u,
136
+ title: `Create Project Folder for User ${i + 1}`,
137
+ })),
138
+ ...rbaHandoffUsers,
139
+ ].entries()) {
140
+ const isRbaHandoffUser = index >= (0, users_1.getTestUsers)().length;
141
+ (0, _8_8_1.test)(user.title, async ({ page, loginPage, homePage, appsPage, modelerHomePage }, testInfo) => {
142
+ const loggedIn = await _8_8_1.test.step('Log in', async () => {
143
+ try {
144
+ await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
145
+ return true;
146
+ }
147
+ catch (error) {
148
+ // Every spec in the project depends on this setup project, so a
149
+ // hand-off slot the org did not provision must not block the whole
150
+ // suite (an 8.9 PR run lost 162 tests to exactly that). Nothing but
151
+ // the RBA specs reads these three workspaces, and each of those logs
152
+ // in as its own user, so it fails on its own login instead.
153
+ //
154
+ // Any failure, not just a credential rejection: unlike 8.9/8.10,
155
+ // this version's LoginPage does not run classifyLoginRejection, so
156
+ // no AUTH0_INVALID_CREDENTIALS marker ever reaches here to match on.
157
+ if (!isRbaHandoffUser) {
158
+ throw error;
159
+ }
160
+ console.warn(`${user.title}: ${String(error)}`);
161
+ testInfo.annotations.push({
162
+ type: 'warning',
163
+ description: 'Could not log in as this hand-off user; no workspace',
164
+ });
165
+ return false;
166
+ }
125
167
  });
168
+ if (!loggedIn) {
169
+ return;
170
+ }
126
171
  await _8_8_1.test.step('Navigate to Web Modeler', async () => {
127
172
  // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
128
173
  // Modeler home directly and has no app switcher to wait for.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1288",
3
+ "version": "0.0.1289",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",