@camunda/e2e-test-suite 0.0.1182 → 0.0.1183
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/AppsPage.d.ts +1 -0
- package/dist/pages/8.8/AppsPage.js +50 -0
- package/dist/pages/8.8/ModelerHomePage.d.ts +3 -0
- package/dist/pages/8.8/ModelerHomePage.js +81 -5
- package/dist/pages/SM-8.10/Authorization.d.ts +2 -0
- package/dist/pages/SM-8.10/Authorization.js +39 -3
- package/dist/pages/SM-8.10/ModelerHomePage.js +3 -3
- package/dist/pages/SM-8.10/OCTenantPage.js +15 -8
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +1 -1
- package/dist/tests/8.8/access-user-role-management.spec.js +54 -2
- package/package.json +1 -1
|
@@ -44,6 +44,7 @@ declare class AppsPage {
|
|
|
44
44
|
private doClickClusterInOptimize;
|
|
45
45
|
clickOperate(clusterName: string): Promise<void>;
|
|
46
46
|
clickOptimize(clusterName: string): Promise<void>;
|
|
47
|
+
openOptimizeWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number, totalTimeout?: number): Promise<void>;
|
|
47
48
|
clickCamundaApps(maxRetries?: number): Promise<void>;
|
|
48
49
|
private dismissBlockingModal;
|
|
49
50
|
clickConsoleLink(): Promise<void>;
|
|
@@ -322,6 +322,56 @@ class AppsPage {
|
|
|
322
322
|
}
|
|
323
323
|
throw new Error(`Failed to click the optimize link after ${maxRetries} attempts.`);
|
|
324
324
|
}
|
|
325
|
+
// `clickOptimize` returns once it has clicked, not once Optimize has
|
|
326
|
+
// rendered, so a hand-off that lands somewhere else (an SSO bounce to
|
|
327
|
+
// `/login`, or `/login?error`) is reported as success. Retrying the click
|
|
328
|
+
// from there does not help: on a dead page none of the app-switcher buttons
|
|
329
|
+
// is visible, so `clickCamundaApps` below burns its three rounds probing and
|
|
330
|
+
// then throws `Failed to open app switcher after 3 attempts` -- and a caller
|
|
331
|
+
// that just re-clicks re-probes the same page until its budget runs out.
|
|
332
|
+
// Going back to `/` first is the only thing that recovers, which is what this
|
|
333
|
+
// wrapper adds: the throw is caught below and turned into a re-entry.
|
|
334
|
+
//
|
|
335
|
+
// Note this is where 8.8 differs from 8.9/8.10, whose `clickCamundaApps`
|
|
336
|
+
// falls out of the same loop and returns *without* throwing. The recovery
|
|
337
|
+
// this wrapper provides is needed either way, but do not carry that
|
|
338
|
+
// description back here -- on 8.8 the failure arrives as an exception.
|
|
339
|
+
// `totalTimeout` is what makes the stated budget true. Retrying on a thrown
|
|
340
|
+
// hand-off (rather than escaping on the first throw) is correct, but it also
|
|
341
|
+
// multiplies the cost: a worst-case attempt here is clickCamundaApps (~188s
|
|
342
|
+
// when it has to hunt for a trigger) + clickOptimize (~456s across its own
|
|
343
|
+
// three retries) + the banner wait, so four attempts could reach ~43 min and
|
|
344
|
+
// blow the 36-minute test.slow() budget of the specs that call this. Checked
|
|
345
|
+
// between attempts, so the real ceiling is the deadline plus at most one
|
|
346
|
+
// attempt -- bounded, which "N x timeout" arithmetic alone never was.
|
|
347
|
+
async openOptimizeWithRetry(banner, clusterName, timeout = 60000, maxRetries = 3, totalTimeout = 600000) {
|
|
348
|
+
const start = Date.now();
|
|
349
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
350
|
+
try {
|
|
351
|
+
// Inside the try, not before it: both of these throw -- `clickOptimize`
|
|
352
|
+
// after its own retries, `clickCamundaApps` once its probes are
|
|
353
|
+
// exhausted or a click times out. With those calls outside, the first
|
|
354
|
+
// such throw escaped this helper before it ever reached the
|
|
355
|
+
// `page.goto('/')` below -- so in exactly the dead page case this
|
|
356
|
+
// wrapper exists for, the recovery was skipped and the remaining
|
|
357
|
+
// attempts never ran.
|
|
358
|
+
await this.clickCamundaApps();
|
|
359
|
+
await this.clickOptimize(clusterName);
|
|
360
|
+
await (0, test_1.expect)(banner).toBeVisible({ timeout });
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
catch (error) {
|
|
364
|
+
const elapsed = Date.now() - start;
|
|
365
|
+
if (attempt === maxRetries - 1 || elapsed >= totalTimeout) {
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
console.warn(`Optimize hand-off failed on attempt ${attempt + 1} ` +
|
|
369
|
+
`(${Math.round(elapsed / 1000)}s elapsed); re-entering through ` +
|
|
370
|
+
`the apps menu. ${error}`);
|
|
371
|
+
await this.page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
325
375
|
async clickCamundaApps(maxRetries = 3) {
|
|
326
376
|
await (0, sleep_1.sleep)(2000);
|
|
327
377
|
const appButtons = [
|
|
@@ -29,12 +29,15 @@ declare class ModelerHomePage {
|
|
|
29
29
|
readonly createIdpApplicationButton: Locator;
|
|
30
30
|
readonly dialog: Locator;
|
|
31
31
|
readonly closeButton: Locator;
|
|
32
|
+
readonly blockingDialog: Locator;
|
|
33
|
+
readonly blockingDialogDismiss: Locator;
|
|
32
34
|
readonly cancelButton: Locator;
|
|
33
35
|
readonly optionsButton: Locator;
|
|
34
36
|
readonly formNameBreadcrumb: (formName: string) => Locator;
|
|
35
37
|
readonly buttonSkipCustomization: Locator;
|
|
36
38
|
constructor(page: Page);
|
|
37
39
|
dismissBlockingDialog(): Promise<void>;
|
|
40
|
+
private dismissUncataloguedDialog;
|
|
38
41
|
dismissOverlays(): Promise<void>;
|
|
39
42
|
clickCreateNewProjectButton(): Promise<void>;
|
|
40
43
|
enterNewProjectName(name: string): Promise<void>;
|
|
@@ -49,6 +49,8 @@ class ModelerHomePage {
|
|
|
49
49
|
createIdpApplicationButton;
|
|
50
50
|
dialog;
|
|
51
51
|
closeButton;
|
|
52
|
+
blockingDialog;
|
|
53
|
+
blockingDialogDismiss;
|
|
52
54
|
cancelButton;
|
|
53
55
|
optionsButton;
|
|
54
56
|
formNameBreadcrumb;
|
|
@@ -127,6 +129,37 @@ class ModelerHomePage {
|
|
|
127
129
|
this.closeButton = page
|
|
128
130
|
.locator('button:visible', { hasText: /^Got it/ })
|
|
129
131
|
.first();
|
|
132
|
+
// Not every modal hub stacks over the projects list carries a "Got it"
|
|
133
|
+
// control. When one does not, the `closeButton` probe finds nothing and
|
|
134
|
+
// `dismissBlockingDialog` returns having dismissed *nothing*, so the caller
|
|
135
|
+
// clicks straight into an overlay that is still there. Observed on this
|
|
136
|
+
// version in run 35102270880 (chromium-v2), which lost `Public Start Form`,
|
|
137
|
+
// `RBA On User Flow - No User Permission`, `- Permission for One Process`
|
|
138
|
+
// and `Form.js Integration with User Task and AI Generated Form` to exactly
|
|
139
|
+
// that.
|
|
140
|
+
//
|
|
141
|
+
// Anchored on the visible dialog so an announcement nobody has catalogued
|
|
142
|
+
// yet is still cleared, rather than on one more label. `:visible` matters --
|
|
143
|
+
// the Modeler home page keeps several `role="dialog"` containers mounted
|
|
144
|
+
// and hidden, which is why the label-first lookup above exists at all.
|
|
145
|
+
//
|
|
146
|
+
// `.last()`, not `.first()`: Carbon stacks modals in DOM order, so the last
|
|
147
|
+
// one is the topmost, and it is the topmost backdrop that intercepts the
|
|
148
|
+
// pointer events this whole helper exists to clear. Targeting `.first()`
|
|
149
|
+
// would click the dismiss button of a *covered* dialog and leave the
|
|
150
|
+
// blocker up -- reproducing the interception rather than fixing it. Same
|
|
151
|
+
// reasoning, and the same conclusion, as `HomePage.closeInformationDialog`.
|
|
152
|
+
// The nth here is deliberate rather than the accidental kind CLAUDE.md
|
|
153
|
+
// warns about: picking the top of a page-wide stack is the whole point, so
|
|
154
|
+
// there is no parent to scope to, and `:visible` already excludes the
|
|
155
|
+
// mounted-but-hidden containers.
|
|
156
|
+
this.blockingDialog = page.locator('[role="dialog"]:visible').last();
|
|
157
|
+
// Only unambiguous dismissals. Deliberately NOT "Cancel": that is a form
|
|
158
|
+
// action on the dialogs specs open on purpose (cluster select, deploy), and
|
|
159
|
+
// clicking it would break a working test rather than unblock a stuck one.
|
|
160
|
+
this.blockingDialogDismiss = this.blockingDialog
|
|
161
|
+
.getByRole('button', { name: /^(Got it|Close|Dismiss)/i })
|
|
162
|
+
.first();
|
|
130
163
|
this.optionsButton = page.getByRole('button', { name: 'Options' });
|
|
131
164
|
this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
|
|
132
165
|
hasText: formName,
|
|
@@ -151,17 +184,60 @@ class ModelerHomePage {
|
|
|
151
184
|
.waitForLoadState('networkidle', { timeout: 15000 })
|
|
152
185
|
.catch(() => false);
|
|
153
186
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
154
|
-
const isOpen = await this.closeButton
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
187
|
+
const isOpen = await this.becameVisible(this.closeButton, 10000);
|
|
188
|
+
// `break`, not `return`: a modal without a "Got it" control is exactly
|
|
189
|
+
// the case the generic pass below exists for, and returning here is what
|
|
190
|
+
// let it through.
|
|
158
191
|
if (!isOpen)
|
|
159
|
-
|
|
192
|
+
break;
|
|
160
193
|
await this.closeButton.click({ timeout: 5000 }).catch(() => { });
|
|
161
194
|
await this.closeButton
|
|
162
195
|
.waitFor({ state: 'hidden', timeout: 5000 })
|
|
163
196
|
.catch(() => false);
|
|
164
197
|
}
|
|
198
|
+
await this.dismissUncataloguedDialog();
|
|
199
|
+
}
|
|
200
|
+
// Second pass for a modal the label-based probe above cannot see. Kept cheap
|
|
201
|
+
// because `dismissBlockingDialog` is called from a dozen places on the happy
|
|
202
|
+
// path: when nothing is open this costs one 2s probe.
|
|
203
|
+
async dismissUncataloguedDialog() {
|
|
204
|
+
// Peel the stack from the top, re-resolving `blockingDialog` each round:
|
|
205
|
+
// closing the topmost uncovers the next, and `.last()` then points at it.
|
|
206
|
+
// Three rounds covers the two hub is known to stack (What's New plus the
|
|
207
|
+
// migration notice) and still terminates on the probe below when the stack
|
|
208
|
+
// is clear; it is not a budget to burn, so a clear page costs one 2s probe.
|
|
209
|
+
let lastClickError;
|
|
210
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
211
|
+
if (!(await this.becameVisible(this.blockingDialog, 2000)))
|
|
212
|
+
return;
|
|
213
|
+
if (!(await this.becameVisible(this.blockingDialogDismiss, 2000))) {
|
|
214
|
+
// Nothing safe to click. Leave it rather than guess: pressing Escape
|
|
215
|
+
// blindly does not close a Carbon dialog (focus sits on the body after
|
|
216
|
+
// a load), and clicking an unrecognised footer button could confirm
|
|
217
|
+
// something. The caller's own assertion then reports the real blocker.
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await this.blockingDialogDismiss.click({ timeout: 5000 });
|
|
222
|
+
lastClickError = undefined;
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// Held, not swallowed. A click can fail here for reasons the next round
|
|
226
|
+
// clears on its own -- a backdrop fading in over the button, a dialog
|
|
227
|
+
// re-rendering mid-animation -- so give it that round. If a dialog is
|
|
228
|
+
// still up once the rounds are spent, this error is what gets reported.
|
|
229
|
+
lastClickError = error;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// Still blocked after three rounds. Say so here rather than letting the
|
|
233
|
+
// caller click into the overlay and report "intercepts pointer events" from
|
|
234
|
+
// a line that has nothing to do with the dialog.
|
|
235
|
+
if (await this.blockingDialog.isVisible().catch(() => false)) {
|
|
236
|
+
throw new Error('A blocking Modeler dialog is still visible after 3 dismiss attempts.' +
|
|
237
|
+
(lastClickError === undefined
|
|
238
|
+
? ' Its dismiss control was clicked without closing it.'
|
|
239
|
+
: ` Last dismiss click failed: ${lastClickError}`));
|
|
240
|
+
}
|
|
165
241
|
}
|
|
166
242
|
async dismissOverlays() {
|
|
167
243
|
// Before the generic pass: clickMessageBanner and the loop below resolve
|
|
@@ -10,6 +10,8 @@ declare class Authorization {
|
|
|
10
10
|
protected readonly createAuthorizationOwnerOption: (name: string) => Locator;
|
|
11
11
|
protected readonly createAuthorizationOwnerSearchInput: Locator;
|
|
12
12
|
protected readonly createAuthorizationOwnerSearchOption: (name: string) => Locator;
|
|
13
|
+
protected readonly createAuthorizationOwnerCommandInput: Locator;
|
|
14
|
+
protected readonly createAuthorizationOwnerCommandOptions: Locator;
|
|
13
15
|
protected readonly createAuthorizationResourceTypeDropdown: Locator;
|
|
14
16
|
protected readonly createAuthorizationResourceTypeOption: (name: string) => Locator;
|
|
15
17
|
protected readonly createAuthorizationResourceIdField: Locator;
|
|
@@ -16,6 +16,8 @@ class Authorization {
|
|
|
16
16
|
createAuthorizationOwnerOption;
|
|
17
17
|
createAuthorizationOwnerSearchInput;
|
|
18
18
|
createAuthorizationOwnerSearchOption;
|
|
19
|
+
createAuthorizationOwnerCommandInput;
|
|
20
|
+
createAuthorizationOwnerCommandOptions;
|
|
19
21
|
createAuthorizationResourceTypeDropdown;
|
|
20
22
|
createAuthorizationResourceTypeOption;
|
|
21
23
|
createAuthorizationResourceIdField;
|
|
@@ -72,6 +74,18 @@ class Authorization {
|
|
|
72
74
|
.locator('.cds--list-box__menu-item, [role="option"]')
|
|
73
75
|
.filter({ hasText: name })
|
|
74
76
|
.first();
|
|
77
|
+
// In the design-system build the closed Owner control is a combobox-role
|
|
78
|
+
// trigger that paints "Search by owner ID" as static text (no placeholder
|
|
79
|
+
// attribute), and the cmdk search box plus its result listbox only mount
|
|
80
|
+
// in a popover once the trigger is opened. That popover is portalled out
|
|
81
|
+
// of the dialog, and while it is open Radix marks the dialog aria-hidden,
|
|
82
|
+
// so both must be queried from the page root.
|
|
83
|
+
this.createAuthorizationOwnerCommandInput = this.page
|
|
84
|
+
.locator('[data-slot="command-input"]')
|
|
85
|
+
.first();
|
|
86
|
+
this.createAuthorizationOwnerCommandOptions = this.page
|
|
87
|
+
.getByRole('listbox')
|
|
88
|
+
.getByRole('option');
|
|
75
89
|
this.createAuthorizationResourceTypeDropdown =
|
|
76
90
|
this.createAuthorizationModal.getByRole('combobox', {
|
|
77
91
|
name: 'Resource type',
|
|
@@ -133,7 +147,10 @@ class Authorization {
|
|
|
133
147
|
.click({ timeout: Math.min(timeout, 30000) });
|
|
134
148
|
}
|
|
135
149
|
// Released 8.10 alpha images still render the Carbon combobox that lists
|
|
136
|
-
// every owner, so accept whichever owner
|
|
150
|
+
// every owner, so accept whichever of the three owner fields this build
|
|
151
|
+
// shows: the Carbon search dropdown, the Carbon combobox that lists every
|
|
152
|
+
// owner inline, or the design-system combobox whose cmdk popover only loads
|
|
153
|
+
// results once the owner ID is typed.
|
|
137
154
|
async selectAuthorizationOwner(ownerId) {
|
|
138
155
|
await (0, test_1.expect)(this.createAuthorizationOwnerSearchInput.or(this.createAuthorizationOwnerComboBox)).toBeVisible({ timeout: 30000 });
|
|
139
156
|
if (await this.createAuthorizationOwnerSearchInput.isVisible()) {
|
|
@@ -151,10 +168,29 @@ class Authorization {
|
|
|
151
168
|
return;
|
|
152
169
|
}
|
|
153
170
|
await this.createAuthorizationOwnerComboBox.click();
|
|
154
|
-
|
|
171
|
+
const commandInput = this.createAuthorizationOwnerCommandInput;
|
|
172
|
+
const inlineOwnerOption = this.createAuthorizationOwnerOption(ownerId);
|
|
173
|
+
await (0, test_1.expect)(commandInput.or(inlineOwnerOption).first()).toBeVisible({
|
|
174
|
+
timeout: 30000,
|
|
175
|
+
});
|
|
176
|
+
if (await commandInput.isVisible()) {
|
|
177
|
+
await commandInput.fill(ownerId);
|
|
178
|
+
// The result list is served by the API and debounced, and each option is
|
|
179
|
+
// labelled "<owner id> — <name>" rather than the bare ID, so narrow the
|
|
180
|
+
// search to the single row this unique owner ID resolves to instead of
|
|
181
|
+
// matching on the label text.
|
|
182
|
+
const ownerOptions = this.createAuthorizationOwnerCommandOptions;
|
|
183
|
+
await (0, test_1.expect)(ownerOptions).toHaveCount(1, { timeout: 60000 });
|
|
184
|
+
await ownerOptions.first().click({ timeout: 30000, force: true });
|
|
185
|
+
// Committing the choice closes the popover, which is what hands the
|
|
186
|
+
// dialog back to the accessibility tree for the fields that follow.
|
|
187
|
+
await (0, test_1.expect)(commandInput).toBeHidden({ timeout: 30000 });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
await (0, test_1.expect)(inlineOwnerOption).toBeVisible({
|
|
155
191
|
timeout: 30000,
|
|
156
192
|
});
|
|
157
|
-
await
|
|
193
|
+
await inlineOwnerOption.click({
|
|
158
194
|
timeout: 30000,
|
|
159
195
|
});
|
|
160
196
|
}
|
|
@@ -24,7 +24,7 @@ class ModelerHomePage {
|
|
|
24
24
|
// Flag-on (workspaces-enabled) state. Under camunda-hub#27903 the c8-e2e
|
|
25
25
|
// "project folder" maps to a Workspace and the "process application" to a
|
|
26
26
|
// Project: `/` redirects to the `/workspaces` overview, a Workspace holds a
|
|
27
|
-
// projects listing at `/
|
|
27
|
+
// projects listing at `/workspaces/{wsId}/projects`, and a Project (the
|
|
28
28
|
// resource container) lives one level deeper. These remember the shared
|
|
29
29
|
// Workspace's projects-listing URL and the overview URL so returns don't
|
|
30
30
|
// re-walk the hierarchy from the root each time.
|
|
@@ -324,7 +324,7 @@ class ModelerHomePage {
|
|
|
324
324
|
await this.ensureWorkspaceOpen();
|
|
325
325
|
}
|
|
326
326
|
// Ensures the shared "Cross Component Test Project" Workspace is open and the
|
|
327
|
-
// browser is on its projects listing (`/
|
|
327
|
+
// browser is on its projects listing (`/workspaces/{wsId}/projects`), where the
|
|
328
328
|
// per-test Project is later created. Retries the whole navigation+login on a
|
|
329
329
|
// transient web-modeler 500 rather than a bare reload, which would drop the
|
|
330
330
|
// authenticated SPA state and land on the empty login form.
|
|
@@ -440,7 +440,7 @@ class ModelerHomePage {
|
|
|
440
440
|
? openButton.first()
|
|
441
441
|
: this.crossComponentProjectFolder;
|
|
442
442
|
await target.click({ timeout: 60000 });
|
|
443
|
-
await this.page.waitForURL(/\/
|
|
443
|
+
await this.page.waitForURL(/\/workspaces\/[^/]+\/projects/, {
|
|
444
444
|
timeout: 120000,
|
|
445
445
|
});
|
|
446
446
|
this.workspaceProjectsUrl = this.page.url();
|
|
@@ -303,6 +303,17 @@ class OCTenantPage {
|
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
305
|
async fillRoleIdSearchBox(roleId, roleIdentifier) {
|
|
306
|
+
// The option row renders the ID followed by the name, so the name is the
|
|
307
|
+
// tail of `roleIdentifier`.
|
|
308
|
+
const roleName = roleIdentifier.startsWith(roleId)
|
|
309
|
+
? roleIdentifier.slice(roleId.length)
|
|
310
|
+
: '';
|
|
311
|
+
// The design-system row separates the two parts with an em dash
|
|
312
|
+
// ('connectors — Connectors') while the legacy Carbon row concatenates
|
|
313
|
+
// them ('connectorsConnectors'), so match both with an optional
|
|
314
|
+
// dash/whitespace run between the ID and the name.
|
|
315
|
+
const escapeForRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
316
|
+
const rolePattern = new RegExp(`${escapeForRegExp(roleId)}[-\\s\\u2010-\\u2015]*${escapeForRegExp(roleName)}`);
|
|
306
317
|
// The design-system shell portals the matches into a listbox rendered as
|
|
307
318
|
// a sibling of the dialog, so they cannot be scoped to the modal. The
|
|
308
319
|
// legacy Carbon list stays inside it and must remain scoped: the tenant's
|
|
@@ -311,19 +322,15 @@ class OCTenantPage {
|
|
|
311
322
|
// the modal overlay.
|
|
312
323
|
const result = this.page
|
|
313
324
|
.getByRole('option')
|
|
314
|
-
.filter({ hasText:
|
|
315
|
-
.or(this.assignRoleModal.getByText(
|
|
325
|
+
.filter({ hasText: rolePattern })
|
|
326
|
+
.or(this.assignRoleModal.getByText(rolePattern))
|
|
316
327
|
.first();
|
|
317
328
|
// The dropdown no longer filters client-side: it queries
|
|
318
329
|
// `/v2/roles/search` with `name: {$like: '*<typed>*'}`, which is
|
|
319
330
|
// case-sensitive, so the role ID ('connectors') never matches the default
|
|
320
|
-
// role's name ('Connectors')
|
|
321
|
-
//
|
|
322
|
-
// first for the shells that still match on it, then fall back to the name.
|
|
331
|
+
// role's name ('Connectors'): search the ID first for the shells that
|
|
332
|
+
// still match on it, then fall back to the name.
|
|
323
333
|
const searchTerms = [roleId];
|
|
324
|
-
const roleName = roleIdentifier.startsWith(roleId)
|
|
325
|
-
? roleIdentifier.slice(roleId.length)
|
|
326
|
-
: '';
|
|
327
334
|
if (roleName !== '' && roleName !== roleId) {
|
|
328
335
|
searchTerms.push(roleName);
|
|
329
336
|
}
|
|
@@ -12,7 +12,7 @@ const resourceContainers = new WeakMap();
|
|
|
12
12
|
// the resource container -- files, folders and uploads live directly inside it,
|
|
13
13
|
// there is no separate process-application level to descend into. It is created
|
|
14
14
|
// from the workspace's projects listing via the "Create project" split button +
|
|
15
|
-
// dialog, and the app lands on `/
|
|
15
|
+
// dialog, and the app lands on `/workspaces/{wsId}/projects/{projectId}`.
|
|
16
16
|
//
|
|
17
17
|
// This page object owns that container's surface (creating a Project, and binding
|
|
18
18
|
// its development-stage cluster). Entering and leaving it is driven from
|
|
@@ -157,7 +157,33 @@ _8_8_1.test.describe('User Roles User Flow', () => {
|
|
|
157
157
|
username: process.env.C8_USERNAME_TEST_9,
|
|
158
158
|
password: process.env.C8_PASSWORD_TEST_9,
|
|
159
159
|
});
|
|
160
|
-
|
|
160
|
+
});
|
|
161
|
+
await _8_8_1.test.step('Wait for the role change to reach Optimize', async () => {
|
|
162
|
+
// Replaces a blind `sleep(600000)`. The role change only takes effect in
|
|
163
|
+
// Optimize once its permission cache refreshes, and reaching Optimize is
|
|
164
|
+
// the single thing the rest of this test depends on -- so retry the
|
|
165
|
+
// hand-off until it succeeds rather than waiting out a fixed ten minutes.
|
|
166
|
+
// Sized against the helper's REAL per-attempt cost, not just the banner
|
|
167
|
+
// timeout: each attempt also pays clickCamundaApps() and clickOptimize()
|
|
168
|
+
// before the banner is ever asserted. Driving the real method under a
|
|
169
|
+
// virtual clock, (40000, 15) -- which looks like "15 x 40s = 600s" --
|
|
170
|
+
// actually runs 986s in the ordinary bad case and 4581s when
|
|
171
|
+
// clickCamundaApps has to hunt for its trigger, the latter alone
|
|
172
|
+
// exceeding this test's 36-minute test.slow() budget. 4 x 120s measures
|
|
173
|
+
// at 581s, which is the honest equivalent of the 600s sleep, and four
|
|
174
|
+
// attempts is ample for the page.goto('/') recovery below (the helper's
|
|
175
|
+
// own default is 3). It still rethrows once exhausted, so a cache that
|
|
176
|
+
// never refreshes fails here rather than further downstream.
|
|
177
|
+
//
|
|
178
|
+
// openOptimizeWithRetry rather than a hand-rolled poll because it carries
|
|
179
|
+
// the recovery this needs: a failed hand-off can leave the page on
|
|
180
|
+
// /login?error, where no app-switcher button is visible, so
|
|
181
|
+
// clickCamundaApps() burns its rounds probing and then throws
|
|
182
|
+
// 'Failed to open app switcher after 3 attempts'. Re-clicking would just
|
|
183
|
+
// retry the same dead page until the budget ran out. openOptimizeWithRetry
|
|
184
|
+
// catches that throw and does page.goto('/') between attempts, which is
|
|
185
|
+
// the only thing that gets back to a usable state.
|
|
186
|
+
await appsPage.openOptimizeWithRetry(optimizeHomePage.optimizeBanner, clusterName, 120000, 4);
|
|
161
187
|
});
|
|
162
188
|
await _8_8_1.test.step('Navigate to Console and Ensure Apps are Available in App Switcher', async () => {
|
|
163
189
|
await appsPage.clickCamundaApps();
|
|
@@ -247,7 +273,33 @@ _8_8_1.test.describe('User Roles User Flow', () => {
|
|
|
247
273
|
username: process.env.C8_USERNAME_TEST_10,
|
|
248
274
|
password: process.env.C8_PASSWORD_TEST_10,
|
|
249
275
|
});
|
|
250
|
-
|
|
276
|
+
});
|
|
277
|
+
await _8_8_1.test.step('Wait for the role change to reach Optimize', async () => {
|
|
278
|
+
// Replaces a blind `sleep(600000)`. The role change only takes effect in
|
|
279
|
+
// Optimize once its permission cache refreshes, and reaching Optimize is
|
|
280
|
+
// the single thing the rest of this test depends on -- so retry the
|
|
281
|
+
// hand-off until it succeeds rather than waiting out a fixed ten minutes.
|
|
282
|
+
// Sized against the helper's REAL per-attempt cost, not just the banner
|
|
283
|
+
// timeout: each attempt also pays clickCamundaApps() and clickOptimize()
|
|
284
|
+
// before the banner is ever asserted. Driving the real method under a
|
|
285
|
+
// virtual clock, (40000, 15) -- which looks like "15 x 40s = 600s" --
|
|
286
|
+
// actually runs 986s in the ordinary bad case and 4581s when
|
|
287
|
+
// clickCamundaApps has to hunt for its trigger, the latter alone
|
|
288
|
+
// exceeding this test's 36-minute test.slow() budget. 4 x 120s measures
|
|
289
|
+
// at 581s, which is the honest equivalent of the 600s sleep, and four
|
|
290
|
+
// attempts is ample for the page.goto('/') recovery below (the helper's
|
|
291
|
+
// own default is 3). It still rethrows once exhausted, so a cache that
|
|
292
|
+
// never refreshes fails here rather than further downstream.
|
|
293
|
+
//
|
|
294
|
+
// openOptimizeWithRetry rather than a hand-rolled poll because it carries
|
|
295
|
+
// the recovery this needs: a failed hand-off can leave the page on
|
|
296
|
+
// /login?error, where no app-switcher button is visible, so
|
|
297
|
+
// clickCamundaApps() burns its rounds probing and then throws
|
|
298
|
+
// 'Failed to open app switcher after 3 attempts'. Re-clicking would just
|
|
299
|
+
// retry the same dead page until the budget ran out. openOptimizeWithRetry
|
|
300
|
+
// catches that throw and does page.goto('/') between attempts, which is
|
|
301
|
+
// the only thing that gets back to a usable state.
|
|
302
|
+
await appsPage.openOptimizeWithRetry(optimizeHomePage.optimizeBanner, clusterName, 120000, 4);
|
|
251
303
|
});
|
|
252
304
|
await _8_8_1.test.step('Navigate to Console and Ensure Apps are Available in App Switcher', async () => {
|
|
253
305
|
await appsPage.clickCamundaApps();
|