@camunda/e2e-test-suite 0.0.1599 → 0.0.1601
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.
- package/dist/pages/8.8/ClusterPage.js +8 -0
- package/dist/pages/8.9/ClientCredentialsDetailsPage.js +17 -7
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +36 -11
- package/dist/tests/8.8/idp-user-flows.spec.js +15 -0
- package/dist/tests/8.9/operate-access-flow.spec.js +14 -3
- package/dist/utils/consoleApiHelpers.js +15 -4
- package/package.json +1 -1
|
@@ -296,6 +296,14 @@ class ClusterPage {
|
|
|
296
296
|
await this.selectSubRegion(region);
|
|
297
297
|
await this.clickClusterOption(process.env.CLUSTER_VERSION);
|
|
298
298
|
await this.determineTasklistAPI(tasklistV1Api);
|
|
299
|
+
// The form re-renders when its session/parameters refresh lands after the
|
|
300
|
+
// name was typed, which clears the name: Create then only shows "Please
|
|
301
|
+
// enter a name for the cluster" and no cluster is ever requested. Re-fill
|
|
302
|
+
// it right before submitting.
|
|
303
|
+
if ((await this.clusterNameInput.inputValue()) !== name) {
|
|
304
|
+
await this.fillClusterNameInput(name);
|
|
305
|
+
}
|
|
306
|
+
await (0, test_1.expect)(this.clusterNameInput).toHaveValue(name, { timeout: 10000 });
|
|
299
307
|
await this.clickCreateClusterButton();
|
|
300
308
|
await this.logClusterCreationInfo(name);
|
|
301
309
|
await this.clickClustersBreadcrumb(name);
|
|
@@ -17,15 +17,25 @@ class ClientCredentialsDetailsPage {
|
|
|
17
17
|
await (0, test_1.expect)(this.clientNameHeading(clientName).or(this.operateUrlRow).first()).toBeVisible({ timeout: 60000 });
|
|
18
18
|
}
|
|
19
19
|
async getOperateUrl() {
|
|
20
|
-
if (await this.operateUrlValue.isVisible(
|
|
20
|
+
if (await this.operateUrlValue.isVisible().catch(() => false)) {
|
|
21
21
|
return await this.operateUrlValue.inputValue();
|
|
22
22
|
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
// locator.isVisible() ignores its timeout, so the old probe read the body
|
|
24
|
+
// the instant the client view opened and failed whenever the credentials
|
|
25
|
+
// rendered a moment later. Poll the rendered text until the URL appears.
|
|
26
|
+
const pattern = /Operate URL\s*([\w:/.-]+\/?)/i;
|
|
27
|
+
let operateUrl = '';
|
|
28
|
+
await test_1.expect
|
|
29
|
+
.poll(async () => {
|
|
30
|
+
const text = await this.page.innerText('body').catch(() => '');
|
|
31
|
+
operateUrl = text.match(pattern)?.[1] ?? '';
|
|
32
|
+
return operateUrl;
|
|
33
|
+
}, {
|
|
34
|
+
timeout: 30000,
|
|
35
|
+
message: 'Operate URL not found in client credentials page',
|
|
36
|
+
})
|
|
37
|
+
.not.toBe('');
|
|
38
|
+
return operateUrl;
|
|
29
39
|
}
|
|
30
40
|
async goBack() {
|
|
31
41
|
await this.page.goBack();
|
|
@@ -39,6 +39,7 @@ declare class ClusterDetailsPage {
|
|
|
39
39
|
readonly createAlertButton: Locator;
|
|
40
40
|
readonly createNewClientButton: Locator;
|
|
41
41
|
readonly clientsList: Locator;
|
|
42
|
+
readonly clientsFilterInput: Locator;
|
|
42
43
|
readonly envVarsTab: Locator;
|
|
43
44
|
readonly zeebeCheckbox: Locator;
|
|
44
45
|
readonly apiClientNameTextbox: Locator;
|
|
@@ -51,6 +51,7 @@ class ClusterDetailsPage {
|
|
|
51
51
|
createAlertButton;
|
|
52
52
|
createNewClientButton;
|
|
53
53
|
clientsList;
|
|
54
|
+
clientsFilterInput;
|
|
54
55
|
envVarsTab;
|
|
55
56
|
zeebeCheckbox;
|
|
56
57
|
apiClientNameTextbox;
|
|
@@ -194,6 +195,13 @@ class ClusterDetailsPage {
|
|
|
194
195
|
name: 'Create new client',
|
|
195
196
|
});
|
|
196
197
|
this.clientsList = page.getByRole('row').filter({ hasNotText: 'Scopes' });
|
|
198
|
+
// The clients table paginates and isn't ordered by creation time, so a
|
|
199
|
+
// newly created client can sit on a page other than the one displayed.
|
|
200
|
+
// Filter it first (same locator as pages/8.10/ClusterDetailsPage.ts).
|
|
201
|
+
this.clientsFilterInput = page
|
|
202
|
+
.getByRole('searchbox', { name: 'Filter table' })
|
|
203
|
+
.or(page.getByRole('searchbox', { name: 'Search' }))
|
|
204
|
+
.or(page.getByPlaceholder('Search'));
|
|
197
205
|
this.envVarsTab = this.clientCredentialsDialog.getByRole('tab', {
|
|
198
206
|
name: 'Env Vars',
|
|
199
207
|
});
|
|
@@ -313,6 +321,8 @@ class ClusterDetailsPage {
|
|
|
313
321
|
const maxAttempts = 3;
|
|
314
322
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
315
323
|
try {
|
|
324
|
+
await (0, test_1.expect)(this.clientsFilterInput).toBeVisible({ timeout: 60000 });
|
|
325
|
+
await this.clientsFilterInput.fill(name);
|
|
316
326
|
await (0, test_1.expect)(row).toContainText(scopes, { timeout: 60000 });
|
|
317
327
|
return;
|
|
318
328
|
}
|
|
@@ -565,6 +575,8 @@ class ClusterDetailsPage {
|
|
|
565
575
|
}
|
|
566
576
|
async searchAndClickClientCredentialsLink(name) {
|
|
567
577
|
await (0, test_1.expect)(this.clientsList.first()).toBeVisible({ timeout: 60000 });
|
|
578
|
+
await (0, test_1.expect)(this.clientsFilterInput).toBeVisible({ timeout: 60000 });
|
|
579
|
+
await this.clientsFilterInput.fill(name);
|
|
568
580
|
const clientLink = this.clientCredentialsLink(name);
|
|
569
581
|
await (0, test_1.expect)(clientLink).toBeVisible({ timeout: 30000 });
|
|
570
582
|
await clientLink.click();
|
|
@@ -823,13 +835,24 @@ class ClusterDetailsPage {
|
|
|
823
835
|
return variables;
|
|
824
836
|
}
|
|
825
837
|
async createAPIClient(name) {
|
|
826
|
-
|
|
838
|
+
// Console API requests on SaaS INT fail in bursts (net::ERR_FAILED) that
|
|
839
|
+
// can last a few minutes: GETs recover via the app's own refetch, but
|
|
840
|
+
// the create POST is not retried and surfaces as "fetch error", and the
|
|
841
|
+
// reload between attempts lands the panel with "Create new client"
|
|
842
|
+
// disabled until the cluster queries succeed. Five attempts, each one
|
|
843
|
+
// waiting for the panel to recover first, ride out such a burst.
|
|
844
|
+
const maxRetries = 5;
|
|
827
845
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
828
846
|
// Use a suffixed name on retries so duplicate-name validation never
|
|
829
847
|
// leaves the Create button permanently disabled. A prior attempt may
|
|
830
848
|
// have created the client before we could detect the credentials dialog.
|
|
831
849
|
const attemptName = attempt === 0 ? name : `${name}-r${attempt}`;
|
|
832
850
|
try {
|
|
851
|
+
// Wait for the clients panel to be usable before clicking: after a
|
|
852
|
+
// failed burst the button renders disabled for tens of seconds, and
|
|
853
|
+
// clicking it only burns clickLocatorWithRetry's attempts.
|
|
854
|
+
await this.dismissOopsIfPresent();
|
|
855
|
+
await (0, test_1.expect)(this.createFirstClientButton.or(this.createNewClientButton)).toBeEnabled({ timeout: 60000 });
|
|
833
856
|
await this.clickCreateClientButton();
|
|
834
857
|
await (0, test_1.expect)(this.createClientCredentialsDialog).toBeVisible({
|
|
835
858
|
timeout: 30000,
|
|
@@ -845,20 +868,22 @@ class ClusterDetailsPage {
|
|
|
845
868
|
// Secret will not be shown again.") dialog never appears. Detect
|
|
846
869
|
// that state and retry instead of burning 60s on a visibility
|
|
847
870
|
// assertion that will never resolve.
|
|
871
|
+
// locator.isVisible() ignores its timeout and returns immediately,
|
|
872
|
+
// so wait for either outcome explicitly before branching on it.
|
|
873
|
+
const fetchErrorBanner = this.createClientCredentialsDialog.getByText(/fetch error/i);
|
|
874
|
+
await this.clientCredentialsDialog
|
|
875
|
+
.or(fetchErrorBanner)
|
|
876
|
+
.first()
|
|
877
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
878
|
+
.catch(() => false);
|
|
848
879
|
const credentialsShown = await this.clientCredentialsDialog
|
|
849
|
-
.isVisible(
|
|
880
|
+
.isVisible()
|
|
850
881
|
.catch(() => false);
|
|
851
882
|
if (!credentialsShown) {
|
|
852
|
-
const
|
|
853
|
-
.isVisible(
|
|
883
|
+
const fetchError = await fetchErrorBanner
|
|
884
|
+
.isVisible()
|
|
854
885
|
.catch(() => false);
|
|
855
|
-
|
|
856
|
-
? await this.createClientCredentialsDialog
|
|
857
|
-
.getByText(/fetch error/i)
|
|
858
|
-
.isVisible({ timeout: 500 })
|
|
859
|
-
.catch(() => false)
|
|
860
|
-
: false;
|
|
861
|
-
if (stuckCreateDialog && fetchError) {
|
|
886
|
+
if (fetchError) {
|
|
862
887
|
throw new Error('createAPIClient: create dialog stuck with "fetch error"');
|
|
863
888
|
}
|
|
864
889
|
// Not the fetch-error path — fall through to the original
|
|
@@ -8,6 +8,7 @@ const randomName_1 = require("../../utils/randomName");
|
|
|
8
8
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
9
9
|
const connectorSecrets_1 = require("../../utils/connectorSecrets");
|
|
10
10
|
const users_1 = require("../../utils/users");
|
|
11
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
11
12
|
const testUser = (0, users_1.getTestUser)('twentiethUser');
|
|
12
13
|
let idpApplicationName;
|
|
13
14
|
let idpDocumentExtractionTemplateName;
|
|
@@ -88,6 +89,13 @@ _8_8_1.test.describe('IDP User Flow Tests', () => {
|
|
|
88
89
|
await modelerHomePage.clickCrossComponentProjectFolder();
|
|
89
90
|
});
|
|
90
91
|
await _8_8_1.test.step('Add an IDP application to the project', async () => {
|
|
92
|
+
// The "Create IDP project" dialog only offers the Environments
|
|
93
|
+
// assigned to the workspace. test-setup's link of the cluster is a
|
|
94
|
+
// no-op when the workspace does not exist yet, so on a fresh org the
|
|
95
|
+
// dialog showed "No deployment environments available" (or only
|
|
96
|
+
// another spec's cluster) on the first attempt. Ensure the link here,
|
|
97
|
+
// as every deploy path in the page objects already does.
|
|
98
|
+
await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(page, clusterName);
|
|
91
99
|
// Workspace root only: an IDP application can never be created
|
|
92
100
|
// inside a process application. The entry point there is the create
|
|
93
101
|
// menu or the empty state, depending on whether the workspace
|
|
@@ -163,6 +171,13 @@ _8_8_1.test.describe('IDP User Flow Tests', () => {
|
|
|
163
171
|
await modelerHomePage.clickCrossComponentProjectFolder();
|
|
164
172
|
});
|
|
165
173
|
await _8_8_1.test.step('Add an IDP application to the project', async () => {
|
|
174
|
+
// The "Create IDP project" dialog only offers the Environments
|
|
175
|
+
// assigned to the workspace. test-setup's link of the cluster is a
|
|
176
|
+
// no-op when the workspace does not exist yet, so on a fresh org the
|
|
177
|
+
// dialog showed "No deployment environments available" (or only
|
|
178
|
+
// another spec's cluster) on the first attempt. Ensure the link here,
|
|
179
|
+
// as every deploy path in the page objects already does.
|
|
180
|
+
await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(page, clusterName);
|
|
166
181
|
// Workspace root only: an IDP application can never be created
|
|
167
182
|
// inside a process application. The entry point there is the create
|
|
168
183
|
// menu or the empty state, depending on whether the workspace
|
|
@@ -57,7 +57,7 @@ _8_9_1.test.describe('Operate access requires authentication @tasklistV2', () =>
|
|
|
57
57
|
(0, test_1.expect)([200, 405]).toContain(response.status());
|
|
58
58
|
});
|
|
59
59
|
});
|
|
60
|
-
(0, _8_9_1.test)('check that POST /v2/user-tasks/search returns 401 without credentials', async ({ homePage, clusterPage, clusterDetailsPage, clientCredentialsDetailsPage, request, }) => {
|
|
60
|
+
(0, _8_9_1.test)('check that POST /v2/user-tasks/search returns 401 without credentials', async ({ page, homePage, clusterPage, clusterDetailsPage, clientCredentialsDetailsPage, request, }) => {
|
|
61
61
|
clientName = `tasklist-deny-${await (0, _setup_1.generateRandomStringAsync)(5)}`;
|
|
62
62
|
let operateUrl = '';
|
|
63
63
|
await _8_9_1.test.step('Add API Client to Cluster', async () => {
|
|
@@ -69,8 +69,19 @@ _8_9_1.test.describe('Operate access requires authentication @tasklistV2', () =>
|
|
|
69
69
|
await (0, test_1.expect)(clusterDetailsPage.clientsList.filter({ hasText: clientName })).toBeVisible({ timeout: 6000 });
|
|
70
70
|
});
|
|
71
71
|
await _8_9_1.test.step('Capture Operate URL (base URL shared with Tasklist v2 endpoint)', async () => {
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
// A failed Console API request can render the client view as "Oops ...
|
|
73
|
+
// something went wrong." instead of its credentials. The view is not a
|
|
74
|
+
// route, so recover by reloading the API tab and reopening the client.
|
|
75
|
+
let firstAttempt = true;
|
|
76
|
+
await (0, test_1.expect)(async () => {
|
|
77
|
+
if (!firstAttempt) {
|
|
78
|
+
await page.reload({ waitUntil: 'domcontentloaded' });
|
|
79
|
+
await clusterDetailsPage.clickAPITab();
|
|
80
|
+
}
|
|
81
|
+
firstAttempt = false;
|
|
82
|
+
await clusterDetailsPage.searchAndClickClientCredentialsLink(clientName);
|
|
83
|
+
operateUrl = await clientCredentialsDetailsPage.getOperateUrl();
|
|
84
|
+
}).toPass({ timeout: 240000, intervals: [5000] });
|
|
74
85
|
(0, test_1.expect)(operateUrl).toMatch(/^https?:\/\//);
|
|
75
86
|
await clientCredentialsDetailsPage.goBack();
|
|
76
87
|
await clusterDetailsPage.clickAPITab();
|
|
@@ -381,15 +381,26 @@ function hasGenerationUuid() {
|
|
|
381
381
|
// without the legacyStreams limit -- which includes every QA org -- so a name
|
|
382
382
|
// lookup cannot find them, but cluster creation still validates against the
|
|
383
383
|
// channel's full generation list. A UUID therefore reaches a generation the org
|
|
384
|
-
// cannot list. The channel is taken, in order, from
|
|
385
|
-
//
|
|
384
|
+
// cannot list. The channel is taken, in order, from the channel that lists the
|
|
385
|
+
// UUID, CLUSTER_CHANNEL_UUID, or the Stable channel. A listing is authoritative:
|
|
386
|
+
// release trains always send the Stable channel, but pre-release generations
|
|
387
|
+
// (e.g. "Camunda 8.11.0-alpha1") live only in the Alpha channel, and Console
|
|
388
|
+
// rejects that pairing with HTTP 400 "Generation is not part of allowed
|
|
389
|
+
// generations of selected channel". CLUSTER_CHANNEL_UUID therefore only decides
|
|
390
|
+
// the channel for a generation the org cannot list.
|
|
386
391
|
function pickGenerationByUuid(channels) {
|
|
387
392
|
const generationId = (process.env.CLUSTER_GENERATION_UUID ?? '').trim();
|
|
388
393
|
if (!generationId)
|
|
389
394
|
return undefined;
|
|
390
395
|
const listedIn = channels.find((channel) => (channel.allowedGenerations ?? []).some((generation) => generation.uuid === generationId));
|
|
391
|
-
const
|
|
392
|
-
|
|
396
|
+
const requestedChannelId = (process.env.CLUSTER_CHANNEL_UUID ?? '').trim();
|
|
397
|
+
if (listedIn && requestedChannelId && requestedChannelId !== listedIn.uuid) {
|
|
398
|
+
console.log(`CLUSTER_CHANNEL_UUID ${requestedChannelId} does not list generation ` +
|
|
399
|
+
`${generationId}; using channel "${listedIn.name}" (${listedIn.uuid}) ` +
|
|
400
|
+
`that does`);
|
|
401
|
+
}
|
|
402
|
+
const channelId = listedIn?.uuid ||
|
|
403
|
+
requestedChannelId ||
|
|
393
404
|
channels.find((channel) => channel.name === STABLE_CHANNEL_NAME)?.uuid;
|
|
394
405
|
if (!channelId) {
|
|
395
406
|
throw new Error(`No channel for generation ${generationId}: set CLUSTER_CHANNEL_UUID ` +
|