@camunda/e2e-test-suite 0.0.1110 → 0.0.1112

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.
@@ -116,7 +116,12 @@ class AppsPage {
116
116
  }
117
117
  }
118
118
  async clickCluster(component, name) {
119
- const cluster = component.getByRole('link', { name: name, exact: true });
119
+ // Cluster names are not unique in an org, so this app's sub-menu can list
120
+ // the same name twice; pick the first entry (same row the Console cluster
121
+ // list resolves to) rather than strict-mode failing on the hand-off.
122
+ const cluster = component
123
+ .getByRole('link', { name: name, exact: true })
124
+ .first();
120
125
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
121
126
  await cluster.click();
122
127
  }
@@ -555,7 +560,17 @@ class AppsPage {
555
560
  }
556
561
  catch (error) {
557
562
  console.warn(`Click attempt ${retries + 1} failed: ${error}`);
558
- await new Promise((resolve) => setTimeout(resolve, 10000));
563
+ // The other three app hand-offs (`clickTasklist`, `clickOperate`,
564
+ // `clickOptimize`) all reload here; this one only slept, and a slept
565
+ // retry re-opens the very same apps menu that just failed to list
566
+ // Console. When the shell is the thing that did not finish rendering
567
+ // -- the tab is still mid SSO hand-off, or `clickCamundaApps` found
568
+ // none of its four triggers and returned without opening anything --
569
+ // nothing changes between attempts and all three burn their 30s wait
570
+ // on an entry that was never going to appear. Re-render the shell
571
+ // instead, then let the loop re-open the menu.
572
+ await this.reloadForRecovery('opening Console');
573
+ await (0, sleep_1.sleep)(10000);
559
574
  }
560
575
  }
561
576
  throw new Error(`Failed to click the console link after ${maxRetries} attempts.`);
@@ -103,8 +103,16 @@ class Authorization {
103
103
  name: /delete authorization/i,
104
104
  });
105
105
  }
106
+ // The authorizations table paginates, and the RBA clusters these flows run
107
+ // against are long-lived: every night's run leaves more process-definition
108
+ // authorizations behind, so the row being asserted is regularly not on the
109
+ // page currently displayed. Scoping the assertion to the rendered rows then
110
+ // reports a row that exists as missing. Walk the pages -- the assertion is
111
+ // unchanged, it is only no longer limited to the first page, and the helper
112
+ // reloads between attempts so a row the product has not finished
113
+ // provisioning yet is still given time to appear.
106
114
  async assertAuthorization(auth) {
107
- await (0, test_1.expect)(this.authorizationsList.getByRole('row').filter({ hasText: auth }).first()).toBeVisible({ timeout: 30000 });
115
+ await (0, expectLocatorWithPagination_1.expectLocatorWithPagination)(this.page, this.authorizationsList.getByRole('row').filter({ hasText: auth }).first());
108
116
  }
109
117
  async createAuthorization(authorization) {
110
118
  await this.createAuthorizationButton.click();
@@ -66,6 +66,7 @@ declare class ClusterDetailsPage {
66
66
  assertAPIClientScopes(name: string): Promise<void>;
67
67
  filterClientsList(name: string): Promise<void>;
68
68
  clickSettingsTab(): Promise<void>;
69
+ private waitForAuthorizationsSection;
69
70
  enableAuthorizations(): Promise<void>;
70
71
  disableAuthorizations(): Promise<void>;
71
72
  searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
@@ -294,9 +294,30 @@ class ClusterDetailsPage {
294
294
  });
295
295
  }).toPass({ timeout: 120000, intervals: [1000, 2000, 5000] });
296
296
  }
297
+ // The Settings panel body is fetched separately from the tab list, so a
298
+ // selected Settings tab does not guarantee the Authorizations section is
299
+ // there: when that fetch fails -- or Console lands on its hard-crash banner
300
+ // ("An error has occurred") on the way in -- the section never renders, and
301
+ // neither state recovers on its own. A flat 60s wait can then only time out.
302
+ // Reload and re-open Settings between attempts, which is the same recovery
303
+ // `clickSettingsTab` already applies to the tab itself.
304
+ async waitForAuthorizationsSection() {
305
+ await (0, test_1.expect)(async () => {
306
+ const rendered = await this.authorizationsHeading
307
+ .waitFor({ state: 'visible', timeout: 60000 })
308
+ .then(() => true)
309
+ .catch(() => false);
310
+ if (rendered) {
311
+ return;
312
+ }
313
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
314
+ await this.clickSettingsTab();
315
+ await (0, test_1.expect)(this.authorizationsHeading).toBeVisible({ timeout: 60000 });
316
+ }).toPass({ timeout: 300000, intervals: [1000, 2000, 5000] });
317
+ }
297
318
  async enableAuthorizations() {
298
319
  await (0, sleep_1.sleep)(5000);
299
- await (0, test_1.expect)(this.authorizationsHeading).toBeVisible({ timeout: 60000 });
320
+ await this.waitForAuthorizationsSection();
300
321
  // Locate all elements with class .cds--toggle__text
301
322
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
302
323
  // Check if toggleTextElements has at least 2 elements
@@ -332,7 +353,7 @@ class ClusterDetailsPage {
332
353
  }
333
354
  }
334
355
  async disableAuthorizations() {
335
- await (0, test_1.expect)(this.authorizationsHeading).toBeVisible({ timeout: 60000 });
356
+ await this.waitForAuthorizationsSection();
336
357
  // Locate all elements with class .cds--toggle__text
337
358
  const toggleTextElements = await this.page.$$('.cds--toggle__text');
338
359
  // Check if toggleTextElements has at least 2 elements
@@ -107,6 +107,17 @@ class ClusterPage {
107
107
  .filter({ hasNotText: 'Generation' }); //Filter out header row
108
108
  this.tasklistV1Api = page.getByText('Tasklist API v1 (legacy)');
109
109
  this.tasklistV2Api = page.getByText('Tasklist API v2');
110
+ // The Console does not enforce unique cluster names, so an org can end up
111
+ // holding several clusters under the same name (a create that was retried
112
+ // after the list-fetch race in clickClustersBreadcrumb, or two runs of the
113
+ // same spec overlapping on the long-lived RBA clusters). Every name-scoped
114
+ // locator below is derived from this one, so without .first() a single
115
+ // leftover duplicate turns each of them into a strict-mode violation and
116
+ // the spec dies in beforeEach instead of reusing the healthy cluster.
117
+ // Resolving to the first matching row keeps this accessor singular, which
118
+ // is how every caller already uses it (isVisible/waitFor/click). Callers
119
+ // that must act on *all* same-named rows (deleteCluster) re-query in a
120
+ // loop, so they still walk the duplicates one by one.
110
121
  this.cluster = (clusterName) => this.clustersList.filter({
111
122
  has: page.getByRole('link', { name: clusterName, exact: true }),
112
123
  });
@@ -272,12 +283,9 @@ class ClusterPage {
272
283
  // matches every ancestor whose combined text spans the name and a status
273
284
  // (the whole app shell does, once a second row carries the same name),
274
285
  // which turns this wait into a strict-mode violation.
275
- const creatingRow = this.clusterRow(clusterName).filter({
276
- hasText: 'Creating',
277
- });
278
- const clusterRowVisible = this.clusterRow(clusterName).filter({
279
- hasText: /Creating|Healthy/,
280
- });
286
+ const clusterRow = this.clusterRow(clusterName);
287
+ const creatingRow = clusterRow.getByText('Creating', { exact: true });
288
+ const clusterRowVisible = creatingRow.or(clusterRow.getByText('Healthy', { exact: true }));
281
289
  try {
282
290
  await (0, test_1.expect)(this.clustersBreadcrumb).toBeVisible({ timeout: 60000 });
283
291
  await this.clustersBreadcrumb.click({ timeout: 60000 });
@@ -628,11 +628,14 @@ class ModelerCreatePage {
628
628
  await this.deployAndRunSubButton.click({ timeout: 30000 });
629
629
  }
630
630
  async runProcessInstance(clusterName, variables = '', formVariables = {}, textToVisible = '', hasDeploymentPermission = true, businessId = '') {
631
- await this.processApplicationPage.ensureStageCluster(clusterName);
632
631
  const maxRetries = 10;
633
632
  const retryDelayMs = 20000;
634
633
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
635
634
  try {
635
+ // Inside the loop, not before it: a no-op once bound, but selectCluster
636
+ // clears the binding when the dialog does not offer the cluster, so the
637
+ // next attempt rebinds rather than reopening an unchanged dialog.
638
+ await this.processApplicationPage.ensureStageCluster(clusterName);
636
639
  await this.clickStartInstanceMainButton();
637
640
  await this.selectCluster(clusterName);
638
641
  if (variables.length > 0) {
@@ -714,7 +717,22 @@ class ModelerCreatePage {
714
717
  .isVisible()
715
718
  .catch(() => false);
716
719
  if (!isLegacyDialog) {
717
- await (0, test_1.expect)(this.dialog.getByText(clusterName, { exact: false }).first()).toBeVisible({ timeout: 30000 });
720
+ const offered = await this.dialog
721
+ .getByText(clusterName, { exact: false })
722
+ .first()
723
+ .waitFor({ state: 'visible', timeout: 30000 })
724
+ .then(() => true)
725
+ .catch(() => false);
726
+ if (!offered) {
727
+ // Forget the binding before failing. The dialog lists exactly the
728
+ // clusters this application's stages point at, so a missing cluster
729
+ // means the binding did not take -- and no amount of reopening the
730
+ // dialog will make it appear. Clearing the record lets the caller's
731
+ // next attempt rebind instead of re-checking an unchanged dialog.
732
+ this.processApplicationPage.forgetStageCluster();
733
+ throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
734
+ 'binding did not take. It will be rebound on the next attempt.');
735
+ }
718
736
  return;
719
737
  }
720
738
  const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
@@ -763,7 +781,11 @@ class ModelerCreatePage {
763
781
  .isVisible()
764
782
  .catch(() => false);
765
783
  if (!menuOpen) {
766
- await this.clickDiagramBreadcrumb();
784
+ // Bounded well below the loop budget rather than reusing
785
+ // `clickDiagramBreadcrumb`'s 60s: at 60s a single reopen is the whole
786
+ // retry window, so the attempt that would have recovered never got to
787
+ // run and toPass reported the previous attempt's error instead.
788
+ await this.diagramBreadcrumb.click({ timeout: 20000 });
767
789
  }
768
790
  // Park the pointer so the breadcrumb's own tooltip gets a chance to
769
791
  // unmount -- best effort, because it is not reliably hover-driven, and
@@ -785,7 +807,13 @@ class ModelerCreatePage {
785
807
  await this.renameDiagramNameButton.dispatchEvent('click');
786
808
  }
787
809
  await (0, test_1.expect)(this.diagramNameInput).toBeVisible({ timeout: 10000 });
788
- }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
810
+ // The menu also closes on its own: `modelDiagramFromFile` opens it right
811
+ // after the diagram is picked, and when bpmn-js finishes importing it
812
+ // takes focus, which dismisses the menu that `isVisible` had just seen.
813
+ // The click then fails with "Rename" gone from the DOM rather than
814
+ // intercepted. Reopening is what recovers, so the budget has to fit
815
+ // several whole attempts, not one.
816
+ }).toPass({ timeout: 180000, intervals: [1000, 2000, 5000] });
789
817
  }
790
818
  // The rename can silently no-op: camunda-hub's breadcrumb name editor
791
819
  // (InlineEditable) resets its input back to the stored diagram name whenever
@@ -1567,9 +1595,8 @@ class ModelerCreatePage {
1567
1595
  // has nothing to deploy and renders no Deploy button at all.
1568
1596
  const alreadyDeployed = (await isDeployReported()) ||
1569
1597
  (await configureScenarioButton
1570
- .isVisible({ timeout: 3000 })
1571
- .catch(() => false)) ||
1572
- (await configureTestPanel.isVisible().catch(() => false));
1598
+ .isEnabled({ timeout: 3000 })
1599
+ .catch(() => false));
1573
1600
  if (!alreadyDeployed) {
1574
1601
  // Step 2: deploy — wait for enabled; button stays disabled until the cluster
1575
1602
  // connection is confirmed by the backend after the Save in step 1.
@@ -8,9 +8,11 @@ declare class TaskProcessesPage {
8
8
  readonly waitingForTasksText: Locator;
9
9
  readonly searchProcessesSearchbox: Locator;
10
10
  readonly processesTab: Locator;
11
+ readonly tasksTab: Locator;
11
12
  constructor(page: Page);
12
13
  searchForProcess(name: string): Promise<void>;
13
14
  clickpopupContinueButton(): Promise<void>;
15
+ private openTasksView;
14
16
  startProcess(name: string): Promise<void>;
15
17
  }
16
18
  export { TaskProcessesPage };
@@ -13,6 +13,7 @@ class TaskProcessesPage {
13
13
  waitingForTasksText;
14
14
  searchProcessesSearchbox;
15
15
  processesTab;
16
+ tasksTab;
16
17
  constructor(page) {
17
18
  this.page = page;
18
19
  this.popupContinueButton = page.getByRole('button', { name: 'Continue' });
@@ -24,6 +25,7 @@ class TaskProcessesPage {
24
25
  name: 'Search processes',
25
26
  });
26
27
  this.processesTab = page.getByRole('link', { name: 'Processes' });
28
+ this.tasksTab = page.getByRole('link', { name: 'Tasks' });
27
29
  }
28
30
  // Tasklist's process list is an infinite list that asks for only 12 process
29
31
  // definitions at a time (`PROCESS_DEFINITIONS_PAGE_SIZE` in the
@@ -71,6 +73,28 @@ class TaskProcessesPage {
71
73
  }
72
74
  await (0, test_1.expect)(this.popup.first()).not.toBeVisible({ timeout: 10000 });
73
75
  }
76
+ // Starting a process only lands on the Tasks view when Tasklist manages to
77
+ // resolve the new instance's user task in time. `startProcessMachine.ts` in
78
+ // camunda/camunda's orchestration-cluster webapp polls
79
+ // `queryUserTasks({processInstanceKey})` every second under a hard
80
+ // `TASKS_TIMEOUT: 15000`; when that elapses the machine goes straight to
81
+ // `Succeeded` and performs no navigation at all, leaving the browser on the
82
+ // Processes page -- which never renders the "Available tasks" panel. On
83
+ // these long-lived SaaS RBA clusters the exporter routinely lags past 15s,
84
+ // so waiting 90s for a redirect the product gave up on after 15 can only
85
+ // time out. Open the Tasks view ourselves when the redirect did not happen;
86
+ // the panel still has to appear, which is what this asserts, and the task
87
+ // count the caller then takes is what the test is actually about.
88
+ async openTasksView() {
89
+ const redirected = await this.availableTasks
90
+ .waitFor({ state: 'visible', timeout: 30000 })
91
+ .then(() => true)
92
+ .catch(() => false);
93
+ if (!redirected) {
94
+ await this.tasksTab.click({ timeout: 60000 });
95
+ }
96
+ await (0, test_1.expect)(this.availableTasks).toBeVisible({ timeout: 90000 });
97
+ }
74
98
  async startProcess(name) {
75
99
  if (process.env.TASKLIST_VERSION === 'v2') {
76
100
  await this.searchForProcess(name);
@@ -88,9 +112,7 @@ class TaskProcessesPage {
88
112
  await (0, test_1.expect)(this.waitingForTasksText).not.toBeVisible({
89
113
  timeout: 60000,
90
114
  });
91
- await (0, test_1.expect)(this.availableTasks.first()).toBeVisible({
92
- timeout: 90000, //redirection to tasks tab
93
- });
115
+ await this.openTasksView();
94
116
  }
95
117
  else if (process.env.TASKLIST_VERSION === 'v1') {
96
118
  const processStartButton = this.processes
@@ -102,9 +124,7 @@ class TaskProcessesPage {
102
124
  await (0, test_1.expect)(this.waitingForTasksText).not.toBeVisible({
103
125
  timeout: 60000,
104
126
  });
105
- await (0, test_1.expect)(this.availableTasks.first()).toBeVisible({
106
- timeout: 90000,
107
- });
127
+ await this.openTasksView();
108
128
  }
109
129
  else {
110
130
  throw new Error(`Unknown TASKLIST_VERSION: ${process.env.TASKLIST_VERSION}`);
@@ -32,7 +32,15 @@ _8_10_1.test.describe('Deploy and run a process in Play', () => {
32
32
  });
33
33
  });
34
34
  await _8_10_1.test.step('Open Cross Component Test Project', async () => {
35
- await modelerHomePage.clickCrossComponentProjectFolder();
35
+ // Self-healing variant: it clicks into the folder when the per-user
36
+ // setup project created it, and creates it first when that setup step
37
+ // did not take for this user. Clicking straight into the folder made a
38
+ // missing project an unrecoverable "Failed to click locator
39
+ // getByTitle('Cross Component Test Project')" five attempts later, even
40
+ // though the spec only needs *a* project to model in. Several sibling
41
+ // 8.10 specs (smoke-tests, rba-enabled-v2, access-user-role-management)
42
+ // already use this variant for the same reason.
43
+ await modelerHomePage.createCrossComponentProjectFolder();
36
44
  });
37
45
  await _8_10_1.test.step('Add A BPMN Template To The Project', async () => {
38
46
  await modelerHomePage.clickDiagramTypeDropdown();
@@ -314,7 +314,15 @@ _8_10_1.test.describe.parallel('RBA Enabled User Flows Test @tasklistV2', () =>
314
314
  await appsPage.openModelerWithRetry(modelerHomePage.modelerPageBanner);
315
315
  });
316
316
  await _8_10_1.test.step('Navigate to Cross Component Test Project', async () => {
317
- await modelerHomePage.clickCrossComponentProjectFolder();
317
+ // The project folder is per Modeler user, and this is the only test in
318
+ // the block that assumed it was already there: the other three call the
319
+ // create-if-missing helper. When the setup project did not manage to
320
+ // create it for this user, clickCrossComponentProjectFolder() burns its
321
+ // full 240s retry budget on a tile that will never render. Use the same
322
+ // helper as the sibling tests — it clicks straight into the folder when
323
+ // it exists (and guards against creating a duplicate), so nothing changes
324
+ // on the happy path.
325
+ await modelerHomePage.createCrossComponentProjectFolder();
318
326
  });
319
327
  await _8_10_1.test.step('Create Two BPMN Diagrams with User Task and Start Process Instance', async () => {
320
328
  await (0, UtilitiesPage_1.runMultipleProcesses)(clusterName, page, modelerHomePage, modelerCreatePage, {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1110",
3
+ "version": "0.0.1112",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",