@camunda/e2e-test-suite 0.0.943 → 0.0.945
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.10/AppsPage.d.ts +5 -0
- package/dist/pages/8.10/AppsPage.js +152 -0
- package/dist/pages/8.10/ModelerHomePage.d.ts +1 -0
- package/dist/pages/8.10/ModelerHomePage.js +52 -5
- package/dist/pages/8.10/OperateHomePage.js +23 -2
- package/dist/pages/8.10/OperateProcessInstancePage.d.ts +17 -3
- package/dist/pages/8.10/OperateProcessInstancePage.js +718 -16
- package/dist/pages/8.10/OperateProcessesPage.d.ts +1 -0
- package/dist/pages/8.10/OperateProcessesPage.js +38 -2
- package/dist/pages/8.10/OptimizeHomePage.d.ts +5 -0
- package/dist/pages/8.10/OptimizeHomePage.js +138 -0
- package/dist/pages/8.10/OptimizeReportPage.d.ts +1 -0
- package/dist/pages/8.10/OptimizeReportPage.js +25 -0
- package/dist/pages/8.10/TaskPanelPage.d.ts +2 -1
- package/dist/pages/8.10/TaskPanelPage.js +37 -9
- package/dist/pages/8.10/UtilitiesPage.d.ts +2 -1
- package/dist/pages/8.10/UtilitiesPage.js +155 -23
- package/dist/pages/SM-8.10/KeycloakAdminPage.js +1 -1
- package/dist/pages/SM-8.10/KeycloakLoginPage.d.ts +1 -0
- package/dist/pages/SM-8.10/KeycloakLoginPage.js +8 -1
- package/dist/pages/SM-8.10/LoginPage.d.ts +1 -1
- package/dist/pages/SM-8.10/LoginPage.js +11 -2
- package/dist/pages/SM-8.10/NavigationPage.d.ts +1 -0
- package/dist/pages/SM-8.10/NavigationPage.js +86 -8
- package/dist/pages/SM-8.10/OCIdentityHomePage.js +12 -2
- package/dist/pages/SM-8.10/OCIdentityRolesPage.d.ts +3 -0
- package/dist/pages/SM-8.10/OCIdentityRolesPage.js +110 -4
- package/dist/pages/SM-8.10/OperateHomePage.js +13 -1
- package/dist/pages/SM-8.10/OperateProcessesPage.d.ts +1 -0
- package/dist/pages/SM-8.10/OperateProcessesPage.js +37 -18
- package/dist/pages/SM-8.10/OptimizeReportPage.js +21 -1
- package/dist/pages/SM-8.10/TaskDetailsPage.js +7 -0
- package/dist/pages/SM-8.10/TaskPanelPage.js +21 -1
- package/dist/pages/SM-8.10/TaskProcessesPage.d.ts +1 -0
- package/dist/pages/SM-8.10/TaskProcessesPage.js +30 -6
- package/dist/pages/SM-8.10/UtilitiesPage.js +15 -4
- package/dist/pages/SM-8.10/optimizeReportUtils.js +30 -4
- package/dist/tests/8.10/smoke-tests.spec.js +182 -44
- package/dist/tests/SM-8.10/identity-user-flows.spec.js +2 -1
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.js +4 -0
- package/package.json +1 -1
|
@@ -1,15 +1,156 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.OperateProcessInstancePage = void 0;
|
|
3
|
+
exports.watchOperateInstanceUrl = exports.OperateProcessInstancePage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
|
+
// Operate bounces a deep link to an instance it has not imported yet: when
|
|
7
|
+
// `GET /v2/process-instances/{key}` answers 404 it shows an
|
|
8
|
+
// "Instance <id> could not be found" notification and replaces the URL with
|
|
9
|
+
// the processes list (see `useEffect` on `error?.response?.status === 404` in
|
|
10
|
+
// camunda/camunda `operate/client/src/App/ProcessInstance/index.tsx`). From
|
|
11
|
+
// that moment on the tab sits on `/processes?active=true&incidents=true`, so
|
|
12
|
+
// every `page.reload()` in a retry loop reloads the LIST, and neither the
|
|
13
|
+
// instance header nor the "Instance History" panel can ever appear no matter
|
|
14
|
+
// how long we keep retrying. Remember the deep-link URL and re-navigate to it
|
|
15
|
+
// between attempts so each retry re-asks for the instance.
|
|
16
|
+
const INSTANCE_PATH = /\/processes\/[^/]+/;
|
|
17
|
+
// URLs the tab shows while the cluster SSO hand-off is still in flight. The
|
|
18
|
+
// Operate popup is opened without a cluster session, so its first navigation
|
|
19
|
+
// goes through that hand-off and `/<cluster>/sso-callback?...&code=...&state=...`
|
|
20
|
+
// commits before the app itself does.
|
|
21
|
+
//
|
|
22
|
+
// The callback is only the LAST leg. Before it the tab visits the IdP
|
|
23
|
+
// (`weblogin.<domain>/authorize?...&state=...`, plus its `/u/login/...` forms
|
|
24
|
+
// when the session has to be re-established) and the cluster proxy's own
|
|
25
|
+
// `/<cluster>/sso` entry point. Recognising only the callback classified every
|
|
26
|
+
// one of those legs as "Operate bounced us to the processes list", so the
|
|
27
|
+
// bounce recovery re-opened the deep link ~5s into a hand-off that was still
|
|
28
|
+
// at the IdP -- aborting an authorization request that was about to come back
|
|
29
|
+
// and leaving the cluster proxy holding a `state`/`code` pair that no longer
|
|
30
|
+
// matches the tab. Every subsequent round did the same, which is how a wait
|
|
31
|
+
// ended its whole 480000ms budget parked on
|
|
32
|
+
// `/<cluster>/sso-callback?uuid=...&code=...&state=...` (27 of the 29 recorded
|
|
33
|
+
// timeouts of this wait).
|
|
34
|
+
const AUTH_TRANSIT_PATH = /\/(sso|sso-callback|login|login-callback)\/?$/;
|
|
35
|
+
const AUTH_TRANSIT_HOST = /^(weblogin|login|auth)\./;
|
|
36
|
+
const IDP_FORM_PATH = /^\/(u|authorize|oauth)(\/|$)/;
|
|
37
|
+
// True while the tab is mid-authentication rather than on an Operate page.
|
|
38
|
+
// `waitForInstanceLocator` must not re-navigate over a hand-off that is still
|
|
39
|
+
// in flight: re-opening the deep link aborts the code-for-session exchange and
|
|
40
|
+
// starts a fresh one, so a hand-off that needs longer than the retry interval
|
|
41
|
+
// could never finish and the loop would re-trigger it until the whole budget is
|
|
42
|
+
// gone. It therefore gives each hand-off a grace window to settle — but only a
|
|
43
|
+
// bounded one, because an exchange the cluster has rejected never completes at
|
|
44
|
+
// all and waiting it out spends the entire budget on a tab that was never asked
|
|
45
|
+
// about the instance ("instance header not rendered,
|
|
46
|
+
// url=<cluster>/sso-callback?uuid=...&code=...&state=..." after 480000ms with
|
|
47
|
+
// the deep link captured).
|
|
48
|
+
function isAuthTransit(rawUrl) {
|
|
49
|
+
try {
|
|
50
|
+
const url = new URL(rawUrl);
|
|
51
|
+
if (AUTH_TRANSIT_HOST.test(url.hostname) ||
|
|
52
|
+
IDP_FORM_PATH.test(url.pathname) ||
|
|
53
|
+
AUTH_TRANSIT_PATH.test(url.pathname)) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
// The OAuth `state` parameter is carried by every leg of the hand-off and
|
|
57
|
+
// by nothing Operate itself serves (neither the instance deep link nor the
|
|
58
|
+
// `/processes?active=true&incidents=true` list it bounces to has one), so a
|
|
59
|
+
// URL carrying it is still in transit even when its path is not one of the
|
|
60
|
+
// shapes above. Requiring `code` as well missed the whole outbound half of
|
|
61
|
+
// the hand-off, where only `state` is present yet.
|
|
62
|
+
return url.searchParams.has('state');
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Not a parsable URL yet (e.g. the popup's initial about:blank).
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
// The cluster proxy appends a bare `?continue` marker to a deep link it served
|
|
70
|
+
// WITHOUT a session. A tab showing it is not waiting on the Zeebe -> Operate
|
|
71
|
+
// import at all — it is waiting on a hand-off that never happened, so plain
|
|
72
|
+
// re-opens of the same URL can only bring the same marker back. That is what
|
|
73
|
+
// reported "instance header not rendered,
|
|
74
|
+
// url=<cluster>/operate/processes/<key>?continue" after a full 480000ms budget.
|
|
75
|
+
function isSessionlessDeepLink(rawUrl) {
|
|
76
|
+
try {
|
|
77
|
+
return new URL(rawUrl).searchParams.has('continue');
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// Not a parsable URL yet (e.g. the popup's initial about:blank).
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Returns the instance deep link out of a document request's redirect chain, or
|
|
85
|
+
// undefined when no hop of the chain points at an instance. Oldest hop first:
|
|
86
|
+
// prefer the URL the tab was opened with over whatever a redirect replaced it
|
|
87
|
+
// by.
|
|
88
|
+
function instanceUrlFromRequest(request) {
|
|
89
|
+
const chain = [];
|
|
90
|
+
for (let hop = request; hop !== null;) {
|
|
91
|
+
chain.push(hop.url());
|
|
92
|
+
hop = hop.redirectedFrom();
|
|
93
|
+
}
|
|
94
|
+
chain.reverse();
|
|
95
|
+
for (const url of chain) {
|
|
96
|
+
try {
|
|
97
|
+
if (INSTANCE_PATH.test(new URL(url).pathname)) {
|
|
98
|
+
return url;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Not a parsable URL yet (e.g. the popup's initial about:blank).
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
// Starts latching Operate instance deep links across the whole browser context.
|
|
108
|
+
// Call this BEFORE the action that opens the Operate popup, and pass the result
|
|
109
|
+
// to `rememberInstanceUrl`.
|
|
110
|
+
//
|
|
111
|
+
// The popup's own events are always too late for its FIRST document request:
|
|
112
|
+
// Playwright emits `popup` only "when the network request to the initial url is
|
|
113
|
+
// done and its response has started loading", and its docs say outright that to
|
|
114
|
+
// listen to that request you must use `browserContext.on('request')` instead of
|
|
115
|
+
// the equivalent method on the Page. `rememberInstanceUrl` attaches its
|
|
116
|
+
// listeners after `waitForEvent('popup')` has resolved, so the deep-link request
|
|
117
|
+
// has already been and gone by then.
|
|
118
|
+
//
|
|
119
|
+
// That only matters because Modeler opens Operate in a fresh tab: while that tab
|
|
120
|
+
// still has no cluster session the auth redirect chain answers the deep link and
|
|
121
|
+
// drops its path, so the only navigation that ever COMMITS is the Operate app
|
|
122
|
+
// root and `framenavigated`/`waitForURL` never see an instance URL either. The
|
|
123
|
+
// deep link is then lost altogether ("instance deep link never captured"), which
|
|
124
|
+
// silently disables the recovery in `waitForInstanceLocator`: `hasBouncedToList`
|
|
125
|
+
// can no longer tell we are off the instance page and `reload` degrades to
|
|
126
|
+
// reloading the app root, where the instance header can never render no matter
|
|
127
|
+
// how long we poll. That is what spent the whole budget as "active/completed
|
|
128
|
+
// icon in Operate timed out after 480000ms (instance header not rendered,
|
|
129
|
+
// url=<cluster>/operate, instance deep link never captured)".
|
|
130
|
+
//
|
|
131
|
+
// The context-level `request` event is already live when the popup is created,
|
|
132
|
+
// so it observes the deep link at its origin. By the time the loop retries, the
|
|
133
|
+
// tab is authenticated and re-opening that URL lands on the instance page.
|
|
134
|
+
function watchOperateInstanceUrl(context) {
|
|
135
|
+
let instanceUrl;
|
|
136
|
+
context.on('request', (request) => {
|
|
137
|
+
if (instanceUrl !== undefined || request.resourceType() !== 'document') {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
instanceUrl = instanceUrlFromRequest(request);
|
|
141
|
+
});
|
|
142
|
+
return () => instanceUrl;
|
|
143
|
+
}
|
|
144
|
+
exports.watchOperateInstanceUrl = watchOperateInstanceUrl;
|
|
6
145
|
class OperateProcessInstancePage {
|
|
7
146
|
page;
|
|
147
|
+
instanceUrl;
|
|
8
148
|
diagram;
|
|
9
149
|
completedIcon;
|
|
10
150
|
diagramSpinner;
|
|
11
151
|
activeIcon;
|
|
12
152
|
incidentIcon;
|
|
153
|
+
incidentsBanner;
|
|
13
154
|
userTaskOpenTasklistLink;
|
|
14
155
|
taskDetailsPopOver;
|
|
15
156
|
connectorResultVariableName;
|
|
@@ -30,6 +171,9 @@ class OperateProcessInstancePage {
|
|
|
30
171
|
this.incidentIcon = page
|
|
31
172
|
.getByTestId('instance-header')
|
|
32
173
|
.getByTestId('INCIDENT-icon');
|
|
174
|
+
// Same test id Operate's own e2e suite uses for the incident bar
|
|
175
|
+
// (camunda/camunda `operate/client/e2e-playwright/pages/ProcessInstance.ts`).
|
|
176
|
+
this.incidentsBanner = page.getByTestId('incidents-banner');
|
|
33
177
|
this.userTaskOpenTasklistLink = page.getByRole('link', {
|
|
34
178
|
name: 'Open Tasklist',
|
|
35
179
|
});
|
|
@@ -45,33 +189,591 @@ class OperateProcessInstancePage {
|
|
|
45
189
|
async connectorResultVariableValue(variableName) {
|
|
46
190
|
return await this.page.getByTestId(variableName).locator('td').last();
|
|
47
191
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
192
|
+
// Records the instance deep link so the waits below can re-open it after
|
|
193
|
+
// Operate's 404 bounce to the processes list (see the note above the class).
|
|
194
|
+
// Called right after the Operate popup opens, before the query can fail.
|
|
195
|
+
//
|
|
196
|
+
// `waitForURL` alone loses this race: the bounce is a client-side navigation
|
|
197
|
+
// fired as soon as the 404 lands, so on a lagging import the deep link can be
|
|
198
|
+
// gone before the wait is evaluated. `waitForURL` then sits out its whole
|
|
199
|
+
// budget on the list page and leaves `instanceUrl` unset -- which silently
|
|
200
|
+
// disables the recovery: `hasBouncedToList` can no longer tell we bounced and
|
|
201
|
+
// `reload` degrades to reloading the LIST page, so every retry afterwards
|
|
202
|
+
// polls a page that can never render the instance header. That is what turns
|
|
203
|
+
// a slow import into "failed after N attempts". Latch the first
|
|
204
|
+
// instance-shaped URL from `framenavigated` instead, which fires on the deep
|
|
205
|
+
// link's own commit and therefore cannot be beaten by the bounce.
|
|
206
|
+
//
|
|
207
|
+
// `watchedUrl` is the getter returned by `watchOperateInstanceUrl`, primed on
|
|
208
|
+
// the browser context before the popup was opened. It is the ONLY observer
|
|
209
|
+
// that is already live when the popup issues its first document request, so
|
|
210
|
+
// for a Modeler-opened Operate tab it is the source that actually works; the
|
|
211
|
+
// listeners below stay as the in-tab path (a navigation that commits later,
|
|
212
|
+
// or a tab that is already on the instance page).
|
|
213
|
+
async rememberInstanceUrl(timeout = 60000, watchedUrl) {
|
|
214
|
+
const latch = (url) => {
|
|
215
|
+
if (this.instanceUrl !== undefined) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
57
218
|
try {
|
|
58
|
-
|
|
219
|
+
if (INSTANCE_PATH.test(new URL(url).pathname)) {
|
|
220
|
+
this.instanceUrl = url;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
// Not a parsable URL yet (e.g. the popup's initial about:blank).
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
// Latch what the context-level watcher already saw. This runs FIRST because
|
|
228
|
+
// for a Modeler-opened tab it is the only source that can have the deep
|
|
229
|
+
// link: `framenavigated` only ever reports a *committed* document, and the
|
|
230
|
+
// auth redirect chain drops the deep-link path before it commits (see
|
|
231
|
+
// `watchOperateInstanceUrl`), so neither the listeners below nor
|
|
232
|
+
// `waitForURL` ever see an instance URL in that tab.
|
|
233
|
+
const latchWatched = () => {
|
|
234
|
+
const seen = watchedUrl?.();
|
|
235
|
+
if (seen !== undefined) {
|
|
236
|
+
latch(seen);
|
|
237
|
+
}
|
|
238
|
+
return this.instanceUrl !== undefined;
|
|
239
|
+
};
|
|
240
|
+
if (latchWatched()) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
// In-tab sources, for a navigation that commits later or a tab that is
|
|
244
|
+
// already on the instance page (e.g. after clicking through the processes
|
|
245
|
+
// list rather than opening a popup).
|
|
246
|
+
this.page.on('request', (request) => {
|
|
247
|
+
if (request.resourceType() !== 'document' ||
|
|
248
|
+
request.frame() !== this.page.mainFrame()) {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const fromRequest = instanceUrlFromRequest(request);
|
|
252
|
+
if (fromRequest !== undefined) {
|
|
253
|
+
latch(fromRequest);
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
this.page.on('framenavigated', (frame) => {
|
|
257
|
+
if (frame === this.page.mainFrame()) {
|
|
258
|
+
latch(frame.url());
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
latch(this.page.url());
|
|
262
|
+
if (this.instanceUrl !== undefined) {
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
await this.page.waitForURL((url) => INSTANCE_PATH.test(url.pathname), {
|
|
267
|
+
timeout,
|
|
268
|
+
});
|
|
269
|
+
latch(this.page.url());
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
if (!latchWatched()) {
|
|
273
|
+
// The tab never showed an instance URL (already bounced, or still on
|
|
274
|
+
// the auth redirect). Retries then behave as before rather than
|
|
275
|
+
// failing here.
|
|
276
|
+
console.warn(`Could not capture the Operate instance URL: ${error}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
// True once Operate has replaced the deep link with the processes list.
|
|
281
|
+
// A tab that is still authenticating is NOT a bounce: it is on its way to the
|
|
282
|
+
// deep link, and re-opening it would restart the hand-off (see
|
|
283
|
+
// `isAuthTransit`).
|
|
284
|
+
hasBouncedToList() {
|
|
285
|
+
if (this.instanceUrl === undefined) {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
if (isAuthTransit(this.page.url())) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
return !INSTANCE_PATH.test(new URL(this.page.url()).pathname);
|
|
292
|
+
}
|
|
293
|
+
// Lets the cluster SSO hand-off finish instead of re-navigating over it.
|
|
294
|
+
// Returns when the tab has left the auth URLs or when this slice runs out —
|
|
295
|
+
// the caller loops either way, so a hand-off that stalls for the whole
|
|
296
|
+
// budget still surfaces through `describeRenderedState`.
|
|
297
|
+
async waitForAuthToSettle(timeout) {
|
|
298
|
+
if (timeout <= 0) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
try {
|
|
302
|
+
await this.page.waitForURL((url) => !isAuthTransit(url.toString()), {
|
|
303
|
+
timeout,
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Still authenticating — handled by the caller's loop.
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
// True when the awaited locator is one of the mutually exclusive instance
|
|
311
|
+
// state icons, which an incident makes unreachable. Everything else on the
|
|
312
|
+
// instance page (the "Instance History" panel, the variables list) renders
|
|
313
|
+
// regardless of the instance state, so those waits must NOT be cut short.
|
|
314
|
+
// Identity comparison against the locators this class exposes: a caller that
|
|
315
|
+
// builds its own equivalent locator simply keeps the plain timeout behaviour.
|
|
316
|
+
isStateIconWait(locator) {
|
|
317
|
+
return locator === this.activeIcon || locator === this.completedIcon;
|
|
318
|
+
}
|
|
319
|
+
// Reports the incident bar's text, so a terminal incident names itself
|
|
320
|
+
// instead of being reported as an import that never caught up.
|
|
321
|
+
async describeIncident() {
|
|
322
|
+
const banner = await this.incidentsBanner
|
|
323
|
+
.textContent({ timeout: 5000 })
|
|
324
|
+
.catch(() => null);
|
|
325
|
+
return banner === null || banner.trim() === ''
|
|
326
|
+
? 'the instance header shows INCIDENT'
|
|
327
|
+
: `the instance raised an incident: ${banner.trim()}`;
|
|
328
|
+
}
|
|
329
|
+
// Reports what the instance-detail page actually rendered, so a timeout below
|
|
330
|
+
// distinguishes "the import never caught up" (no header at all, or the tab is
|
|
331
|
+
// parked on the processes list) from "the instance reached a different state"
|
|
332
|
+
// (e.g. the header shows INCIDENT while we wait for ACTIVE) -- the plain
|
|
333
|
+
// "element(s) not found" of the underlying assertion cannot tell those apart.
|
|
334
|
+
async describeRenderedState() {
|
|
335
|
+
const header = this.page.getByTestId('instance-header');
|
|
336
|
+
if (!(await header.isVisible().catch(() => false))) {
|
|
337
|
+
// Report whether the deep link was captured: without it the retries could
|
|
338
|
+
// only reload whatever page the tab was left on, which is a different
|
|
339
|
+
// failure from an import that never caught up.
|
|
340
|
+
const link = this.instanceUrl === undefined
|
|
341
|
+
? 'instance deep link never captured'
|
|
342
|
+
: `instance deep link=${this.instanceUrl}`;
|
|
343
|
+
return `instance header not rendered, url=${this.page.url()}, ${link}`;
|
|
344
|
+
}
|
|
345
|
+
const states = ['ACTIVE', 'COMPLETED', 'INCIDENT', 'CANCELED'];
|
|
346
|
+
const rendered = [];
|
|
347
|
+
for (const state of states) {
|
|
348
|
+
const visible = await header
|
|
349
|
+
.getByTestId(`${state}-icon`)
|
|
350
|
+
.isVisible()
|
|
351
|
+
.catch(() => false);
|
|
352
|
+
if (visible) {
|
|
353
|
+
rendered.push(state);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return rendered.length > 0
|
|
357
|
+
? `instance header shows ${rendered.join(', ')}`
|
|
358
|
+
: 'instance header rendered without a state icon';
|
|
359
|
+
}
|
|
360
|
+
// Waits for a locator on the instance-detail page, re-opening the deep link
|
|
361
|
+
// whenever Operate has bounced the tab to the processes list. Waiting a full
|
|
362
|
+
// per-attempt budget on the bounced list page can never succeed, so the time
|
|
363
|
+
// is spent re-asking Operate for the instance instead: the instance header
|
|
364
|
+
// and the "Instance History" panel only render once the Zeebe -> Operate
|
|
365
|
+
// import has caught up.
|
|
366
|
+
//
|
|
367
|
+
// The locator is checked in short slices so the bounce is noticed within
|
|
368
|
+
// seconds. Checking it once per `pollTimeout` meant the bounce -- which lands
|
|
369
|
+
// about a second after each re-navigation while the import lags -- was only
|
|
370
|
+
// seen at the END of that wait, so ~29 of every 30 seconds were spent idling
|
|
371
|
+
// on the list page and the whole 8 minute budget bought only ~16 re-asks.
|
|
372
|
+
// That is what ran out as "active/completed icon in Operate timed out after
|
|
373
|
+
// 480000ms" on a slow import. Same budget, but a bouncing page now re-asks
|
|
374
|
+
// every few seconds, while a page that stays on the instance keeps the
|
|
375
|
+
// original one-reload-per-`pollTimeout` cadence.
|
|
376
|
+
async waitForInstanceLocator(locator, description, totalTimeout = 300000, pollTimeout = 30000) {
|
|
377
|
+
const deadline = Date.now() + totalTimeout;
|
|
378
|
+
// Short enough that a bounce costs seconds rather than a full poll window,
|
|
379
|
+
// long enough not to fill the trace with hundreds of failed assertions.
|
|
380
|
+
const checkSlice = 5000;
|
|
381
|
+
// How long the FIRST cluster SSO hand-off is given to complete before it is
|
|
382
|
+
// treated as stalled. A healthy hand-off commits in a couple of seconds, so
|
|
383
|
+
// this is generous; it only has to be long enough that a merely slow
|
|
384
|
+
// exchange is never restarted underneath itself.
|
|
385
|
+
const authGrace = 60000;
|
|
386
|
+
// Every hand-off after that gets a much shorter window: once one has
|
|
387
|
+
// already stalled, the tab has proved this exchange does not just need a
|
|
388
|
+
// little longer, and repeating the generous wait is what limited the whole
|
|
389
|
+
// budget to ~7 restarts (60s idle + 5s backoff each).
|
|
390
|
+
const authRetryGrace = 15000;
|
|
391
|
+
let lastError;
|
|
392
|
+
let nextReload = Date.now() + pollTimeout;
|
|
393
|
+
// When the tab entered its current auth hand-off, or undefined while it is
|
|
394
|
+
// not on an auth URL.
|
|
395
|
+
let authTransitSince;
|
|
396
|
+
// How many hand-offs have already been restarted, and whether the last
|
|
397
|
+
// restart's navigation actually went through.
|
|
398
|
+
let authStalls = 0;
|
|
399
|
+
let authRestartFailed = false;
|
|
400
|
+
// Consecutive re-opens made while the instance header had not rendered at
|
|
401
|
+
// all. A tab that came back from a failed hand-off lands on the deep link
|
|
402
|
+
// itself carrying the cluster proxy's session-less `?continue` marker --
|
|
403
|
+
// neither an auth URL nor the processes list, so both recoveries above see
|
|
404
|
+
// nothing wrong and the loop only re-opens the same URL. Every round landed
|
|
405
|
+
// on the same session-less page, which is what reported "instance header
|
|
406
|
+
// not rendered, url=<cluster>/operate/processes/<key>?continue" after the
|
|
407
|
+
// full budget. Once a few re-opens have brought back no header at all, stop
|
|
408
|
+
// re-asking the cluster directly and re-enter through the Console.
|
|
409
|
+
let headerlessReopens = 0;
|
|
410
|
+
while (Date.now() < deadline) {
|
|
411
|
+
// Don't re-navigate over an in-flight cluster SSO hand-off: doing so
|
|
412
|
+
// aborts the code-for-session exchange and starts a fresh one, so a
|
|
413
|
+
// hand-off that needs longer than the retry interval could never finish
|
|
414
|
+
// (see `isAuthTransit`). But don't wait on it indefinitely either -- an
|
|
415
|
+
// exchange whose code was rejected never turns into a session, and the
|
|
416
|
+
// tab then sits on `/<cluster>/sso-callback?...&code=...&state=...`
|
|
417
|
+
// forever. Waiting that out consumed the ENTIRE budget without ever
|
|
418
|
+
// re-asking Operate anything, which is what reported "active icon in
|
|
419
|
+
// Operate timed out after 480000ms (instance header not rendered,
|
|
420
|
+
// url=<cluster>/sso-callback?uuid=...&code=...&state=...)" with the deep
|
|
421
|
+
// link captured. So: give each hand-off `authGrace` to settle, then
|
|
422
|
+
// re-open the deep link to start a genuinely fresh one.
|
|
423
|
+
if (isAuthTransit(this.page.url())) {
|
|
424
|
+
if (authTransitSince === undefined) {
|
|
425
|
+
authTransitSince = Date.now();
|
|
426
|
+
}
|
|
427
|
+
const grace = authStalls === 0 ? authGrace : authRetryGrace;
|
|
428
|
+
// A restart whose navigation never happened left the tab exactly where
|
|
429
|
+
// it was, so there is no fresh exchange to wait on: retry the
|
|
430
|
+
// navigation instead of idling another window on the stalled one.
|
|
431
|
+
const graceLeft = authRestartFailed
|
|
432
|
+
? 0
|
|
433
|
+
: grace - (Date.now() - authTransitSince);
|
|
434
|
+
if (graceLeft > 0) {
|
|
435
|
+
await this.waitForAuthToSettle(Math.min(graceLeft, Math.max(0, deadline - Date.now())));
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
// Back off before re-opening so a cluster that is throttling or
|
|
439
|
+
// rejecting the exchange gets room to recover instead of being hammered
|
|
440
|
+
// with a tight restart loop. It grows with each stall, because a
|
|
441
|
+
// hand-off that has failed several times in a row is not going to be
|
|
442
|
+
// fixed by asking again immediately.
|
|
443
|
+
const backoff = Math.min(30000, 5000 * 2 ** authStalls);
|
|
444
|
+
await (0, sleep_1.sleep)(Math.min(backoff, Math.max(0, deadline - Date.now())));
|
|
445
|
+
// Only a re-navigation that actually happened starts a new hand-off.
|
|
446
|
+
// `reload` reports a `goto` that aborted against the callback's own
|
|
447
|
+
// in-flight navigation instead of hiding it, so such a round no longer
|
|
448
|
+
// buys the untouched tab another full grace window.
|
|
449
|
+
//
|
|
450
|
+
// The first restart re-opens the deep link directly, which is enough
|
|
451
|
+
// for an exchange that was merely slow. Every restart after that goes
|
|
452
|
+
// through the Console root: a hand-off that has already failed once is
|
|
453
|
+
// not recovered by re-entering it from its own dead callback page (see
|
|
454
|
+
// `reenterViaConsole`), which is why ~10 direct restarts in a row still
|
|
455
|
+
// ended the budget parked on `sso-callback`.
|
|
456
|
+
authRestartFailed = !(await (authStalls === 0
|
|
457
|
+
? this.reload()
|
|
458
|
+
: this.reenterViaConsole(deadline)));
|
|
459
|
+
authStalls++;
|
|
460
|
+
if (!authRestartFailed) {
|
|
461
|
+
authTransitSince = undefined;
|
|
462
|
+
}
|
|
463
|
+
nextReload = Date.now() + pollTimeout;
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
authTransitSince = undefined;
|
|
467
|
+
authRestartFailed = false;
|
|
468
|
+
// The tab has left the callback, so the exchange that was in flight
|
|
469
|
+
// COMPLETED. `authStalls` shortens the grace window because "this
|
|
470
|
+
// exchange has proved it does not just need a little longer" -- that is
|
|
471
|
+
// a statement about one exchange, so it must count CONSECUTIVE stalls.
|
|
472
|
+
// Latching it for the whole wait meant that after a single early stall
|
|
473
|
+
// every later hand-off -- the ones the ordinary bounce recovery and the
|
|
474
|
+
// periodic re-opens below start, and the fresh one a Console re-entry
|
|
475
|
+
// mints -- was capped at `authRetryGrace` and torn down 15s in, then
|
|
476
|
+
// charged a longer backoff and another full Console re-entry. The loop
|
|
477
|
+
// thrashed instead of converging, which is the other half of how a wait
|
|
478
|
+
// ended its budget parked on
|
|
479
|
+
// `/<cluster>/sso-callback?...&code=...&state=...`.
|
|
480
|
+
authStalls = 0;
|
|
481
|
+
if (this.hasBouncedToList()) {
|
|
482
|
+
// Back off before re-opening so a still-importing instance is not
|
|
483
|
+
// hammered with a tight bounce/re-open loop.
|
|
484
|
+
await (0, sleep_1.sleep)(Math.min(5000, Math.max(0, deadline - Date.now())));
|
|
485
|
+
await this.reload();
|
|
486
|
+
nextReload = Date.now() + pollTimeout;
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
const remaining = deadline - Date.now();
|
|
490
|
+
if (remaining <= 0) {
|
|
59
491
|
break;
|
|
60
492
|
}
|
|
493
|
+
try {
|
|
494
|
+
await (0, test_1.expect)(locator).toBeVisible({
|
|
495
|
+
timeout: Math.min(checkSlice, remaining),
|
|
496
|
+
});
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
61
499
|
catch (error) {
|
|
62
|
-
|
|
63
|
-
|
|
500
|
+
lastError = error;
|
|
501
|
+
}
|
|
502
|
+
// An incident ends this wait: the header renders exactly one state icon,
|
|
503
|
+
// so neither ACTIVE nor COMPLETED can appear while the instance sits in
|
|
504
|
+
// an incident, and nothing in these flows resolves one. Waiting out the
|
|
505
|
+
// rest of the budget only delays the failure by minutes and then reports
|
|
506
|
+
// it as an import lag ("timed out after 480000ms (instance header shows
|
|
507
|
+
// INCIDENT)"), which hides the cause. Confirm it once after a short pause
|
|
508
|
+
// first, so a header rendered from partially imported records is not
|
|
509
|
+
// mistaken for a settled incident.
|
|
510
|
+
if (this.isStateIconWait(locator) &&
|
|
511
|
+
(await this.incidentIcon.isVisible().catch(() => false))) {
|
|
512
|
+
await (0, sleep_1.sleep)(Math.min(10000, Math.max(0, deadline - Date.now())));
|
|
513
|
+
if (await this.incidentIcon.isVisible().catch(() => false)) {
|
|
514
|
+
throw new Error(`Assertion of ${description} in Operate cannot succeed: ` +
|
|
515
|
+
`${await this.describeIncident()}. The awaited state is no ` +
|
|
516
|
+
`longer reachable, so failing now instead of waiting out the ` +
|
|
517
|
+
`remaining budget of ${totalTimeout}ms.`);
|
|
64
518
|
}
|
|
65
|
-
|
|
66
|
-
|
|
519
|
+
}
|
|
520
|
+
if (Date.now() >= nextReload && Date.now() < deadline) {
|
|
521
|
+
// A rendered header means the app booted and we are only waiting on the
|
|
522
|
+
// instance state, so keep re-opening the deep link directly. No header
|
|
523
|
+
// at all points at the app never having booted -- escalate to the
|
|
524
|
+
// Console re-entry once a couple of plain re-opens have not changed it.
|
|
525
|
+
const headerRendered = await this.page
|
|
526
|
+
.getByTestId('instance-header')
|
|
527
|
+
.isVisible()
|
|
528
|
+
.catch(() => false);
|
|
529
|
+
headerlessReopens = headerRendered ? 0 : headerlessReopens + 1;
|
|
530
|
+
// A session-less deep link escalates straight away: the `?continue`
|
|
531
|
+
// marker is proof the cluster hand-off never happened, so the two
|
|
532
|
+
// plain re-opens the counter would otherwise spend first can only
|
|
533
|
+
// bring the same marker back (see `isSessionlessDeepLink`).
|
|
534
|
+
const sessionless = !headerRendered && isSessionlessDeepLink(this.page.url());
|
|
535
|
+
if (headerlessReopens > 2 || sessionless) {
|
|
536
|
+
await this.reenterViaConsole(deadline);
|
|
537
|
+
}
|
|
538
|
+
else {
|
|
539
|
+
await this.reload();
|
|
540
|
+
}
|
|
541
|
+
nextReload = Date.now() + pollTimeout;
|
|
67
542
|
}
|
|
68
543
|
}
|
|
544
|
+
throw new Error(`Assertion of ${description} in Operate timed out after ` +
|
|
545
|
+
`${totalTimeout}ms (${await this.describeRenderedState()}): ` +
|
|
546
|
+
`${lastError}`);
|
|
547
|
+
}
|
|
548
|
+
// Dismisses Operate's "what's new" popup without re-gating on the import.
|
|
549
|
+
// Use this when the caller has already waited for something on the instance
|
|
550
|
+
// page (the instance header, the COMPLETED icon): that wait is a strictly
|
|
551
|
+
// stronger gate than "Instance History" is visible, so repeating it here
|
|
552
|
+
// just spends a second full budget proving what is already known -- and on
|
|
553
|
+
// a lagging import it is the duplicate gate, not the real assertion, that
|
|
554
|
+
// reports the failure. `closePopOverIfVisible` keeps the combined behaviour
|
|
555
|
+
// for callers that have not waited for the instance page yet.
|
|
556
|
+
async dismissWhatsNewIfVisible() {
|
|
69
557
|
if (await this.whatsNewPopUp.isVisible()) {
|
|
70
558
|
await this.gotItButton.click();
|
|
71
559
|
}
|
|
72
560
|
}
|
|
561
|
+
async closePopOverIfVisible() {
|
|
562
|
+
// The instance-detail page is opened via a deep link right after the
|
|
563
|
+
// instance is started, so on a busy SaaS cluster the instance is often not
|
|
564
|
+
// imported yet when the popup lands and Operate bounces the tab to the
|
|
565
|
+
// processes list. Poll the deep link rather than the bounced page; the
|
|
566
|
+
// 8 minute budget matches the previous 8 x 60s attempts.
|
|
567
|
+
await this.waitForInstanceLocator(this.page.getByText('Instance History').first(), 'Instance History panel', 480000);
|
|
568
|
+
if (await this.whatsNewPopUp.isVisible()) {
|
|
569
|
+
await this.gotItButton.click();
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
// The host that serves the instance deep link, i.e. the origin whose cluster
|
|
573
|
+
// session the Operate app needs. Undefined only before any cluster URL has
|
|
574
|
+
// been seen at all.
|
|
575
|
+
clusterHost() {
|
|
576
|
+
for (const raw of [this.instanceUrl, this.page.url()]) {
|
|
577
|
+
if (raw === undefined) {
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
try {
|
|
581
|
+
return new URL(raw).hostname;
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// Not a parsable URL yet (e.g. the popup's initial about:blank).
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return undefined;
|
|
588
|
+
}
|
|
589
|
+
// Re-opens the instance by way of the Console's apps menu instead of going
|
|
590
|
+
// straight back at the cluster deep link.
|
|
591
|
+
//
|
|
592
|
+
// Once a tab's cluster SSO hand-off has actually failed, re-opening the deep
|
|
593
|
+
// link cannot recover it: that starts the exchange again from the dead
|
|
594
|
+
// callback page, whose `code` is single use, so the tab just lands back on
|
|
595
|
+
// `/<cluster>/sso-callback?...&code=...&state=...` (or on the deep link
|
|
596
|
+
// itself carrying the proxy's session-less `?continue` marker) and the
|
|
597
|
+
// Operate app never boots. Repeating it only buys more identical rounds --
|
|
598
|
+
// that is what spent whole 480000ms budgets as "instance header not
|
|
599
|
+
// rendered" with the deep link captured.
|
|
600
|
+
//
|
|
601
|
+
// The Console is the one origin whose session is known good in these flows
|
|
602
|
+
// (the test logged in there), so letting IT drive a fresh hand-off -- by
|
|
603
|
+
// clicking Operate in its apps menu -- hands the cluster a real session
|
|
604
|
+
// rather than chaining off the failed callback. Merely LOADING the Console
|
|
605
|
+
// root is not enough: that establishes a Console session only, and the
|
|
606
|
+
// deep-link re-open then puts the cluster proxy back in charge of the very
|
|
607
|
+
// exchange that is wedged (see the click below). This is the same recovery
|
|
608
|
+
// `AppsPage.openOperateWithRetry` / `openModelerWithRetry` already use for
|
|
609
|
+
// the identical Operate and Modeler `login-callback` stalls.
|
|
610
|
+
async reenterViaConsole(deadline) {
|
|
611
|
+
// Clamp every wait below to the time the caller's budget actually has
|
|
612
|
+
// left, so one escalation can never overrun the loop's own deadline (and
|
|
613
|
+
// through it the flow's reserve for the afterEach screenshot). Never 0 —
|
|
614
|
+
// Playwright reads that as "no timeout".
|
|
615
|
+
const left = (want) => deadline === undefined
|
|
616
|
+
? want
|
|
617
|
+
: Math.max(1000, Math.min(want, deadline - Date.now()));
|
|
618
|
+
let booted = false;
|
|
619
|
+
try {
|
|
620
|
+
// Drop the CLUSTER origin's own cookies before re-entering. A hand-off
|
|
621
|
+
// that parked on `/<cluster>/sso-callback?...&code=...&state=...` left
|
|
622
|
+
// the cluster proxy holding that exchange's state/session cookie, and
|
|
623
|
+
// every restart re-sends it -- so the proxy keeps validating the fresh
|
|
624
|
+
// `code` against the dead flow and bounces the tab straight back to the
|
|
625
|
+
// callback. Neither recovery above touches those cookies (re-opening the
|
|
626
|
+
// deep link sends them again, and the Console root is a different
|
|
627
|
+
// origin), which is why ~8 restarts in a row all ended on that same URL
|
|
628
|
+
// and whole 480000ms budgets went by with the Operate app never booting.
|
|
629
|
+
// Scoped to the cluster host, so the Console and weblogin IdP sessions
|
|
630
|
+
// the test logged in with are untouched and no re-login is needed -- the
|
|
631
|
+
// same recovery `AppsPage.clickModeler` already uses for a wedged
|
|
632
|
+
// Modeler hand-off.
|
|
633
|
+
//
|
|
634
|
+
// `clearCookies` matches its `domain` filter by exact string equality,
|
|
635
|
+
// and Chromium reports a cookie that was set with an explicit `Domain=`
|
|
636
|
+
// attribute with a LEADING DOT. Filtering on the bare host therefore
|
|
637
|
+
// re-added every `.<host>` cookie -- the proxy's own session/CSRF pair,
|
|
638
|
+
// i.e. precisely the dead exchange's state this clear exists to drop --
|
|
639
|
+
// so each restart re-sent them and the proxy kept validating the fresh
|
|
640
|
+
// `code` against the wedged flow. Match both forms, still scoped to the
|
|
641
|
+
// cluster host itself so the Console and weblogin IdP sessions on the
|
|
642
|
+
// parent domain stay untouched and no re-login is needed.
|
|
643
|
+
const host = this.clusterHost();
|
|
644
|
+
if (host !== undefined) {
|
|
645
|
+
const escaped = host.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
646
|
+
await this.page
|
|
647
|
+
.context()
|
|
648
|
+
.clearCookies({ domain: new RegExp(`^\\.?${escaped}$`) });
|
|
649
|
+
}
|
|
650
|
+
await this.page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
651
|
+
// Wait for the Console APP, not just for its shell document.
|
|
652
|
+
// `domcontentloaded` resolves as soon as the root HTML is parsed, which
|
|
653
|
+
// is well before the Console has booted and run its own auth/token
|
|
654
|
+
// refresh. Gate on the app switcher -- the same marker the flows use to
|
|
655
|
+
// know the Console is ready -- so the re-entry below starts from a
|
|
656
|
+
// Console session that actually exists.
|
|
657
|
+
const appSwitcher = this.page
|
|
658
|
+
.getByLabel('Camunda components')
|
|
659
|
+
.or(this.page.getByLabel('Camunda apps'))
|
|
660
|
+
.or(this.page.getByLabel('App Switcher'))
|
|
661
|
+
.first();
|
|
662
|
+
await appSwitcher.waitFor({ state: 'visible', timeout: left(30000) });
|
|
663
|
+
// Now re-enter the CLUSTER through the apps menu, instead of going
|
|
664
|
+
// straight back at the deep link. Reaching the Console root only
|
|
665
|
+
// establishes a CONSOLE session; the CLUSTER session is minted by the
|
|
666
|
+
// hand-off, and re-requesting the deep link makes the cluster PROXY
|
|
667
|
+
// drive that hand-off -- which is precisely the exchange that is wedged
|
|
668
|
+
// (it is what emits `/<cluster>/sso-callback?...&code=...&state=...` and
|
|
669
|
+
// the session-less `?continue` marker). Without this click every
|
|
670
|
+
// "Console re-entry" degenerated into just another plain deep-link
|
|
671
|
+
// re-open, which is why ~8 of them in a row all ended on that same URL
|
|
672
|
+
// and whole 480000ms budgets went by with Operate never booting.
|
|
673
|
+
// Clicking Operate here makes the CONSOLE mint a fresh authorization
|
|
674
|
+
// request off its own known-good session -- the same hand-off
|
|
675
|
+
// `AppsPage.openOperateWithRetry` uses successfully elsewhere in these
|
|
676
|
+
// flows. The locators mirror `AppsPage.clickOperate`; they are inlined
|
|
677
|
+
// rather than imported because `AppsPage` -> `UtilitiesPage` imports
|
|
678
|
+
// this module back.
|
|
679
|
+
// Give the menu clicks the same tolerance `AppsPage` uses for the very
|
|
680
|
+
// same hand-off (30s), not the config-wide 10s `actionTimeout`: a Console
|
|
681
|
+
// that is still settling made this single click lapse at 10s and threw
|
|
682
|
+
// the WHOLE re-entry away -- 50 to 73 of the recorded re-entry failures
|
|
683
|
+
// in a nightly are exactly `locator.click: Timeout 10000ms exceeded`.
|
|
684
|
+
// Still clamped to the caller's budget through `left`.
|
|
685
|
+
await appSwitcher.click({ timeout: left(30000) });
|
|
686
|
+
const operateLink = this.page.getByRole('link', {
|
|
687
|
+
name: 'Operate',
|
|
688
|
+
exact: true,
|
|
689
|
+
});
|
|
690
|
+
const operateGroup = this.page.getByRole('button', {
|
|
691
|
+
name: 'Operate',
|
|
692
|
+
exact: true,
|
|
693
|
+
});
|
|
694
|
+
// The apps panel populates asynchronously after the switcher click, and
|
|
695
|
+
// `isVisible()` IGNORES its `timeout` option -- it reports the state as
|
|
696
|
+
// of right now and returns (see the `@deprecated This option is ignored`
|
|
697
|
+
// note on `locator.isVisible` in playwright-core's types). Probing
|
|
698
|
+
// immediately after the click therefore almost always answered "no
|
|
699
|
+
// link", fell through to the multi-cluster branch, and threw when that
|
|
700
|
+
// button was not rendered yet either -- so the whole re-entry was
|
|
701
|
+
// swallowed by the catch below and degenerated into exactly the plain
|
|
702
|
+
// deep-link re-open it exists to replace. That is why every "Console
|
|
703
|
+
// re-entry" left the tab back on
|
|
704
|
+
// `/<cluster>/sso-callback?...&code=...&state=...`. Wait for the panel
|
|
705
|
+
// to actually render first, the same way `AppsPage.clickTasklist` does.
|
|
706
|
+
await operateLink
|
|
707
|
+
.or(operateGroup)
|
|
708
|
+
.first()
|
|
709
|
+
.waitFor({ state: 'visible', timeout: left(30000) });
|
|
710
|
+
if (await operateLink.isVisible()) {
|
|
711
|
+
// Single cluster.
|
|
712
|
+
await operateLink.click({ timeout: left(30000) });
|
|
713
|
+
}
|
|
714
|
+
else {
|
|
715
|
+
// Multiple clusters: expand the Operate group, then pick the cluster
|
|
716
|
+
// serving this instance.
|
|
717
|
+
await operateGroup.click({ timeout: left(30000) });
|
|
718
|
+
await this.page
|
|
719
|
+
.getByRole('listitem')
|
|
720
|
+
.filter({ hasText: 'Operate' })
|
|
721
|
+
.getByRole('link')
|
|
722
|
+
.first()
|
|
723
|
+
.click({ timeout: left(30000) });
|
|
724
|
+
}
|
|
725
|
+
// Gate on Operate itself, not on the Console shell: only a booted
|
|
726
|
+
// Operate proves the cluster session now exists, which is exactly what
|
|
727
|
+
// the deep-link re-open below needs. Returning any earlier would hand
|
|
728
|
+
// the cluster the same missing session as the round before it.
|
|
729
|
+
await this.page
|
|
730
|
+
.getByRole('link', { name: 'Camunda logo Operate' })
|
|
731
|
+
.waitFor({ state: 'visible', timeout: left(60000) });
|
|
732
|
+
booted = true;
|
|
733
|
+
}
|
|
734
|
+
catch (error) {
|
|
735
|
+
// Left to the caller's loop, exactly as in `reload` below: the re-open
|
|
736
|
+
// that follows is what matters, and it is retried anyway.
|
|
737
|
+
console.warn(`Could not re-enter through the Console root: ${error}`);
|
|
738
|
+
}
|
|
739
|
+
// Report whether a cluster session was actually minted, not merely whether
|
|
740
|
+
// the deep-link re-open navigated. Only a re-entry that booted Operate has
|
|
741
|
+
// started a NEW hand-off; returning `reload()`'s result said "restarted"
|
|
742
|
+
// for every round whose re-entry had timed out, so the caller granted the
|
|
743
|
+
// untouched, still-wedged callback another full grace window (and a longer
|
|
744
|
+
// backoff) before trying again -- roughly a third of the budget spent
|
|
745
|
+
// idling on an exchange nothing had restarted.
|
|
746
|
+
const reopened = await this.reload();
|
|
747
|
+
return booted && reopened;
|
|
748
|
+
}
|
|
749
|
+
// Returns whether the re-navigation actually went through, so the auth
|
|
750
|
+
// recovery in `waitForInstanceLocator` can tell "a new hand-off was started"
|
|
751
|
+
// apart from "the tab never moved".
|
|
73
752
|
async reload() {
|
|
74
|
-
|
|
753
|
+
// A failed re-navigation is logged and left to the caller's loop rather
|
|
754
|
+
// than thrown: this runs as the recovery step of `waitForInstanceLocator`,
|
|
755
|
+
// and a navigation that races the page's own in-flight one fails with
|
|
756
|
+
// `net::ERR_ABORTED; maybe frame was detached?` (a DNS blip with
|
|
757
|
+
// `net::ERR_NAME_NOT_RESOLVED`). Those used to escape the retry loop and
|
|
758
|
+
// fail the test outright, even though the very next poll would have
|
|
759
|
+
// re-opened the deep link anyway. A page that genuinely never comes back
|
|
760
|
+
// still fails, through the loop's own timeout and rendered-state report.
|
|
761
|
+
try {
|
|
762
|
+
// Always re-open the remembered instance deep link: once Operate has
|
|
763
|
+
// replaced the URL with the processes list, reloading that list can never
|
|
764
|
+
// bring the instance back (see the class note).
|
|
765
|
+
if (this.instanceUrl !== undefined) {
|
|
766
|
+
await this.page.goto(this.instanceUrl, { waitUntil: 'domcontentloaded' });
|
|
767
|
+
return true;
|
|
768
|
+
}
|
|
769
|
+
await this.page.reload();
|
|
770
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
catch (error) {
|
|
774
|
+
console.warn(`Could not re-open the Operate instance page: ${error}`);
|
|
775
|
+
return false;
|
|
776
|
+
}
|
|
75
777
|
}
|
|
76
778
|
async assertProcessCompleteStatusWithRetry(timeout = 60000, maxRetries = 10) {
|
|
77
779
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|