@camunda/e2e-test-suite 0.0.1108 → 0.0.1110
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.
|
@@ -32,12 +32,15 @@ declare class ClusterPage {
|
|
|
32
32
|
readonly tasklistV1Api: Locator;
|
|
33
33
|
readonly tasklistV2Api: Locator;
|
|
34
34
|
readonly cluster: (clusterName: string) => Locator;
|
|
35
|
+
readonly clusterRow: (clusterName: string) => Locator;
|
|
35
36
|
readonly clusterHealthiness: (clusterName: string) => Locator;
|
|
36
37
|
readonly clusterLink: (clusterName: string) => Locator;
|
|
37
38
|
readonly dialog: Locator;
|
|
38
39
|
readonly clusterVersion: (clusterName: string, versionText: string) => Locator;
|
|
39
40
|
constructor(page: Page, testInfo?: TestInfo);
|
|
40
41
|
clickClusterBanner(): Promise<void>;
|
|
42
|
+
private confirmClusterDeletion;
|
|
43
|
+
private dismissOpenDialog;
|
|
41
44
|
deleteCluster(name: string): Promise<void>;
|
|
42
45
|
clickClusterTab(): Promise<void>;
|
|
43
46
|
clickCreateNewClusterButton(): Promise<void>;
|
|
@@ -54,6 +57,30 @@ declare class ClusterPage {
|
|
|
54
57
|
clickConnectorSecretsTab(clusterName: string): Promise<void>;
|
|
55
58
|
clickLatestStableForMinorVersion(): Promise<void>;
|
|
56
59
|
clickClusterOption(): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Counts the clusters carrying `clusterName`. The clusters table renders
|
|
62
|
+
* asynchronously after a navigation or reload, so a bare `count()` can read
|
|
63
|
+
* zero rows off a list that is merely still loading -- and the caller would
|
|
64
|
+
* then create a second cluster with the same name. Give the row a chance to
|
|
65
|
+
* appear before trusting a zero.
|
|
66
|
+
*/
|
|
67
|
+
private countClusters;
|
|
68
|
+
/**
|
|
69
|
+
* Deletes the surplus rows when several clusters share `clusterName`, so the
|
|
70
|
+
* name identifies exactly one cluster again. Duplicates are created when
|
|
71
|
+
* concurrent workers run the same spec against a fixed cluster name: both
|
|
72
|
+
* see "no such cluster" and both create one (each in its own random region,
|
|
73
|
+
* which the Console accepts). Leaving them in place is not an option -- the
|
|
74
|
+
* cluster name is what every later step resolves the cluster by, in the
|
|
75
|
+
* Console list, in the apps menu, and in the Modeler deployment dialog, and
|
|
76
|
+
* those lists do not have to agree on ordering, so the flow could enable RBA
|
|
77
|
+
* on one cluster and then assert against the other.
|
|
78
|
+
*
|
|
79
|
+
* Always removes the trailing row so the surviving cluster stays the one
|
|
80
|
+
* `clusterRow()` resolves to, and a concurrent worker pruning the same list
|
|
81
|
+
* targets the same row rather than the survivor.
|
|
82
|
+
*/
|
|
83
|
+
private pruneDuplicateClusters;
|
|
57
84
|
createClusterIfNotExists(clusterName: string, region?: string): Promise<void>;
|
|
58
85
|
assertClusterHealthyStatusWithRetry(name: string, visibilityTimeout?: number, totalTimeout?: number): Promise<void>;
|
|
59
86
|
clickAlphaTab(): Promise<void>;
|
|
@@ -37,6 +37,7 @@ class ClusterPage {
|
|
|
37
37
|
tasklistV1Api;
|
|
38
38
|
tasklistV2Api;
|
|
39
39
|
cluster;
|
|
40
|
+
clusterRow;
|
|
40
41
|
clusterHealthiness;
|
|
41
42
|
clusterLink;
|
|
42
43
|
dialog;
|
|
@@ -109,15 +110,22 @@ class ClusterPage {
|
|
|
109
110
|
this.cluster = (clusterName) => this.clustersList.filter({
|
|
110
111
|
has: page.getByRole('link', { name: clusterName, exact: true }),
|
|
111
112
|
});
|
|
112
|
-
|
|
113
|
+
// The Console only enforces cluster-name uniqueness per region, so the
|
|
114
|
+
// org can hold several rows with the same name (parallel workers sharing
|
|
115
|
+
// a fixed cluster name do produce them). `cluster()` stays the full set --
|
|
116
|
+
// deleteCluster needs it -- while everything that means "the one cluster
|
|
117
|
+
// under test" goes through this single row, so a duplicate can no longer
|
|
118
|
+
// turn an ordinary visibility check into a strict-mode violation.
|
|
119
|
+
this.clusterRow = (clusterName) => this.cluster(clusterName).first();
|
|
120
|
+
this.clusterHealthiness = (clusterName) => this.clusterRow(clusterName).getByText(`Healthy`, {
|
|
113
121
|
exact: true,
|
|
114
122
|
});
|
|
115
|
-
this.clusterLink = (clusterName) => this.
|
|
123
|
+
this.clusterLink = (clusterName) => this.clusterRow(clusterName).getByRole('link', {
|
|
116
124
|
name: clusterName,
|
|
117
125
|
exact: true,
|
|
118
126
|
});
|
|
119
127
|
this.dialog = page.getByRole('dialog');
|
|
120
|
-
this.clusterVersion = (clusterName, versionText) => this.
|
|
128
|
+
this.clusterVersion = (clusterName, versionText) => this.clusterRow(clusterName).getByText(versionText, { exact: false });
|
|
121
129
|
}
|
|
122
130
|
async clickClusterBanner() {
|
|
123
131
|
await this.page
|
|
@@ -125,6 +133,30 @@ class ClusterPage {
|
|
|
125
133
|
.getByRole('link', { name: 'Clusters' })
|
|
126
134
|
.click({ timeout: 60000 });
|
|
127
135
|
}
|
|
136
|
+
async confirmClusterDeletion() {
|
|
137
|
+
await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 60000 });
|
|
138
|
+
await (0, test_1.expect)(this.confirmDeleteInput).toBeVisible({ timeout: 60000 });
|
|
139
|
+
await this.confirmDeleteInput.click({ force: true, timeout: 60000 });
|
|
140
|
+
await this.confirmDeleteInput.fill('DELETE', { timeout: 60000 });
|
|
141
|
+
const dangerDeleteButtons = await this.dangerDeleteButton.all();
|
|
142
|
+
const lastDangerDeleteButton = dangerDeleteButtons[dangerDeleteButtons.length - 1];
|
|
143
|
+
await lastDangerDeleteButton.click();
|
|
144
|
+
await (0, test_1.expect)(this.page.getByText('Deleting...')).not.toBeVisible({
|
|
145
|
+
timeout: 60000,
|
|
146
|
+
});
|
|
147
|
+
await (0, test_1.expect)(this.dialog).not.toBeVisible({ timeout: 60000 });
|
|
148
|
+
}
|
|
149
|
+
async dismissOpenDialog() {
|
|
150
|
+
const cancelButton = this.dialog
|
|
151
|
+
.first()
|
|
152
|
+
.getByRole('button', { name: 'Cancel' });
|
|
153
|
+
const cancelVisible = await cancelButton
|
|
154
|
+
.isVisible({ timeout: 2000 })
|
|
155
|
+
.catch(() => false);
|
|
156
|
+
if (cancelVisible) {
|
|
157
|
+
await cancelButton.click({ timeout: 10000 }).catch(() => false);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
128
160
|
async deleteCluster(name) {
|
|
129
161
|
await this.page.waitForTimeout(1000);
|
|
130
162
|
const clusterInstance = this.cluster(name).first();
|
|
@@ -147,17 +179,7 @@ class ClusterPage {
|
|
|
147
179
|
await (0, test_1.expect)(deleteButton).toBeVisible({ timeout: 60000 });
|
|
148
180
|
await deleteButton.click({ force: true, timeout: 60000 });
|
|
149
181
|
}
|
|
150
|
-
await
|
|
151
|
-
await (0, test_1.expect)(this.confirmDeleteInput).toBeVisible({ timeout: 60000 });
|
|
152
|
-
await this.confirmDeleteInput.click({ force: true, timeout: 60000 });
|
|
153
|
-
await this.confirmDeleteInput.fill('DELETE', { timeout: 60000 });
|
|
154
|
-
const dangerDeleteButtons = await this.dangerDeleteButton.all();
|
|
155
|
-
const lastDangerDeleteButton = dangerDeleteButtons[dangerDeleteButtons.length - 1];
|
|
156
|
-
await lastDangerDeleteButton.click();
|
|
157
|
-
await (0, test_1.expect)(this.page.getByText('Deleting...')).not.toBeVisible({
|
|
158
|
-
timeout: 60000,
|
|
159
|
-
});
|
|
160
|
-
await (0, test_1.expect)(this.dialog).not.toBeVisible({ timeout: 60000 });
|
|
182
|
+
await this.confirmClusterDeletion();
|
|
161
183
|
// Refresh the list of delete buttons
|
|
162
184
|
deleteButtons = this.cluster(name).getByRole('button', {
|
|
163
185
|
name: 'Delete',
|
|
@@ -245,15 +267,21 @@ class ClusterPage {
|
|
|
245
267
|
await this.createClusterButton.click();
|
|
246
268
|
}
|
|
247
269
|
async clickClustersBreadcrumb(clusterName) {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
270
|
+
// Scope the status check to the cluster's own row instead of matching the
|
|
271
|
+
// status text anywhere on the page: a page-wide `getByText` regex also
|
|
272
|
+
// matches every ancestor whose combined text spans the name and a status
|
|
273
|
+
// (the whole app shell does, once a second row carries the same name),
|
|
274
|
+
// 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
|
+
});
|
|
253
281
|
try {
|
|
254
282
|
await (0, test_1.expect)(this.clustersBreadcrumb).toBeVisible({ timeout: 60000 });
|
|
255
283
|
await this.clustersBreadcrumb.click({ timeout: 60000 });
|
|
256
|
-
await (0, test_1.expect)(
|
|
284
|
+
await (0, test_1.expect)(creatingRow).toBeVisible({
|
|
257
285
|
timeout: 30000,
|
|
258
286
|
});
|
|
259
287
|
return;
|
|
@@ -347,19 +375,84 @@ class ClusterPage {
|
|
|
347
375
|
}
|
|
348
376
|
throw new Error('Failed to find cluster generation');
|
|
349
377
|
}
|
|
378
|
+
/**
|
|
379
|
+
* Counts the clusters carrying `clusterName`. The clusters table renders
|
|
380
|
+
* asynchronously after a navigation or reload, so a bare `count()` can read
|
|
381
|
+
* zero rows off a list that is merely still loading -- and the caller would
|
|
382
|
+
* then create a second cluster with the same name. Give the row a chance to
|
|
383
|
+
* appear before trusting a zero.
|
|
384
|
+
*/
|
|
385
|
+
async countClusters(clusterName) {
|
|
386
|
+
await this.clusterRow(clusterName)
|
|
387
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
388
|
+
.catch(() => false);
|
|
389
|
+
return this.cluster(clusterName).count();
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Deletes the surplus rows when several clusters share `clusterName`, so the
|
|
393
|
+
* name identifies exactly one cluster again. Duplicates are created when
|
|
394
|
+
* concurrent workers run the same spec against a fixed cluster name: both
|
|
395
|
+
* see "no such cluster" and both create one (each in its own random region,
|
|
396
|
+
* which the Console accepts). Leaving them in place is not an option -- the
|
|
397
|
+
* cluster name is what every later step resolves the cluster by, in the
|
|
398
|
+
* Console list, in the apps menu, and in the Modeler deployment dialog, and
|
|
399
|
+
* those lists do not have to agree on ordering, so the flow could enable RBA
|
|
400
|
+
* on one cluster and then assert against the other.
|
|
401
|
+
*
|
|
402
|
+
* Always removes the trailing row so the surviving cluster stays the one
|
|
403
|
+
* `clusterRow()` resolves to, and a concurrent worker pruning the same list
|
|
404
|
+
* targets the same row rather than the survivor.
|
|
405
|
+
*/
|
|
406
|
+
async pruneDuplicateClusters(clusterName) {
|
|
407
|
+
const rows = this.cluster(clusterName);
|
|
408
|
+
const maxAttempts = 4;
|
|
409
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
410
|
+
const total = await this.countClusters(clusterName);
|
|
411
|
+
if (total <= 1)
|
|
412
|
+
return;
|
|
413
|
+
console.warn(`pruneDuplicateClusters: found ${total} clusters named '${clusterName}'; deleting the surplus row (attempt ${attempt}/${maxAttempts}).`);
|
|
414
|
+
try {
|
|
415
|
+
await rows
|
|
416
|
+
.nth(total - 1)
|
|
417
|
+
.getByRole('button', { name: 'Delete' })
|
|
418
|
+
.click({ force: true, timeout: 60000 });
|
|
419
|
+
await this.confirmClusterDeletion();
|
|
420
|
+
}
|
|
421
|
+
catch (error) {
|
|
422
|
+
console.warn(`pruneDuplicateClusters: could not delete the surplus '${clusterName}' row: ${error}`);
|
|
423
|
+
await this.dismissOpenDialog();
|
|
424
|
+
}
|
|
425
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
426
|
+
}
|
|
427
|
+
const remaining = await this.countClusters(clusterName);
|
|
428
|
+
if (remaining > 1) {
|
|
429
|
+
throw new Error(`Could not reduce the ${remaining} clusters named '${clusterName}' to a single one after ${maxAttempts} attempts; the cluster name no longer identifies one cluster.`);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
350
432
|
async createClusterIfNotExists(clusterName, region = 'GCP') {
|
|
351
433
|
await (0, sleep_1.sleep)(15000);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
console.log(`${clusterName} exists and is healthy.`);
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
434
|
+
await this.pruneDuplicateClusters(clusterName);
|
|
435
|
+
if ((await this.countClusters(clusterName)) === 0) {
|
|
357
436
|
await this.createCluster(clusterName, region);
|
|
358
437
|
await this.assertClusterHealthyStatusWithRetry(clusterName, 120000, 600000);
|
|
438
|
+
return;
|
|
359
439
|
}
|
|
440
|
+
if (await this.clusterHealthiness(clusterName).isVisible({ timeout: 20000 })) {
|
|
441
|
+
console.log(`${clusterName} exists and is healthy.`);
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
// The cluster exists but is not Healthy yet -- typically still Creating
|
|
445
|
+
// from a concurrent worker, or briefly status-less right after a reload.
|
|
446
|
+
// Wait for it to come up instead of deleting and recreating it: tearing
|
|
447
|
+
// down a cluster mid-provisioning is what leaves the "Delete cluster"
|
|
448
|
+
// dialog hanging open on a delete the backend rejects.
|
|
449
|
+
console.log(`${clusterName} exists but is not Healthy yet; waiting for it to come up.`);
|
|
450
|
+
await this.assertClusterHealthyStatusWithRetry(clusterName, 120000, 600000);
|
|
360
451
|
}
|
|
361
452
|
async assertClusterHealthyStatusWithRetry(name, visibilityTimeout = 100000, totalTimeout = 500000) {
|
|
362
|
-
const unhealthy = this.
|
|
453
|
+
const unhealthy = this.clusterRow(name).getByText('Unhealthy', {
|
|
454
|
+
exact: true,
|
|
455
|
+
});
|
|
363
456
|
const start = Date.now();
|
|
364
457
|
const maxRetries = 5;
|
|
365
458
|
let attempt = 0;
|
|
@@ -369,7 +462,7 @@ class ClusterPage {
|
|
|
369
462
|
attempt++;
|
|
370
463
|
try {
|
|
371
464
|
await this.page.reload();
|
|
372
|
-
await (0, test_1.expect)(this.
|
|
465
|
+
await (0, test_1.expect)(this.clusterRow(name)).toBeVisible({ timeout: 10000 });
|
|
373
466
|
// Only treat Unhealthy as terminal if it persists across two
|
|
374
467
|
// consecutive polling attempts. A cluster mid-provisioning can flash
|
|
375
468
|
// through Unhealthy briefly during the Unhealthy → Creating → Healthy
|
|
@@ -1149,18 +1149,21 @@ class ModelerCreatePage {
|
|
|
1149
1149
|
if (!alreadyDeployed) {
|
|
1150
1150
|
// Step 2: deploy
|
|
1151
1151
|
await setupDeployButton.click({ timeout });
|
|
1152
|
-
//
|
|
1153
|
-
//
|
|
1154
|
-
//
|
|
1155
|
-
//
|
|
1156
|
-
//
|
|
1157
|
-
//
|
|
1158
|
-
//
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1152
|
+
// Deploy is done when the "Configure test case" button unlocks. There
|
|
1153
|
+
// is no `deploy-success-badge` in the Test Studio DOM -- the overlay
|
|
1154
|
+
// stays open with the deploy step showing a green check and that button
|
|
1155
|
+
// enabled (this is how hub's own `Definition.setupEnvironment` e2e page
|
|
1156
|
+
// object drives it: click Deploy, then click Configure, with no badge
|
|
1157
|
+
// in between). On a build that auto-collapses the overlay the
|
|
1158
|
+
// configure-test panel renders instead, so accept either. A failed
|
|
1159
|
+
// deploy leaves the button disabled and the panel absent, so this still
|
|
1160
|
+
// times out on a real deploy failure rather than masking it.
|
|
1161
|
+
await (0, test_1.expect)(async () => {
|
|
1162
|
+
const deployComplete = (await configureScenarioButton
|
|
1163
|
+
.isEnabled({ timeout: 3000 })
|
|
1164
|
+
.catch(() => false)) || (await configureTestPanel.isVisible());
|
|
1165
|
+
(0, test_1.expect)(deployComplete).toBe(true);
|
|
1166
|
+
}).toPass({ timeout: 90000 });
|
|
1164
1167
|
}
|
|
1165
1168
|
// Step 3: leave the overlay, and confirm we actually left it. The
|
|
1166
1169
|
// button only calls hideConfigurationOverlay(); the overlay's own
|