@camunda/e2e-test-suite 0.0.837 → 0.0.839

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';
@@ -97,9 +97,24 @@ class ModelerHomePage {
97
97
  });
98
98
  }
99
99
  async enterNewProjectName(name) {
100
- await this.projectNameInput.click({ timeout: 60000 });
101
- await this.projectNameInput.fill(name);
102
- await this.projectNameInput.press('Enter');
100
+ // The inline rename silently drops the value when the editable title field
101
+ // is not yet in edit mode as fill() runs, leaving the project named
102
+ // "New project". Re-enter until the field reflects the requested name.
103
+ //
104
+ // Once the rename commits, the editable input unmounts and the title
105
+ // renders as a static heading (`[title="<name>"]`) instead. A retry that
106
+ // still assumes `[data-test="editable-input"]` is clickable then hangs on
107
+ // an element that will never reappear, even though the rename already
108
+ // succeeded. Treat the static title becoming visible as success.
109
+ await (0, test_1.expect)(async () => {
110
+ if (await this.page.getByTitle(name, { exact: true }).isVisible()) {
111
+ return;
112
+ }
113
+ await this.projectNameInput.click({ timeout: 60000 });
114
+ await this.projectNameInput.fill(name);
115
+ await this.projectNameInput.press('Enter');
116
+ await (0, test_1.expect)(this.projectNameInput).toHaveValue(name, { timeout: 5000 });
117
+ }).toPass({ timeout: 120000 });
103
118
  }
104
119
  async enterIdpApplicationName(name) {
105
120
  await this.idpApplicationNameInput.click({ timeout: 60000 });
@@ -201,15 +216,51 @@ class ModelerHomePage {
201
216
  });
202
217
  }
203
218
  async createCrossComponentProjectFolder() {
204
- await (0, sleep_1.sleep)(15000);
205
- await this.dismissOverlays();
206
- if (await this.crossComponentProjectFolder.isVisible()) {
207
- console.log('Cross Component Project folder already exists. Clicking into it');
208
- await this.clickCrossComponentProjectFolder();
209
- return;
219
+ // Retry the whole create+rename sequence, not just the inline rename.
220
+ // enterNewProjectName only retries typing the name; if the "New project"
221
+ // click never puts the new project into inline-edit mode (the editable
222
+ // input never renders, common when the modeler backend is slow), that
223
+ // retry can never recover. Re-clicking "New project" on the next attempt
224
+ // gives the edit field a fresh chance to appear. Mirrors the SM
225
+ // createCrossComponentProjectFolder retry.
226
+ const maxAttempts = 3;
227
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
228
+ await (0, sleep_1.sleep)(attempt === 0 ? 15000 : 5000);
229
+ await this.dismissOverlays();
230
+ // The project may already exist (a sibling user's setup, or a previous
231
+ // attempt that created it but tripped on the rename check). Detect it in
232
+ // BOTH view states before trying to create, otherwise we click "New
233
+ // project" — which does not exist inside an open project — and fail:
234
+ // - on Home it shows as a folder tile (crossComponentProjectFolder);
235
+ // - if a previous step already opened it, the page is INSIDE the project
236
+ // (project breadcrumb visible), where the tile is not rendered, so it
237
+ // already exists and we are done.
238
+ if (await this.crossComponentProjectFolder.isVisible()) {
239
+ console.log('Cross Component Project folder already exists. Clicking into it');
240
+ await this.clickCrossComponentProjectFolder();
241
+ return;
242
+ }
243
+ if (await this.projectBreadcrumb.isVisible().catch(() => false)) {
244
+ console.log('Already inside the Cross Component Project folder; it exists');
245
+ return;
246
+ }
247
+ try {
248
+ await this.clickCreateNewProjectButton();
249
+ await this.enterNewProjectName(this.defaultFolderName);
250
+ return;
251
+ }
252
+ catch (error) {
253
+ if (attempt >= maxAttempts - 1) {
254
+ throw error;
255
+ }
256
+ // A prior attempt may have created and opened the project but failed
257
+ // the rename check. Return Home so the next attempt's existence check
258
+ // sees the folder tile instead of trying to create it again from
259
+ // inside the project (where there is no "New project" button).
260
+ await this.clickHomeBreadcrumb().catch(() => { });
261
+ console.log(`createCrossComponentProjectFolder attempt ${attempt + 1} failed; retrying...`);
262
+ }
210
263
  }
211
- await this.clickCreateNewProjectButton();
212
- await this.enterNewProjectName(this.defaultFolderName);
213
264
  }
214
265
  async clickManageButton(retries = 3) {
215
266
  await this.optionsButton.click({ timeout: 30000 });
@@ -249,7 +300,7 @@ class ModelerHomePage {
249
300
  async selectCluster(clusterName) {
250
301
  //Select the correct cluster if multiple exist
251
302
  const baseClusterName = clusterName.split('(')[0].trim();
252
- const cluster = this.dialog.getByText(baseClusterName, { exact: false });
303
+ const cluster = this.dialog.getByText(new RegExp(`^${baseClusterName}`));
253
304
  await (0, test_1.expect)(cluster).toBeVisible({
254
305
  timeout: 30000,
255
306
  });
@@ -257,7 +308,7 @@ class ModelerHomePage {
257
308
  await (0, test_1.expect)(this.dialog
258
309
  .locator('label')
259
310
  .filter({
260
- hasText: clusterName,
311
+ hasText: new RegExp(`^${clusterName}`),
261
312
  })
262
313
  .getByRole('switch')).toBeChecked({ timeout: 30000 });
263
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') {
@@ -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.837",
3
+ "version": "0.0.839",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",