@camunda/e2e-test-suite 0.0.1478 → 0.0.1479

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.
@@ -209,7 +209,21 @@ class FormJsPage {
209
209
  throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
210
210
  'binding did not take. It will be rebound on the next attempt.');
211
211
  }
212
- return;
212
+ // The probe above runs the instant the dialog opens, before its cluster
213
+ // list has rendered, so a selectable list can be mistaken for the
214
+ // auto-selecting one. The dialog then deploys to whichever cluster it
215
+ // pre-selected: a nightly trace shows the form going to "Test Cluster"
216
+ // while "AWS Cluster" was requested, so the user task linking it never
217
+ // reached Tasklist. Re-probe now that the name has rendered and select
218
+ // the cluster explicitly when it turns out to be a list row.
219
+ const isSelectableRow = await legacyClusterToggle
220
+ .first()
221
+ .waitFor({ state: 'visible', timeout: 5000 })
222
+ .then(() => true)
223
+ .catch(() => false);
224
+ if (!isSelectableRow) {
225
+ return;
226
+ }
213
227
  }
214
228
  // "HealthydevManage" was the Carbon row's chrome concatenated together.
215
229
  // The design-system dialog renders the cluster row as
@@ -879,7 +879,20 @@ class ModelerCreatePage {
879
879
  throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
880
880
  'binding did not take. It will be rebound on the next attempt.');
881
881
  }
882
- return;
882
+ // The probe above runs the instant the dialog opens, before its cluster
883
+ // list has rendered, so a selectable list can be mistaken for the
884
+ // auto-selecting one and the run goes to whichever cluster the dialog
885
+ // pre-selected (seen in FormJsPage.selectCluster's nightly trace).
886
+ // Re-probe now that the name has rendered and select the cluster
887
+ // explicitly when it turns out to be a list row.
888
+ const isSelectableRow = await legacyClusterToggle
889
+ .first()
890
+ .waitFor({ state: 'visible', timeout: 5000 })
891
+ .then(() => true)
892
+ .catch(() => false);
893
+ if (!isSelectableRow) {
894
+ return;
895
+ }
883
896
  }
884
897
  const healthyRegex = new RegExp(`${clusterName}.*?Healthy`);
885
898
  const clusterLabel = this.dialog
@@ -1169,7 +1169,19 @@ class ModelerHomePage {
1169
1169
  // rows. Fall back to clicking the row itself (it is `cursor-pointer` and
1170
1170
  // handles the row activation) when the id is not exposed.
1171
1171
  const rowMenuTrigger = projectEntryRow.locator('[data-test="project-row-menu-trigger"]');
1172
- if (await this.becameVisible(rowMenuTrigger, 10000)) {
1172
+ let rowVisible = await this.becameVisible(rowMenuTrigger, 10000);
1173
+ // Hub can drop the session while the list loads (its projects call answers
1174
+ // 230 and the SPA bounces to /login), so the probe above misses a project
1175
+ // that exists and the create branch below adds an empty duplicate -- in
1176
+ // which the caller's diagram can never be found. Re-sign-in (often a
1177
+ // silent SSO round trip that has already finished by now, so do not gate
1178
+ // on recoverModelerSession's result) returns the browser to this list;
1179
+ // look again before creating anything.
1180
+ if (!rowVisible) {
1181
+ await this.recoverModelerSession();
1182
+ rowVisible = await this.becameVisible(rowMenuTrigger, 30000);
1183
+ }
1184
+ if (rowVisible) {
1173
1185
  const projectId = await rowMenuTrigger.getAttribute('data-project-id');
1174
1186
  const workspaceId = new URL(this.page.url()).pathname.match(/\/workspaces\/([^/]+)/)?.[1];
1175
1187
  if (workspaceId) {
@@ -273,31 +273,46 @@ async function authSaasAPI(audience, clusterType) {
273
273
  // "You are being rate limited" page when many specs fetch a token at once
274
274
  // (each MCP spec authenticates in beforeAll). Retry with exponential
275
275
  // backoff so the rate-limit window clears instead of failing the suite.
276
- const maxAttempts = 5;
277
- const baseDelayMs = 3000;
276
+ // While throttled, Cloudflare may also hold the connection open, so a
277
+ // request timeout is retried like a 429 rather than aborting the loop.
278
+ const maxAttempts = 7;
279
+ const baseDelayMs = 5000;
280
+ const maxDelayMs = 60000;
281
+ const requestTimeoutMs = 30000;
278
282
  let lastErrorMessage = '';
279
283
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
280
- const tokenResponse = await apiRequestContext.post(creds.authUrl, {
281
- headers: {
282
- 'Content-Type': 'application/json',
283
- },
284
- data: {
285
- audience: tokenAudience,
286
- client_id: creds.clientId,
287
- client_secret: creds.clientSecret,
288
- grant_type: 'client_credentials',
289
- },
290
- });
291
- if (tokenResponse.status() === 200) {
292
- const tokenJson = await tokenResponse.json();
293
- return `Bearer ${tokenJson.access_token}`;
284
+ let retryAfterMs = 0;
285
+ try {
286
+ const tokenResponse = await apiRequestContext.post(creds.authUrl, {
287
+ headers: {
288
+ 'Content-Type': 'application/json',
289
+ },
290
+ data: {
291
+ audience: tokenAudience,
292
+ client_id: creds.clientId,
293
+ client_secret: creds.clientSecret,
294
+ grant_type: 'client_credentials',
295
+ },
296
+ timeout: requestTimeoutMs,
297
+ });
298
+ if (tokenResponse.status() === 200) {
299
+ const tokenJson = await tokenResponse.json();
300
+ return `Bearer ${tokenJson.access_token}`;
301
+ }
302
+ const retryAfter = Number(tokenResponse.headers()['retry-after']);
303
+ if (Number.isFinite(retryAfter) && retryAfter > 0) {
304
+ retryAfterMs = Math.min(retryAfter * 1000, maxDelayMs);
305
+ }
306
+ const errorBody = await tokenResponse.text();
307
+ lastErrorMessage = `HTTP ${tokenResponse.status()} - ${errorBody.slice(0, 200)}`;
308
+ }
309
+ catch (error) {
310
+ lastErrorMessage = error.message.split('\n')[0];
294
311
  }
295
- const errorBody = await tokenResponse.text();
296
- lastErrorMessage = `HTTP ${tokenResponse.status()} - ${errorBody.slice(0, 200)}`;
297
312
  if (attempt < maxAttempts) {
313
+ const backoffMs = Math.min(baseDelayMs * 2 ** (attempt - 1), maxDelayMs);
298
314
  console.warn(`authSaasAPI attempt ${attempt}/${maxAttempts} failed to fetch access token (${lastErrorMessage}); retrying after backoff...`);
299
- await new Promise((r) => setTimeout(r, baseDelayMs * 2 ** (attempt - 1)));
300
- continue;
315
+ await new Promise((r) => setTimeout(r, Math.max(backoffMs, retryAfterMs)));
301
316
  }
302
317
  }
303
318
  throw new Error(`Failed to fetch access token. Response: ${lastErrorMessage}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1478",
3
+ "version": "0.0.1479",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",