@camunda/e2e-test-suite 0.0.838 → 0.0.840

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.
@@ -476,22 +476,94 @@ class ClusterDetailsPage {
476
476
  return variables;
477
477
  }
478
478
  async createAPIClient(name) {
479
- await this.clickCreateClientButton();
480
- await (0, test_1.expect)(this.createClientCredentialsDialog).toBeVisible({
481
- timeout: 30000,
482
- });
483
- await this.fillAPIClientName(name);
484
- await this.checkOrchestrationClusterCheckbox();
485
- await this.checkOptimizeCheckbox();
486
- await this.checkSecretsCheckbox();
487
- await this.clickCreateButton();
488
- await (0, test_1.expect)(this.clientCredentialsDialog).toBeVisible({
489
- timeout: 20000,
490
- });
491
- await (0, test_1.expect)(this.clientCredentialsDialog
492
- .getByText('The Client Secret will not be shown again.')
493
- .first()).toBeVisible();
494
- await (0, test_1.expect)(this.clientsList.filter({ hasText: name })).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/);
479
+ const maxRetries = 3;
480
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
481
+ // Use a suffixed name on retries so duplicate-name validation never
482
+ // leaves the Create button permanently disabled. A prior attempt may
483
+ // have created the client before we could detect the credentials dialog.
484
+ const attemptName = attempt === 0 ? name : `${name}-r${attempt}`;
485
+ try {
486
+ await this.clickCreateClientButton();
487
+ await (0, test_1.expect)(this.createClientCredentialsDialog).toBeVisible({
488
+ timeout: 30000,
489
+ });
490
+ await this.fillAPIClientName(attemptName);
491
+ await this.checkOrchestrationClusterCheckbox();
492
+ await this.checkOptimizeCheckbox();
493
+ await this.checkSecretsCheckbox();
494
+ await this.clickCreateButton();
495
+ // The POST behind Create occasionally returns "fetch error" — the
496
+ // create dialog stays open with an in-modal "Error fetch error"
497
+ // banner and the success ("Client credentials" / "The Client
498
+ // Secret will not be shown again.") dialog never appears. Detect
499
+ // that state and retry instead of burning 60s on a visibility
500
+ // assertion that will never resolve.
501
+ const credentialsShown = await this.clientCredentialsDialog
502
+ .isVisible({ timeout: 30000 })
503
+ .catch(() => false);
504
+ if (!credentialsShown) {
505
+ const stuckCreateDialog = await this.createClientCredentialsDialog
506
+ .isVisible({ timeout: 500 })
507
+ .catch(() => false);
508
+ const fetchError = stuckCreateDialog
509
+ ? await this.createClientCredentialsDialog
510
+ .getByText(/fetch error/i)
511
+ .isVisible({ timeout: 500 })
512
+ .catch(() => false)
513
+ : false;
514
+ if (stuckCreateDialog && fetchError) {
515
+ throw new Error('createAPIClient: create dialog stuck with "fetch error"');
516
+ }
517
+ // Not the fetch-error path — fall through to the original
518
+ // visibility expectation so the failure surface stays informative.
519
+ await (0, test_1.expect)(this.clientCredentialsDialog).toBeVisible({
520
+ timeout: 30000,
521
+ });
522
+ }
523
+ await (0, test_1.expect)(this.clientCredentialsDialog
524
+ .getByText('The Client Secret will not be shown again.')
525
+ .first()).toBeVisible();
526
+ try {
527
+ await (0, test_1.expect)(this.clientsList.filter({ hasText: attemptName })).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/, {
528
+ timeout: 10000,
529
+ });
530
+ }
531
+ catch (err) {
532
+ // The clients list panel can land in "Oops ... something went wrong."
533
+ // while the post-create modal is still open. The modal blocks pointer
534
+ // events so we can't click Reload here; the modal's "Client Secret will
535
+ // not be shown again" message above already proves the client exists.
536
+ const oopsVisible = await this.page
537
+ .getByRole('heading', { name: /Oops/i })
538
+ .isVisible({ timeout: 1000 })
539
+ .catch(() => false);
540
+ if (!oopsVisible)
541
+ throw err;
542
+ console.warn(`createAPIClient: clients list panel is in Oops state; skipping row scope assertion for "${attemptName}".`);
543
+ }
544
+ return;
545
+ }
546
+ catch (error) {
547
+ if (attempt < maxRetries - 1) {
548
+ console.warn(`createAPIClient attempt ${attempt + 1} for "${name}" failed: ${error}; cancelling any stuck dialog and reloading.`);
549
+ // Cancel the stuck create dialog if it's still open, then reload
550
+ // the API tab so the next attempt starts from a clean state.
551
+ const cancel = this.createClientCredentialsDialog.getByRole('button', { name: 'Cancel' });
552
+ if (await cancel.isVisible({ timeout: 500 }).catch(() => false)) {
553
+ await cancel.click({ timeout: 5000 }).catch(() => { });
554
+ }
555
+ await this.page.keyboard.press('Escape').catch(() => { });
556
+ await this.page
557
+ .reload({ waitUntil: 'domcontentloaded' })
558
+ .catch(() => { });
559
+ // The API tab needs to be re-selected after reload.
560
+ await this.clickAPITab().catch(() => { });
561
+ }
562
+ else {
563
+ throw new Error(`Creating API client "${name}" failed after ${maxRetries} attempts: ${error}`);
564
+ }
565
+ }
566
+ }
495
567
  }
496
568
  async clickEnvVarsButton() {
497
569
  await (0, test_1.expect)(this.envVarsTab).toBeVisible({ timeout: 40000 });
@@ -104,12 +104,17 @@ class ClusterPage {
104
104
  .filter({ hasNotText: 'Generation' }); //Filter out header row
105
105
  this.tasklistV1Api = page.getByText('Tasklist API v1 (legacy)');
106
106
  this.tasklistV2Api = page.getByText('Tasklist API v2');
107
- this.cluster = (clusterName) => this.clustersList.filter({ hasText: clusterName }).first();
107
+ this.cluster = (clusterName) => this.clustersList
108
+ .filter({
109
+ has: page.getByRole('link', { name: clusterName, exact: true }),
110
+ })
111
+ .first();
108
112
  this.clusterHealthiness = (clusterName) => this.cluster(clusterName).getByText(`Healthy`, {
109
113
  exact: true,
110
114
  });
111
115
  this.clusterLink = (clusterName) => this.cluster(clusterName).getByRole('link', {
112
116
  name: clusterName,
117
+ exact: true,
113
118
  });
114
119
  this.clusterVersion = (clusterName, versionText) => this.cluster(clusterName).getByText(versionText);
115
120
  }
@@ -130,7 +130,7 @@ class FormJsPage {
130
130
  await cluster.click();
131
131
  await (0, test_1.expect)(this.dialog
132
132
  .locator('label')
133
- .filter({ hasText: clusterName })
133
+ .filter({ hasText: new RegExp(`^${clusterName}`) })
134
134
  .getByRole('switch')).toBeChecked({ timeout: 30000 });
135
135
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
136
136
  timeout: healthCheckTimeout,
@@ -94,7 +94,11 @@ class HomePage {
94
94
  state: 'visible',
95
95
  timeout: 30000,
96
96
  });
97
- await this.buttonSkipCustomization.click({ timeout: 60000 });
97
+ // Best-effort: dismiss any Carbon modal that has a named close button.
98
+ await this.closeInformationDialog();
99
+ // force: true bypasses the "pointer events intercepted by modal backdrop"
100
+ // check — the same pattern used by closeInformationDialog itself.
101
+ await this.buttonSkipCustomization.click({ timeout: 60000, force: true });
98
102
  await this.closeInformationDialog();
99
103
  }
100
104
  catch {
@@ -604,13 +604,14 @@ class ModelerCreatePage {
604
604
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
605
605
  const healthCheckTimeout = 30000;
606
606
  const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
607
- const cluster = this.dialog.getByText(clusterName);
608
- await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
609
- await cluster.click();
610
- await (0, test_1.expect)(this.dialog
607
+ const clusterLabel = this.dialog
611
608
  .locator('label')
612
- .filter({ hasText: clusterName })
613
- .getByRole('switch')).toBeChecked({ timeout: 30000 });
609
+ .filter({ hasText: new RegExp(`^${clusterName}`) });
610
+ await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
611
+ await clusterLabel.click();
612
+ await (0, test_1.expect)(clusterLabel.getByRole('switch')).toBeChecked({
613
+ timeout: 30000,
614
+ });
614
615
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
615
616
  timeout: healthCheckTimeout,
616
617
  });
@@ -1102,7 +1103,7 @@ class ModelerCreatePage {
1102
1103
  // before saving. This handles environments with multiple clusters.
1103
1104
  const clusterLabel = configureEnvDialog
1104
1105
  .locator('label')
1105
- .filter({ hasText: clusterName });
1106
+ .filter({ hasText: new RegExp(`^${clusterName}`) });
1106
1107
  const clusterSwitch = clusterLabel.getByRole('switch');
1107
1108
  await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout });
1108
1109
  const alreadyChecked = (await clusterSwitch.getAttribute('aria-checked')) === 'true';
@@ -300,7 +300,7 @@ class ModelerHomePage {
300
300
  async selectCluster(clusterName) {
301
301
  //Select the correct cluster if multiple exist
302
302
  const baseClusterName = clusterName.split('(')[0].trim();
303
- const cluster = this.dialog.getByText(baseClusterName, { exact: false });
303
+ const cluster = this.dialog.getByText(new RegExp(`^${baseClusterName}`));
304
304
  await (0, test_1.expect)(cluster).toBeVisible({
305
305
  timeout: 30000,
306
306
  });
@@ -308,7 +308,7 @@ class ModelerHomePage {
308
308
  await (0, test_1.expect)(this.dialog
309
309
  .locator('label')
310
310
  .filter({
311
- hasText: clusterName,
311
+ hasText: new RegExp(`^${clusterName}`),
312
312
  })
313
313
  .getByRole('switch')).toBeChecked({ timeout: 30000 });
314
314
  }
@@ -32,7 +32,12 @@ class TaskPanelPage {
32
32
  }
33
33
  async openTask(name) {
34
34
  let attempts = 0;
35
- const maxAttempts = 3;
35
+ // Tasklist's importer can lag well behind Zeebe under heavy concurrent
36
+ // load from the sibling chromium-v1/chromium-v2 projects sharing one
37
+ // cluster -- trace evidence from failing runs showed other tests' tasks
38
+ // only appearing 15-17 minutes after creation. 10 x 120s gives 20 minutes
39
+ // of budget, comfortably covering that observed lag.
40
+ const maxAttempts = 10;
36
41
  while (attempts < maxAttempts) {
37
42
  try {
38
43
  const task = this.availableTasks.getByText(name, { exact: true }).first();
@@ -46,6 +51,12 @@ class TaskPanelPage {
46
51
  throw error;
47
52
  }
48
53
  await this.page.reload();
54
+ // Tasklist polls continuously for new tasks, so `networkidle` never
55
+ // fires and the wait times out at 60s -- burning the retry budget
56
+ // without ever re-attempting the task click. `domcontentloaded`
57
+ // resolves once the reloaded HTML has parsed, which is the actual
58
+ // signal we need before trying to find the task again.
59
+ await this.page.waitForLoadState('domcontentloaded', { timeout: 60000 });
49
60
  }
50
61
  }
51
62
  }
@@ -71,6 +71,17 @@ _8_9_1.test.describe('Console User Flow Tests @tasklistV2', () => {
71
71
  await (0, UtilitiesPage_1.assertLocatorVisibleWithRetry)(operateTabProcessInstancePage, operateTabProcessInstancePage.completedIcon, 'completed icon in Operate', 60000);
72
72
  });
73
73
  });
74
+ (0, _8_9_1.test)('Create Cluster', async ({ homePage, clusterPage }) => {
75
+ _8_9_1.test.slow();
76
+ const newClusterName = 'UI Test Cluster';
77
+ await _8_9_1.test.step('Navigate to clusters and create cluster', async () => {
78
+ await homePage.clickClusters();
79
+ await clusterPage.createCluster(newClusterName);
80
+ });
81
+ await _8_9_1.test.step('Assert cluster is healthy', async () => {
82
+ await clusterPage.assertClusterHealthyStatusWithRetry(newClusterName, 300000, 1500000);
83
+ });
84
+ });
74
85
  (0, _8_9_1.test)('Alert Trigger Flow - Email Notification', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, clusterPage, clusterDetailsPage, connectorSettingsPage, operateHomePage, operateProcessesPage, operateProcessInstancePage, }) => {
75
86
  _8_9_1.test.slow();
76
87
  const processName = 'Email_Alert_Process' + (await (0, _setup_1.generateRandomStringAsync)(3));
@@ -7,6 +7,7 @@ const connectorSecrets_1 = require("../../utils/connectorSecrets");
7
7
  const mcpSecrets_1 = require("../../utils/mcpSecrets");
8
8
  const UtilitiesPage_1 = require("../../pages/8.9/UtilitiesPage");
9
9
  const users_1 = require("../../utils/users");
10
+ const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
10
11
  _8_9_1.test.describe.configure({ mode: 'parallel' });
11
12
  _8_9_1.test.describe('Cluster Setup Tests', () => {
12
13
  _8_9_1.test.afterEach(async ({ page }, testInfo) => {
@@ -26,9 +27,15 @@ _8_9_1.test.describe('Cluster Setup Tests', () => {
26
27
  const clusterName = 'Test Cluster';
27
28
  const apiClientName = 'Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
28
29
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
29
- await homePage.clickClusters();
30
- await clusterPage.createCluster(clusterName);
31
- await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
30
+ if (process.env.IS_PROD === 'true') {
31
+ await clusterPage.createCluster(clusterName);
32
+ await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
33
+ }
34
+ else {
35
+ const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName);
36
+ await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid);
37
+ await homePage.clickClusters();
38
+ }
32
39
  await clusterPage.clickClusterLink(clusterName);
33
40
  await clusterDetailsPage.assertComponentsHealth();
34
41
  if (process.env.IS_AG !== 'true') {
@@ -51,9 +58,9 @@ _8_9_1.test.describe('Cluster Setup Tests', () => {
51
58
  const apiClientName = 'Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
52
59
  const mcpClientName = 'MCP_Test_API_Client' + (await (0, _setup_1.generateRandomStringAsync)(3));
53
60
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
61
+ const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName);
62
+ await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid, 1500000);
54
63
  await homePage.clickClusters();
55
- await clusterPage.createCluster(clusterName);
56
- await clusterPage.assertClusterHealthyStatusWithRetry(clusterName);
57
64
  await clusterPage.clickClusterLink(clusterName);
58
65
  await clusterDetailsPage.assertComponentsHealth();
59
66
  await clusterDetailsPage.clickSettingsTab();
@@ -75,10 +82,16 @@ _8_9_1.test.describe('Cluster Setup Tests', () => {
75
82
  _8_9_1.test.slow();
76
83
  const clusterName = 'AWS Cluster';
77
84
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
78
- await homePage.clickClusters();
79
- await clusterPage.createCluster(clusterName, 'AWS');
80
- // AWS clusters on SaaS INT routinely need 10+ min to reach Healthy.
81
- await clusterPage.assertClusterHealthyStatusWithRetry(clusterName, 300000, 1500000);
85
+ if (process.env.IS_PROD === 'true') {
86
+ await clusterPage.createCluster(clusterName, 'AWS');
87
+ // AWS clusters on SaaS INT routinely need 10+ min to reach Healthy.
88
+ await clusterPage.assertClusterHealthyStatusWithRetry(clusterName, 300000, 1500000);
89
+ }
90
+ else {
91
+ const clusterUuid = await (0, consoleApiHelpers_1.ensureClusterViaApi)(page, clusterName, 'AWS');
92
+ await (0, consoleApiHelpers_1.waitForClusterHealthyViaApi)(page, clusterUuid, 1500000);
93
+ await homePage.clickClusters();
94
+ }
82
95
  await clusterPage.clickClusterLink(clusterName);
83
96
  await clusterDetailsPage.assertComponentsHealth();
84
97
  if (process.env.IS_AG !== 'true') {
@@ -175,17 +175,6 @@ SM_8_9_1.test.describe('Web Modeler User Flow Tests', () => {
175
175
  await operateProcessInstancePage.assertProcessVariableContainsText('myVar', '8');
176
176
  });
177
177
  if (process.env.IS_OPTIMIZE !== 'false') {
178
- await SM_8_9_1.test.step('Navigate to Optimize and verify process in dashboard', async () => {
179
- await navigationPage.goToOptimize();
180
- await optimizeHomePage.clickDashboardLink();
181
- await (0, sleep_1.sleep)(3000); // Brief wait for Optimize dashboard to initialize
182
- await page.reload();
183
- await optimizeDashboardPage.clickFilterTable();
184
- await optimizeDashboardPage.fillFilterTable(processName);
185
- await optimizeDashboardPage.processAssertion(processName, {
186
- timeout: 60000,
187
- });
188
- });
189
178
  await SM_8_9_1.test.step('Navigate to Optimize and verify process appears in dashboard (CE-OPT-01)', async () => {
190
179
  await navigationPage.goToOptimize();
191
180
  await optimizeHomePage.clickDashboardLink();
@@ -123,6 +123,13 @@ async function createClusterViaApi(page, clusterName, region = 'GCP') {
123
123
  k8sContextId,
124
124
  autoUpdate: true,
125
125
  stageLabel: 'dev',
126
+ // Upgrading a cluster to Tasklist v2 is a one-way, irreversible switch,
127
+ // and v2 cannot run job-worker-based user tasks, draft variables, or
128
+ // public start forms -- all of which the @tasklistV1-tagged suites
129
+ // exercise. Explicitly request v1 rather than relying on whatever the
130
+ // Console API defaults new clusters to, since a cluster that ends up
131
+ // v2-enabled can never be brought back to v1 for the rest of its life.
132
+ tasklistV2Enabled: false,
126
133
  },
127
134
  });
128
135
  if (createResponse.status() !== 200 && createResponse.status() !== 201) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.838",
3
+ "version": "0.0.840",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",