@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.
@@ -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({ timeout: 10000 }).catch(() => false)) {
20
+ if (await this.operateUrlValue.isVisible().catch(() => false)) {
21
21
  return await this.operateUrlValue.inputValue();
22
22
  }
23
- const text = await this.page.innerText('body');
24
- const match = text.match(/Operate URL\s*([\w:/.-]+\/?)/i);
25
- if (!match || !match[1]) {
26
- throw new Error('Operate URL not found in client credentials page');
27
- }
28
- return match[1];
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
- const maxRetries = 3;
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({ timeout: 30000 })
880
+ .isVisible()
850
881
  .catch(() => false);
851
882
  if (!credentialsShown) {
852
- const stuckCreateDialog = await this.createClientCredentialsDialog
853
- .isVisible({ timeout: 500 })
883
+ const fetchError = await fetchErrorBanner
884
+ .isVisible()
854
885
  .catch(() => false);
855
- const fetchError = stuckCreateDialog
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
- await clusterDetailsPage.searchAndClickClientCredentialsLink(clientName);
73
- operateUrl = await clientCredentialsDetailsPage.getOperateUrl();
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 CLUSTER_CHANNEL_UUID, the
385
- // channel that lists the UUID, or the Stable channel.
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 channelId = (process.env.CLUSTER_CHANNEL_UUID ?? '').trim() ||
392
- listedIn?.uuid ||
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 ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1599",
3
+ "version": "0.0.1601",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",