@camunda/e2e-test-suite 0.0.1621 → 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
369
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
});
|