@camunda/e2e-test-suite 0.0.906 → 0.0.908

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.
@@ -33,12 +33,14 @@ declare class ClusterSecretsPage {
33
33
  name: string;
34
34
  value: string;
35
35
  }[]): Promise<void>;
36
+ reopenConnectorSecretsTab(cluster: string): Promise<void>;
37
+ assertSecretsToolbarVisible(cluster: string): Promise<void>;
36
38
  assertSecretsOrCreate(cluster: string, secrets: {
37
39
  name: string;
38
40
  value: string;
39
41
  }[]): Promise<void>;
40
- clickImportButton(): Promise<void>;
41
- bulkImportSecrets(secrets: {
42
+ clickImportButton(cluster: string): Promise<void>;
43
+ bulkImportSecrets(cluster: string, secrets: {
42
44
  name: string;
43
45
  value: string;
44
46
  }[]): Promise<void>;
@@ -169,9 +169,33 @@ class ClusterSecretsPage {
169
169
  });
170
170
  }
171
171
  async createSetOfSecrets(cluster, secrets) {
172
- await this.bulkImportSecrets(secrets);
172
+ await this.bulkImportSecrets(cluster, secrets);
173
173
  await this.assertSecretsOrCreate(cluster, secrets);
174
174
  }
175
+ // The Connector Secrets panel renders "Oops ... something went wrong" as soon
176
+ // as one of its Console API calls is rejected (the org-wide rate limit answers
177
+ // 429 under the nightly's worker count), and reloading from that state can
178
+ // land on the Console SPA's global "An error has occurred" page, which never
179
+ // renders the secrets toolbar again. Re-enter the tab from the Clusters list
180
+ // instead of reloading in place, so recovery does not depend on the broken
181
+ // page's own client-side state.
182
+ async reopenConnectorSecretsTab(cluster) {
183
+ await this.page.goto('/');
184
+ await this.clickClusterBanner();
185
+ await this.clickCluster(cluster);
186
+ await (0, test_1.expect)(this.clusterSecretTab).toBeVisible({ timeout: 30000 });
187
+ await this.clusterSecretTab.click({ timeout: 30000 });
188
+ }
189
+ async assertSecretsToolbarVisible(cluster) {
190
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewSecretButton, {
191
+ visibilityTimeout: 60000,
192
+ totalTimeout: 300000,
193
+ maxRetries: 3,
194
+ postAction: async () => {
195
+ await this.reopenConnectorSecretsTab(cluster);
196
+ },
197
+ });
198
+ }
175
199
  async assertSecretsOrCreate(cluster, secrets) {
176
200
  for (const secret of secrets) {
177
201
  try {
@@ -189,19 +213,29 @@ class ClusterSecretsPage {
189
213
  }
190
214
  }
191
215
  }
192
- async clickImportButton() {
193
- await (0, test_1.expect)(this.importButton).toBeVisible({ timeout: 30000 });
216
+ async clickImportButton(cluster) {
217
+ // Same broken-panel state as assertSecretsToolbarVisible guards against:
218
+ // when the secrets panel failed to load, Import is simply absent, and only
219
+ // re-entering the tab brings it back.
220
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.importButton, {
221
+ visibilityTimeout: 30000,
222
+ totalTimeout: 180000,
223
+ maxRetries: 3,
224
+ postAction: async () => {
225
+ await this.reopenConnectorSecretsTab(cluster);
226
+ },
227
+ });
194
228
  await this.importButton.click({ timeout: 30000 });
195
229
  }
196
- async bulkImportSecrets(secrets) {
230
+ async bulkImportSecrets(cluster, secrets) {
197
231
  const mapped = secrets
198
232
  .map((secret) => `${secret.name}=${secret.value}`)
199
233
  .join('\n');
200
- await this.clickImportButton();
234
+ await this.clickImportButton(cluster);
201
235
  if (!(await this.dialog.isVisible())) {
202
236
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.fromFileButton, {
203
237
  postAction: async () => {
204
- await this.clickImportButton();
238
+ await this.clickImportButton(cluster);
205
239
  },
206
240
  });
207
241
  await this.fromFileButton.click();
@@ -229,9 +263,11 @@ class ClusterSecretsPage {
229
263
  await this.page.reload();
230
264
  await (0, sleep_1.sleep)(5000);
231
265
  }
232
- await (0, test_1.expect)(this.createNewSecretButton).toBeVisible({
233
- timeout: 60000,
234
- });
266
+ // A single reload is not enough when the panel broke because the Console API
267
+ // rate-limited it: the reload itself can land on the SPA's global error page
268
+ // (observed in run 30971320305 for both cluster setups), so keep re-entering
269
+ // the tab until the toolbar is genuinely rendered.
270
+ await this.assertSecretsToolbarVisible(cluster);
235
271
  await this.page.reload();
236
272
  await (0, sleep_1.sleep)(5000);
237
273
  }
@@ -376,7 +376,9 @@ class ModelerCreatePage {
376
376
  await clusterSelect.click();
377
377
  const escapedClusterName = modelerClusterName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
378
378
  await this.dialog
379
- .getByText(new RegExp(`^${escapedClusterName}(?: \\(|$)`))
379
+ .getByRole('option', {
380
+ name: new RegExp(`^${escapedClusterName}(?: \\(|$)`),
381
+ })
380
382
  .click();
381
383
  await (0, test_1.expect)(clusterSelect).toContainText(modelerClusterName);
382
384
  }
@@ -55,19 +55,54 @@ async function getUserToken(page) {
55
55
  }
56
56
  return token;
57
57
  }
58
+ // Console's API sits behind an Envoy local rate limit, which rejects bursts
59
+ // with HTTP 429 "local_rate_limited". The nightly drives a single org from many
60
+ // workers (both Tasklist projects' setups plus one project-folder test per test
61
+ // user), so a cluster call can be rejected while nothing is actually wrong.
62
+ // Retry those — and gateway 5xx — with backoff instead of failing setup on the
63
+ // first rejection.
64
+ const TRANSIENT_API_STATUSES = [429, 500, 502, 503, 504];
65
+ const API_MAX_ATTEMPTS = 5;
66
+ async function clusterApiRequest(page, method, url, data) {
67
+ let response;
68
+ for (let attempt = 1; attempt <= API_MAX_ATTEMPTS; attempt++) {
69
+ const token = await getUserToken(page);
70
+ response = await page.request.fetch(url, {
71
+ method,
72
+ headers: { Authorization: `Bearer ${token}` },
73
+ ...(data === undefined ? {} : { data }),
74
+ });
75
+ if (!TRANSIENT_API_STATUSES.includes(response.status()))
76
+ return response;
77
+ console.log(`Console API ${method} ${url} returned HTTP ${response.status()} ` +
78
+ `(attempt ${attempt}/${API_MAX_ATTEMPTS}), retrying`);
79
+ if (attempt < API_MAX_ATTEMPTS)
80
+ await (0, sleep_1.sleep)(attempt * 10000);
81
+ }
82
+ return response;
83
+ }
58
84
  async function findClusterByName(page, clusterName) {
59
- const token = await getUserToken(page);
60
85
  const orgId = getOrgId();
61
- const response = await page.request.get(`${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`, { headers: { Authorization: `Bearer ${token}` } });
62
- if (response.status() !== 200)
63
- return undefined;
86
+ const response = await clusterApiRequest(page, 'GET', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`);
87
+ // Never report "not found" for a lookup that did not actually complete.
88
+ // ensureClusterViaApi treats undefined as "no such cluster yet" and creates
89
+ // one, so swallowing a rate-limited/failed listing here makes it create a
90
+ // second cluster with the same name — orphaning the one the sibling project's
91
+ // setup is already using (and, when the create is rate-limited too, failing
92
+ // the whole setup with a misleading "Failed to create cluster" error). Only a
93
+ // successful, parseable listing can prove a cluster is absent.
94
+ if (response.status() !== 200) {
95
+ throw new Error(`Failed to list clusters of org ${orgId}: ` +
96
+ `HTTP ${response.status()} - ${await response.text()}`);
97
+ }
64
98
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
99
  let data;
66
100
  try {
67
101
  data = await response.json();
68
102
  }
69
103
  catch {
70
- return undefined;
104
+ throw new Error(`Cluster listing of org ${orgId} was not valid JSON: ` +
105
+ `${await response.text()}`);
71
106
  }
72
107
  const clusters = Array.isArray(data)
73
108
  ? data
@@ -199,7 +234,6 @@ function requestedGenerationName() {
199
234
  return clusterVersion;
200
235
  }
201
236
  async function createClusterViaApi(page, clusterName, region = 'GCP') {
202
- const token = await getUserToken(page);
203
237
  const orgId = getOrgId();
204
238
  // Some trigger flows (e.g. playwright_saas_pr_trigger_monorepo.yml) create a
205
239
  // throwaway cluster generation per run, built from that specific commit's
@@ -234,24 +268,21 @@ async function createClusterViaApi(page, clusterName, region = 'GCP') {
234
268
  generationId = resolveGenerationId();
235
269
  }
236
270
  const k8sContextId = region.toUpperCase() === 'AWS' ? AWS_REGION_ID : GCP_REGION_ID;
237
- const createResponse = await page.request.post(`${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`, {
238
- headers: { Authorization: `Bearer ${token}` },
239
- data: {
240
- name: clusterName,
241
- planTypeId: PLAN_TYPE_ID,
242
- channelId,
243
- generationId,
244
- k8sContextId,
245
- autoUpdate: true,
246
- stageLabel: 'dev',
247
- // Upgrading a cluster to Tasklist v2 is a one-way, irreversible switch,
248
- // and v2 cannot run job-worker-based user tasks, draft variables, or
249
- // public start forms -- all of which the @tasklistV1-tagged suites
250
- // exercise. Explicitly request v1 rather than relying on whatever the
251
- // Console API defaults new clusters to, since a cluster that ends up
252
- // v2-enabled can never be brought back to v1 for the rest of its life.
253
- tasklistV2Enabled: false,
254
- },
271
+ const createResponse = await clusterApiRequest(page, 'POST', `${getConsoleBaseUrl()}/api/orgs/${orgId}/clusters`, {
272
+ name: clusterName,
273
+ planTypeId: PLAN_TYPE_ID,
274
+ channelId,
275
+ generationId,
276
+ k8sContextId,
277
+ autoUpdate: true,
278
+ stageLabel: 'dev',
279
+ // Upgrading a cluster to Tasklist v2 is a one-way, irreversible switch,
280
+ // and v2 cannot run job-worker-based user tasks, draft variables, or
281
+ // public start forms -- all of which the @tasklistV1-tagged suites
282
+ // exercise. Explicitly request v1 rather than relying on whatever the
283
+ // Console API defaults new clusters to, since a cluster that ends up
284
+ // v2-enabled can never be brought back to v1 for the rest of its life.
285
+ tasklistV2Enabled: false,
255
286
  });
256
287
  if (createResponse.status() !== 200 && createResponse.status() !== 201) {
257
288
  throw new Error(`Failed to create cluster "${clusterName}": ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.906",
3
+ "version": "0.0.908",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",