@camunda/e2e-test-suite 0.0.1477 → 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.
- package/dist/pages/8.8/FormJsPage.js +15 -1
- package/dist/pages/8.8/ModelerCreatePage.js +14 -1
- package/dist/pages/8.8/ModelerHomePage.js +13 -1
- package/dist/pages/SM-8.7/NavigationPage.d.ts +1 -1
- package/dist/pages/SM-8.7/NavigationPage.js +15 -5
- package/dist/utils/apiHelpers.js +35 -20
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) {
|
|
@@ -20,7 +20,7 @@ declare class NavigationPage {
|
|
|
20
20
|
goToTasklist(sleepTimeout?: number): Promise<void>;
|
|
21
21
|
goToOperate(sleepTimeout?: number): Promise<void>;
|
|
22
22
|
goToOptimize(sleepTimeout?: number): Promise<void>;
|
|
23
|
-
goToKeycloak(maxRetries?: number): Promise<void>;
|
|
23
|
+
goToKeycloak(maxRetries?: number, retryWindowMs?: number): Promise<void>;
|
|
24
24
|
goToIdentity(sleepTimeout?: number): Promise<void>;
|
|
25
25
|
goToConsole(sleepTimeout?: number): Promise<void>;
|
|
26
26
|
}
|
|
@@ -134,12 +134,16 @@ class NavigationPage {
|
|
|
134
134
|
async goToOptimize(sleepTimeout) {
|
|
135
135
|
await this.goTo(OPTIMIZE_CONTEXT_PATH, this.optimizePageBanner, sleepTimeout);
|
|
136
136
|
}
|
|
137
|
-
async goToKeycloak(maxRetries = 5) {
|
|
137
|
+
async goToKeycloak(maxRetries = 5, retryWindowMs = 90000) {
|
|
138
138
|
const keycloakUrl = process.env.KEYCLOAK_URL
|
|
139
139
|
? `${process.env.KEYCLOAK_URL}/auth`
|
|
140
140
|
: '/auth';
|
|
141
|
+
const startTime = Date.now();
|
|
141
142
|
let lastError;
|
|
142
|
-
|
|
143
|
+
let attempt = 0;
|
|
144
|
+
// Fast failures such as NS_ERROR_CONNECTION_REFUSED would exhaust
|
|
145
|
+
// maxRetries in ~20s, so keep retrying until retryWindowMs has elapsed.
|
|
146
|
+
while (attempt < maxRetries || Date.now() - startTime < retryWindowMs) {
|
|
143
147
|
try {
|
|
144
148
|
await this.page.goto(keycloakUrl);
|
|
145
149
|
await (0, test_1.expect)(this.keyboardPageBanner).toBeVisible({ timeout: 60000 });
|
|
@@ -147,14 +151,20 @@ class NavigationPage {
|
|
|
147
151
|
}
|
|
148
152
|
catch (error) {
|
|
149
153
|
lastError = error;
|
|
150
|
-
|
|
151
|
-
|
|
154
|
+
attempt++;
|
|
155
|
+
const backoffMs = Math.min(5000 * attempt, 15000);
|
|
156
|
+
if (attempt < maxRetries ||
|
|
157
|
+
Date.now() - startTime + backoffMs < retryWindowMs) {
|
|
158
|
+
await (0, sleep_1.sleep)(backoffMs);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
break;
|
|
152
162
|
}
|
|
153
163
|
}
|
|
154
164
|
}
|
|
155
165
|
throw lastError instanceof Error
|
|
156
166
|
? lastError
|
|
157
|
-
: new Error(`goToKeycloak failed after ${
|
|
167
|
+
: new Error(`goToKeycloak failed after ${attempt} attempts`);
|
|
158
168
|
}
|
|
159
169
|
async goToIdentity(sleepTimeout) {
|
|
160
170
|
await this.goTo('/identity', this.identityPageBanner, sleepTimeout);
|
package/dist/utils/apiHelpers.js
CHANGED
|
@@ -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
|
-
|
|
277
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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,
|
|
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}`);
|