@camunda/e2e-test-suite 0.0.1620 → 0.0.1622

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.
@@ -55,7 +55,32 @@ declare class ClusterPage {
55
55
  deleteCluster(name: string): Promise<void>;
56
56
  clickClusterTab(): Promise<void>;
57
57
  clickCreateNewClusterButton(): Promise<void>;
58
- clickClusterNameInput(): Promise<void>;
58
+ /**
59
+ * `recoverViaClusterList` decides what happens after a failed click:
60
+ * clickLocatorWithRetry runs it as the recovery between attempts, and it
61
+ * navigates to the cluster list.
62
+ *
63
+ * On the duplication form that is fatal. The form is reached from a
64
+ * cluster's own overflow menu, so navigating away abandons it: the name
65
+ * input no longer exists, and attempts 2 and 3 cannot find it however long
66
+ * they wait. One recoverable slow render therefore became "Failed to click
67
+ * locator getByPlaceholder('Enter a name for the cluster') after 3
68
+ * attempts" — which is why DUP-01 failed on a run where the same form
69
+ * worked for DUP-04 minutes earlier.
70
+ *
71
+ * The default is true to preserve existing behaviour, NOT because the
72
+ * recovery works on the create-cluster form. It does not: createCluster()
73
+ * opens that form once before awaiting this helper, and the retry loop
74
+ * never reopens it, so returning to the list strands those attempts the
75
+ * same way. It is simply rarely reached there — the create form renders
76
+ * synchronously after its button click, where the duplication form waits
77
+ * on a menu and a navigation. Turning it off for every caller is the real
78
+ * fix and belongs in its own change, with a run behind it: createCluster()
79
+ * is on the critical path of every spec in this suite.
80
+ */
81
+ clickClusterNameInput(options?: {
82
+ recoverViaClusterList?: boolean;
83
+ }): Promise<void>;
59
84
  fillClusterNameInput(name: string): Promise<void>;
60
85
  private extractClusterIdFromUrl;
61
86
  private logClusterCreationInfo;
@@ -363,11 +363,39 @@ class ClusterPage {
363
363
  },
364
364
  });
365
365
  }
366
- async clickClusterNameInput() {
366
+ /**
367
+ * `recoverViaClusterList` decides what happens after a failed click:
368
+ * clickLocatorWithRetry runs it as the recovery between attempts, and it
369
+ * navigates to the cluster list.
370
+ *
371
+ * On the duplication form that is fatal. The form is reached from a
372
+ * cluster's own overflow menu, so navigating away abandons it: the name
373
+ * input no longer exists, and attempts 2 and 3 cannot find it however long
374
+ * they wait. One recoverable slow render therefore became "Failed to click
375
+ * locator getByPlaceholder('Enter a name for the cluster') after 3
376
+ * attempts" — which is why DUP-01 failed on a run where the same form
377
+ * worked for DUP-04 minutes earlier.
378
+ *
379
+ * The default is true to preserve existing behaviour, NOT because the
380
+ * recovery works on the create-cluster form. It does not: createCluster()
381
+ * opens that form once before awaiting this helper, and the retry loop
382
+ * never reopens it, so returning to the list strands those attempts the
383
+ * same way. It is simply rarely reached there — the create form renders
384
+ * synchronously after its button click, where the duplication form waits
385
+ * on a menu and a navigation. Turning it off for every caller is the real
386
+ * fix and belongs in its own change, with a run behind it: createCluster()
387
+ * is on the critical path of every spec in this suite.
388
+ */
389
+ async clickClusterNameInput(options = {}) {
390
+ const { recoverViaClusterList = true } = options;
367
391
  await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.clusterNameInput, {
368
- postAction: async () => {
369
- await this.clickClusterBanner();
370
- },
392
+ ...(recoverViaClusterList
393
+ ? {
394
+ postAction: async () => {
395
+ await this.clickClusterBanner();
396
+ },
397
+ }
398
+ : {}),
371
399
  });
372
400
  }
373
401
  async fillClusterNameInput(name) {
@@ -124,7 +124,11 @@ _8_10_1.test.describe('Console cluster duplication', () => {
124
124
  await duplicateAction.click();
125
125
  // The pre-fill itself is covered a layer down (DUP-03); here we only rename
126
126
  // the duplicate so it is uniquely identifiable and deletable.
127
- await clusterPage.clickClusterNameInput();
127
+ // No cluster-list recovery here: this form is reached from the
128
+ // cluster's overflow menu, so navigating away abandons it.
129
+ await clusterPage.clickClusterNameInput({
130
+ recoverViaClusterList: false,
131
+ });
128
132
  await clusterPage.fillClusterNameInput(duplicateClusterName);
129
133
  createdClusterNames.push(duplicateClusterName);
130
134
  // Duplicate opens a "Duplicate a cluster" page (not a dialog) that submits
@@ -205,7 +209,11 @@ _8_10_1.test.describe('Console cluster duplication', () => {
205
209
  await duplicateAction.click();
206
210
  // Every editable field of the duplication form, changed away from the
207
211
  // value the form carried over from the source cluster.
208
- await clusterPage.clickClusterNameInput();
212
+ // No cluster-list recovery here: this form is reached from the
213
+ // cluster's overflow menu, so navigating away abandons it.
214
+ await clusterPage.clickClusterNameInput({
215
+ recoverViaClusterList: false,
216
+ });
209
217
  await clusterPage.fillClusterNameInput(duplicateClusterName);
210
218
  // clickGCPRegion only acts when REGION_SELECTION is set, so the tile
211
219
  // click below is what actually moves the region either way.
@@ -256,6 +264,125 @@ _8_10_1.test.describe('Console cluster duplication', () => {
256
264
  await clusterDetailsPage.assertComponentsHealth(undefined, (0, UtilitiesPage_1.consoleSessionRecovery)(page, page.url()));
257
265
  });
258
266
  });
267
+ // DUP-04 - the case the epic exists for: configuration drift. Everything is
268
+ // asserted on the cluster that was really created, never on the duplication
269
+ // form's pre-fill (DUP-03 owns that layer).
270
+ //
271
+ // Scope. The epic lists plan type, region, channel and generation, labels,
272
+ // IP allowlist rules, encryption settings, connector secrets, alert
273
+ // subscriptions, BYOM credentials, the BYOIDP secret and the backup
274
+ // schedule. This case asserts the two the suite's page objects can both
275
+ // populate on the source and read back off the duplicate: region and
276
+ // connector secrets. The rest have no page object that can set them on the
277
+ // source, and a source cluster where a setting was never populated makes
278
+ // the matching assertion on the duplicate pass by copying nothing - exactly
279
+ // the false green the preconditions warn about. Covering them needs
280
+ // page-object work first, not an invented selector here.
281
+ //
282
+ // Alert subscriptions belong to that second group despite createAlert()
283
+ // existing. It takes no recipient and no name: it selects the Email
284
+ // delivery method and submits. So an alert on the duplicate cannot be told
285
+ // apart from any other alert, and asserting one would pass whether or not
286
+ // duplication copied anything. assertAlertText() reads the detail row, not
287
+ // the recipient - every other caller in this suite passes an alert name
288
+ // such as 'Amount triggered1'. Asserting carry-over needs createAlert() to
289
+ // accept something that distinguishes the subscription.
290
+ (0, _8_10_1.test)('DUP-04: the carried-over settings are present on the created duplicate', async ({ page, homePage, clusterPage, clusterDetailsPage, clusterSecretsPage, }) => {
291
+ // Two cluster creations plus health polling - this case is long by nature.
292
+ _8_10_1.test.setTimeout(60 * 60 * 1000);
293
+ const randomSuffix = await (0, _setup_1.generateRandomStringAsync)(6);
294
+ const sourceClusterName = `dup-src-cfg-${randomSuffix}`;
295
+ const duplicateClusterName = `dup-cfg-${randomSuffix}`;
296
+ const secretKey = `DUP_SECRET_${randomSuffix}`;
297
+ const secretValue = `dup-secret-value-${randomSuffix}`;
298
+ await _8_10_1.test.step('given a healthy source cluster in a known region, with a connector secret', async () => {
299
+ await clusterPage.clickClusterTab();
300
+ createdClusterNames.push(sourceClusterName);
301
+ // The region is pinned rather than left to selectSubRegion()'s random
302
+ // pick: a known source region is the only way the duplicate's region can
303
+ // be asserted as "the source's one" instead of "some region".
304
+ await clusterPage.createCluster(sourceClusterName, 'GCP', undefined, SOURCE_SUB_REGION);
305
+ await clusterPage.assertClusterHealthyStatusWithRetry(sourceClusterName, undefined, undefined, (0, UtilitiesPage_1.consoleSessionRecovery)(page, page.url()));
306
+ await clusterSecretsPage.createNewSecret(secretKey, secretValue, sourceClusterName);
307
+ // The source really holds each setting before anything is duplicated.
308
+ // Without this, a missing setting on the duplicate below would be
309
+ // indistinguishable from a setting that was never created at all.
310
+ await homePage.clickClusters();
311
+ await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
312
+ await (0, test_1.expect)(clusterPage.clusterRegion(sourceClusterName, SOURCE_REGION_LABEL)).toBeVisible({ timeout: 60000 });
313
+ });
314
+ await _8_10_1.test.step('when I duplicate that cluster, changing only its name', async () => {
315
+ await homePage.clickClusters();
316
+ await clusterPage.clickClusterLink(sourceClusterName);
317
+ await clusterDetailsPage.dismissOopsIfPresent();
318
+ await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
319
+ await page.getByRole('button', { name: 'cluster operations' }).click();
320
+ const duplicateAction = page
321
+ .getByRole('menuitem', { name: /duplicate/i })
322
+ .or(page.getByRole('button', { name: /duplicate/i }));
323
+ await (0, test_1.expect)(duplicateAction).toBeVisible({ timeout: 30000 });
324
+ await duplicateAction.click();
325
+ // Only the name changes: every other field keeps whatever the form
326
+ // carried over, which is what this case is here to verify landed.
327
+ await clusterPage.clickClusterNameInput();
328
+ await clusterPage.fillClusterNameInput(duplicateClusterName);
329
+ createdClusterNames.push(duplicateClusterName);
330
+ const submitDuplicate = page.getByRole('button', {
331
+ name: 'Duplicate cluster',
332
+ exact: true,
333
+ });
334
+ await (0, test_1.expect)(submitDuplicate).toBeEnabled({ timeout: 30000 });
335
+ // Same as DUP-01/DUP-02: hold the page until the duplicate POST has
336
+ // answered, otherwise navigating away aborts the in-flight request.
337
+ const duplicateResponse = page.waitForResponse((response) => response.request().method() === 'POST' &&
338
+ /\/clusters\/[^/]+\/duplicate$/.test(new URL(response.url()).pathname), { timeout: 60000 });
339
+ await submitDuplicate.click();
340
+ const duplicate = await duplicateResponse;
341
+ (0, test_1.expect)(duplicate.ok()).toBe(true);
342
+ const { clusterId } = await duplicate.json();
343
+ await page.waitForURL(new RegExp(`/cluster/${clusterId}`), {
344
+ timeout: 60000,
345
+ });
346
+ });
347
+ await _8_10_1.test.step('then the duplicate comes up healthy', async () => {
348
+ await homePage.clickClusters();
349
+ await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
350
+ await (0, test_1.expect)(clusterPage.clusterLink(duplicateClusterName)).toBeVisible({
351
+ timeout: 60000,
352
+ });
353
+ // Completion rule: the settings below are only meaningful on a cluster
354
+ // that actually runs, so health is asserted before them.
355
+ await clusterPage.assertClusterHealthyStatusWithRetry(duplicateClusterName, undefined, undefined, (0, UtilitiesPage_1.consoleSessionRecovery)(page, page.url()));
356
+ await clusterPage.clickClusterLink(duplicateClusterName);
357
+ await clusterDetailsPage.dismissOopsIfPresent();
358
+ await clusterDetailsPage.assertComponentsHealth(undefined, (0, UtilitiesPage_1.consoleSessionRecovery)(page, page.url()));
359
+ });
360
+ await _8_10_1.test.step('and the duplicate carries the region of the source cluster', async () => {
361
+ await homePage.clickClusters();
362
+ await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
363
+ // Scoped to the duplicate's own row: the source is still listed on this
364
+ // page with the same label, so a page-wide text read would pass on the
365
+ // source alone.
366
+ await (0, test_1.expect)(clusterPage.clusterRegion(duplicateClusterName, SOURCE_REGION_LABEL)).toBeVisible({ timeout: 60000 });
367
+ // And it did not silently land in the other region the org can create
368
+ // in - drift in either direction is the failure this case guards.
369
+ await (0, test_1.expect)(clusterPage.clusterRegion(duplicateClusterName, OVERRIDE_REGION_LABEL)).toHaveCount(0);
370
+ });
371
+ await _8_10_1.test.step('and the connector secret of the source is present on the duplicate', async () => {
372
+ await homePage.clickClusters();
373
+ await clusterPage.clickClusterLink(duplicateClusterName);
374
+ await clusterDetailsPage.dismissOopsIfPresent();
375
+ await clusterPage.clickConnectorSecretsTab(duplicateClusterName);
376
+ await (0, UtilitiesPage_1.waitForLoadingToFinish)(page);
377
+ // Scoped to the duplicate's own secrets view. Only the key is asserted:
378
+ // Hub never renders a secret's value back, by design.
379
+ const duplicateSecrets = page.getByRole('main');
380
+ const carriedOverSecret = duplicateSecrets
381
+ .getByRole('cell', { name: secretKey, exact: true })
382
+ .or(duplicateSecrets.getByText(secretKey, { exact: true }));
383
+ await (0, test_1.expect)(carriedOverSecret.first()).toBeVisible({ timeout: 60000 });
384
+ });
385
+ });
259
386
  // DUP-05. The epic's claim for this slice is a claim about the form: the
260
387
  // admin learns *before submitting* that the process data of the source does
261
388
  // not come across and the duplicate starts empty. So the assertion is the
@@ -314,7 +441,11 @@ _8_10_1.test.describe('Console cluster duplication', () => {
314
441
  await (0, test_1.expect)(submitDuplicate).toBeVisible({ timeout: 30000 });
315
442
  });
316
443
  await _8_10_1.test.step('and submitting that form creates a duplicate that reaches a healthy running state', async () => {
317
- await clusterPage.clickClusterNameInput();
444
+ // No cluster-list recovery here: this form is reached from the
445
+ // cluster's overflow menu, so navigating away abandons it.
446
+ await clusterPage.clickClusterNameInput({
447
+ recoverViaClusterList: false,
448
+ });
318
449
  await clusterPage.fillClusterNameInput(duplicateClusterName);
319
450
  createdClusterNames.push(duplicateClusterName);
320
451
  const submitDuplicate = page.getByRole('button', {
@@ -21,6 +21,11 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
21
21
  (0, _8_10_1.test)('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
22
22
  _8_10_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
23
23
  _8_10_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
24
+ // A scoped run judges one case against a throwaway organisation that is
25
+ // not entitled to alpha features, so the AI-powered-features row never
26
+ // renders and this test cannot pass there. The nightlies, which run
27
+ // against an entitled org, are unaffected: IS_SCOPED_RUN is unset there.
28
+ _8_10_1.test.skip(process.env.IS_SCOPED_RUN === 'true', 'Skipping test because a scoped run org has no alpha-feature entitlement');
24
29
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
25
30
  await homePage.clickOrganization();
26
31
  await (0, UtilitiesPage_1.assertLocatorVisibleWithRetry)(page, consoleOrganizationsPage.settingsTab, 'settings tab', 60000, false, 2);
@@ -72,16 +77,27 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
72
77
  // a bounded, non-blocking loss -- not worth risking the whole run for.
73
78
  if (process.env.IS_AG !== 'true') {
74
79
  await clusterPage.clickConnectorSecretsTab(clusterName);
75
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.awsSecretsValues);
76
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.openAiSecretsValues);
77
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.gcpSecretsValues);
78
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.azureSecretsValues);
80
+ // Every set below but connectorSecretsValues reads
81
+ // credentials from the environment. A scoped run imports
82
+ // none of them, so each value is undefined and the create
83
+ // fallback throws. connectorSecretsValues is three
84
+ // literals, so it is seeded there too -- the connector
85
+ // secret user flows need it and it cannot be undefined.
86
+ const envBackedSecrets = process.env.IS_SCOPED_RUN !== 'true';
87
+ if (envBackedSecrets) {
88
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.awsSecretsValues);
89
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.openAiSecretsValues);
90
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.gcpSecretsValues);
91
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.azureSecretsValues);
92
+ }
79
93
  await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.connectorSecretsValues);
80
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockKbSecretsValues);
81
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreSecretsValues);
82
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreLtmSecretsValues);
83
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockCodeInterpreterSecretsValues);
84
- await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.anthropicBedrockMantleSecretsValues);
94
+ if (envBackedSecrets) {
95
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockKbSecretsValues);
96
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreSecretsValues);
97
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.agentcoreLtmSecretsValues);
98
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.bedrockCodeInterpreterSecretsValues);
99
+ await clusterSecretsPage.createSetOfSecrets(clusterName, connectorSecrets_1.anthropicBedrockMantleSecretsValues);
100
+ }
85
101
  }
86
102
  }
87
103
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1620",
3
+ "version": "0.0.1622",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",