@camunda/e2e-test-suite 0.0.1180 → 0.0.1182
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 +1 -1
- package/dist/pages/8.10/AppsPage.js +23 -6
- package/dist/pages/8.10/ClusterPage.js +56 -18
- package/dist/pages/8.10/ModelerHomePage.d.ts +3 -0
- package/dist/pages/8.10/ModelerHomePage.js +51 -5
- package/dist/pages/8.7/ModelerCreatePage.d.ts +4 -1
- package/dist/pages/8.7/ModelerCreatePage.js +84 -29
- package/dist/pages/8.7/UtilitiesPage.js +4 -1
- package/dist/pages/8.9/AppsPage.d.ts +1 -0
- package/dist/pages/8.9/AppsPage.js +43 -0
- package/dist/pages/8.9/ModelerHomePage.d.ts +3 -0
- package/dist/pages/8.9/ModelerHomePage.js +51 -5
- package/dist/tests/8.10/access-user-role-management.spec.js +52 -2
- package/dist/tests/8.9/access-user-role-management.spec.js +52 -2
- package/package.json +1 -1
|
@@ -31,7 +31,7 @@ declare class AppsPage {
|
|
|
31
31
|
openOperateWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number): Promise<void>;
|
|
32
32
|
clickTasklist(clusterName: string): Promise<void>;
|
|
33
33
|
openTasklistWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number): Promise<void>;
|
|
34
|
-
openOptimizeWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number): Promise<void>;
|
|
34
|
+
openOptimizeWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number, totalTimeout?: number): Promise<void>;
|
|
35
35
|
clickAdmin(clusterName: string): Promise<void>;
|
|
36
36
|
private doClickClusterInOperate;
|
|
37
37
|
private doClickClusterInTasklist;
|
|
@@ -352,20 +352,37 @@ class AppsPage {
|
|
|
352
352
|
// instead. Every other 8.10 spec already asserts `optimizeBanner` right
|
|
353
353
|
// after `clickOptimize` for exactly this reason; only the smoke flow did
|
|
354
354
|
// not.
|
|
355
|
-
|
|
355
|
+
// `totalTimeout` is what makes the stated budget true. Retrying on a thrown
|
|
356
|
+
// hand-off (rather than escaping on the first throw) is correct, but it also
|
|
357
|
+
// multiplies the cost: a worst-case attempt here is clickCamundaApps (~188s
|
|
358
|
+
// when it has to hunt for a trigger) + clickOptimize (~456s across its own
|
|
359
|
+
// three retries) + the banner wait, so four attempts could reach ~43 min and
|
|
360
|
+
// blow the 36-minute test.slow() budget of the specs that call this. Checked
|
|
361
|
+
// between attempts, so the real ceiling is the deadline plus at most one
|
|
362
|
+
// attempt -- bounded, which "N x timeout" arithmetic alone never was.
|
|
363
|
+
async openOptimizeWithRetry(banner, clusterName, timeout = 60000, maxRetries = 3, totalTimeout = 600000) {
|
|
364
|
+
const start = Date.now();
|
|
356
365
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
357
|
-
await this.clickCamundaApps();
|
|
358
|
-
await this.clickOptimize(clusterName);
|
|
359
366
|
try {
|
|
367
|
+
// Inside the try, not before it: `clickOptimize` throws after its own
|
|
368
|
+
// retries, and `clickCamundaApps` can throw on a click timeout. With
|
|
369
|
+
// those calls outside, the first such throw escaped this helper before
|
|
370
|
+
// it ever reached the `page.goto('/')` below -- so in exactly the dead
|
|
371
|
+
// page case this wrapper exists for, the recovery was skipped and the
|
|
372
|
+
// remaining attempts never ran.
|
|
373
|
+
await this.clickCamundaApps();
|
|
374
|
+
await this.clickOptimize(clusterName);
|
|
360
375
|
await (0, test_1.expect)(banner).toBeVisible({ timeout });
|
|
361
376
|
return;
|
|
362
377
|
}
|
|
363
378
|
catch (error) {
|
|
364
|
-
|
|
379
|
+
const elapsed = Date.now() - start;
|
|
380
|
+
if (attempt === maxRetries - 1 || elapsed >= totalTimeout) {
|
|
365
381
|
throw error;
|
|
366
382
|
}
|
|
367
|
-
console.warn(`Optimize
|
|
368
|
-
|
|
383
|
+
console.warn(`Optimize hand-off failed on attempt ${attempt + 1} ` +
|
|
384
|
+
`(${Math.round(elapsed / 1000)}s elapsed); re-entering through ` +
|
|
385
|
+
`the apps menu. ${error}`);
|
|
369
386
|
await this.page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
370
387
|
}
|
|
371
388
|
}
|
|
@@ -462,37 +462,69 @@ class ClusterPage {
|
|
|
462
462
|
exact: true,
|
|
463
463
|
});
|
|
464
464
|
const start = Date.now();
|
|
465
|
-
|
|
465
|
+
// The loop is bounded by `totalTimeout` alone; it used to also stop at a
|
|
466
|
+
// hardcoded `maxRetries = 5`. What that cap changed is the *reload* rate,
|
|
467
|
+
// not the budget: with `visibilityTimeout` at 120s (what
|
|
468
|
+
// createClusterIfNotExists passes) five attempts of ~123s each already
|
|
469
|
+
// consumed the full 600s, so the old loop was not giving budget back.
|
|
470
|
+
//
|
|
471
|
+
// What it did do is reload the cluster list only 5 times across those 600s.
|
|
472
|
+
// `pollTimeout` caps how long any single `waitFor` blocks, which raises that
|
|
473
|
+
// to ~33 reloads for the same wall clock. That matters because the Healthy
|
|
474
|
+
// badge is only re-read after a reload -- a `waitFor` sitting on an
|
|
475
|
+
// already-rendered page cannot observe a status that changed server-side,
|
|
476
|
+
// so the effective polling resolution is the reload interval (~123s before,
|
|
477
|
+
// ~18s now), not the `waitFor` timeout.
|
|
478
|
+
//
|
|
479
|
+
// Worth knowing if you touch this: if the cluster list ever starts
|
|
480
|
+
// live-updating its status without a reload, this change stops buying
|
|
481
|
+
// anything, because then the old loop's long `waitFor` would already notice
|
|
482
|
+
// the transition the moment it happened.
|
|
483
|
+
const pollTimeout = Math.min(visibilityTimeout, 15000);
|
|
466
484
|
let attempt = 0;
|
|
467
|
-
|
|
485
|
+
// Unhealthy is confirmed by how long it persists, not by a poll count: a
|
|
486
|
+
// cluster mid-provisioning can flash through Unhealthy during the
|
|
487
|
+
// Unhealthy -> Creating -> Healthy transition. The old "2 consecutive
|
|
488
|
+
// polls" gate was poll-rate dependent -- at a `visibilityTimeout`-long
|
|
489
|
+
// interval it meant roughly 2 x visibilityTimeout of Unhealthy -- so now
|
|
490
|
+
// that we poll every 15s it has to be expressed as elapsed time, or the
|
|
491
|
+
// tolerance silently shrinks to 30s.
|
|
492
|
+
//
|
|
493
|
+
// It is derived from the caller's `visibilityTimeout` rather than fixed, so
|
|
494
|
+
// each caller keeps the tolerance it had: createClusterIfNotExists (120s)
|
|
495
|
+
// gets 4 minutes, and console-user-flows' cluster-creation check (300s,
|
|
496
|
+
// with a 25-minute total budget) keeps its ~10 minutes instead of being cut
|
|
497
|
+
// to 2. The floor matters for callers that pass a small visibilityTimeout.
|
|
498
|
+
const unhealthyConfirmMs = Math.max(2 * visibilityTimeout, 120000);
|
|
499
|
+
let unhealthySince = null;
|
|
468
500
|
let lastErr;
|
|
469
|
-
while (Date.now() - start < totalTimeout
|
|
501
|
+
while (Date.now() - start < totalTimeout) {
|
|
470
502
|
attempt++;
|
|
471
503
|
try {
|
|
472
504
|
await this.page.reload();
|
|
473
505
|
await (0, test_1.expect)(this.clusterRow(name)).toBeVisible({ timeout: 10000 });
|
|
474
|
-
// Only treat Unhealthy as terminal
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
// transition and we don't want a
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
// on attempt 2.
|
|
506
|
+
// Only treat Unhealthy as terminal once it has persisted for
|
|
507
|
+
// `unhealthyConfirmMs`. A cluster mid-provisioning can flash through
|
|
508
|
+
// Unhealthy briefly during the Unhealthy → Creating → Healthy
|
|
509
|
+
// transition and we don't want a transient reading to abort the whole
|
|
510
|
+
// helper — earlier versions gated on `attempt > 1`, then on two
|
|
511
|
+
// consecutive polls, both of which are poll-rate dependent.
|
|
481
512
|
const isUnhealthy = await unhealthy
|
|
482
513
|
.isVisible({ timeout: 2000 })
|
|
483
514
|
.catch(() => false);
|
|
484
515
|
if (isUnhealthy) {
|
|
485
|
-
|
|
516
|
+
unhealthySince ??= Date.now();
|
|
517
|
+
const unhealthyFor = Date.now() - unhealthySince;
|
|
518
|
+
if (unhealthyFor >= unhealthyConfirmMs) {
|
|
519
|
+
throw new Error(`Cluster "${name}" reached terminal Unhealthy state after ${Math.round((Date.now() - start) / 1000)}s (Unhealthy continuously for ${Math.round(unhealthyFor / 1000)}s); aborting Healthy poll.`);
|
|
520
|
+
}
|
|
486
521
|
}
|
|
487
522
|
else {
|
|
488
|
-
|
|
489
|
-
}
|
|
490
|
-
if (unhealthyConsecutive >= 2) {
|
|
491
|
-
throw new Error(`Cluster "${name}" reached terminal Unhealthy state after ${Math.round((Date.now() - start) / 1000)}s (${unhealthyConsecutive} consecutive Unhealthy polls); aborting Healthy poll.`);
|
|
523
|
+
unhealthySince = null;
|
|
492
524
|
}
|
|
493
525
|
await this.clusterHealthiness(name).waitFor({
|
|
494
526
|
state: 'visible',
|
|
495
|
-
timeout:
|
|
527
|
+
timeout: pollTimeout,
|
|
496
528
|
});
|
|
497
529
|
console.log('Cluster is healthy:', name);
|
|
498
530
|
return;
|
|
@@ -503,11 +535,17 @@ class ClusterPage {
|
|
|
503
535
|
throw err;
|
|
504
536
|
}
|
|
505
537
|
lastErr = err;
|
|
506
|
-
|
|
538
|
+
// Only log every 8th attempt now that we poll every 15s: the previous
|
|
539
|
+
// one-line-per-attempt output was 4 lines, it would otherwise be ~40
|
|
540
|
+
// identical lines per cluster and this suite creates several.
|
|
541
|
+
if (attempt === 1 || attempt % 8 === 0) {
|
|
542
|
+
console.log(`Attempt ${attempt} failed for cluster "${name}" Healthy check ` +
|
|
543
|
+
`(${Math.round((Date.now() - start) / 1000)}s elapsed), retrying...`, err instanceof Error ? err.message : err);
|
|
544
|
+
}
|
|
507
545
|
await this.page.waitForTimeout(500);
|
|
508
546
|
}
|
|
509
547
|
}
|
|
510
|
-
throw new Error(`Cluster "${name}" did not become Healthy
|
|
548
|
+
throw new Error(`Cluster "${name}" did not become Healthy within ${Math.round(totalTimeout / 1000)}s (${attempt} polls). Last error: ${lastErr instanceof Error ? lastErr.message : lastErr}`);
|
|
511
549
|
}
|
|
512
550
|
async clickAlphaTab() {
|
|
513
551
|
await this.alphaTab.click();
|
|
@@ -29,6 +29,8 @@ 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 optionsButton: Locator;
|
|
33
35
|
readonly formNameBreadcrumb: (formName: string) => Locator;
|
|
34
36
|
readonly breadcrumbProjectMenu: Locator;
|
|
@@ -68,6 +70,7 @@ declare class ModelerHomePage {
|
|
|
68
70
|
clickManageButton(retries?: number): Promise<void>;
|
|
69
71
|
clickUploadFilesButton(): Promise<void>;
|
|
70
72
|
dismissBlockingDialog(): Promise<void>;
|
|
73
|
+
private dismissUncataloguedDialog;
|
|
71
74
|
clickMessageBanner(): Promise<void>;
|
|
72
75
|
selectCluster(clusterName: string): Promise<void>;
|
|
73
76
|
clickIdpApplicationTemplateOption(): Promise<void>;
|
|
@@ -47,6 +47,8 @@ class ModelerHomePage {
|
|
|
47
47
|
createIdpApplicationButton;
|
|
48
48
|
dialog;
|
|
49
49
|
closeButton;
|
|
50
|
+
blockingDialog;
|
|
51
|
+
blockingDialogDismiss;
|
|
50
52
|
optionsButton;
|
|
51
53
|
formNameBreadcrumb;
|
|
52
54
|
breadcrumbProjectMenu;
|
|
@@ -132,6 +134,25 @@ class ModelerHomePage {
|
|
|
132
134
|
this.closeButton = page
|
|
133
135
|
.locator('button:visible', { hasText: /^Got it/ })
|
|
134
136
|
.first();
|
|
137
|
+
// Not every modal hub stacks over the projects list carries a "Got it"
|
|
138
|
+
// control. When one does not, the `closeButton` probe above finds nothing
|
|
139
|
+
// and `dismissBlockingDialog` returns having dismissed *nothing*, so the
|
|
140
|
+
// caller clicks straight into an overlay that is still there. Run
|
|
141
|
+
// 35102164337 lost 9 tests that way: the project link resolved "visible,
|
|
142
|
+
// enabled and stable", then the click retried 116 times against a modal
|
|
143
|
+
// reported as intercepting pointer events.
|
|
144
|
+
//
|
|
145
|
+
// Anchored on the visible dialog so an announcement nobody has catalogued
|
|
146
|
+
// yet is still cleared, rather than on one more label. `:visible` matters —
|
|
147
|
+
// the Modeler home page keeps several `role="dialog"` containers mounted
|
|
148
|
+
// and hidden, which is why the label-first lookup above exists at all.
|
|
149
|
+
this.blockingDialog = page.locator('[role="dialog"]:visible').first();
|
|
150
|
+
// Only unambiguous dismissals. Deliberately NOT "Cancel": that is a form
|
|
151
|
+
// action on the dialogs specs open on purpose (cluster select, deploy), and
|
|
152
|
+
// clicking it would break a working test rather than unblock a stuck one.
|
|
153
|
+
this.blockingDialogDismiss = this.blockingDialog
|
|
154
|
+
.getByRole('button', { name: /^(Got it|Close|Dismiss)/i })
|
|
155
|
+
.first();
|
|
135
156
|
this.optionsButton = page.getByRole('button', { name: 'Options' });
|
|
136
157
|
this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
|
|
137
158
|
hasText: formName,
|
|
@@ -585,17 +606,42 @@ class ModelerHomePage {
|
|
|
585
606
|
.waitForLoadState('networkidle', { timeout: 15000 })
|
|
586
607
|
.catch(() => false);
|
|
587
608
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
588
|
-
const isOpen = await this.closeButton
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
609
|
+
const isOpen = await this.becameVisible(this.closeButton, 10000);
|
|
610
|
+
// `break`, not `return`: a modal without a "Got it" control is exactly
|
|
611
|
+
// the case the generic pass below exists for, and returning here is what
|
|
612
|
+
// let it through.
|
|
592
613
|
if (!isOpen)
|
|
593
|
-
|
|
614
|
+
break;
|
|
594
615
|
await this.closeButton.click({ timeout: 5000 }).catch(() => { });
|
|
595
616
|
await this.closeButton
|
|
596
617
|
.waitFor({ state: 'hidden', timeout: 5000 })
|
|
597
618
|
.catch(() => false);
|
|
598
619
|
}
|
|
620
|
+
await this.dismissUncataloguedDialog();
|
|
621
|
+
}
|
|
622
|
+
// Second pass for a modal the label-based probe above cannot see. Kept cheap
|
|
623
|
+
// because `dismissBlockingDialog` is called from a dozen places on the happy
|
|
624
|
+
// path: when nothing is open this costs one 2s probe, so it cannot turn into
|
|
625
|
+
// the kind of hidden budget the Optimize retry sizing did.
|
|
626
|
+
async dismissUncataloguedDialog() {
|
|
627
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
628
|
+
if (!(await this.becameVisible(this.blockingDialog, 2000)))
|
|
629
|
+
return;
|
|
630
|
+
if (!(await this.becameVisible(this.blockingDialogDismiss, 2000))) {
|
|
631
|
+
// Nothing safe to click. Leave it rather than guess: pressing Escape
|
|
632
|
+
// blindly does not close a Carbon dialog (focus sits on the body after
|
|
633
|
+
// a load), and clicking an unrecognised footer button could confirm
|
|
634
|
+
// something. The caller's own assertion then reports the real blocker.
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
await this.blockingDialogDismiss.click({ timeout: 5000 }).catch(() => { });
|
|
638
|
+
const closed = await this.blockingDialog
|
|
639
|
+
.waitFor({ state: 'hidden', timeout: 5000 })
|
|
640
|
+
.then(() => true)
|
|
641
|
+
.catch(() => false);
|
|
642
|
+
if (closed)
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
599
645
|
}
|
|
600
646
|
async clickMessageBanner() {
|
|
601
647
|
try {
|
|
@@ -11,6 +11,7 @@ declare class ModelerCreatePage {
|
|
|
11
11
|
readonly userTaskOption: Locator;
|
|
12
12
|
readonly appendEndEventButton: Locator;
|
|
13
13
|
readonly viewProcessInstanceLink: Locator;
|
|
14
|
+
readonly autosaveIndicator: Locator;
|
|
14
15
|
readonly nameInput: Locator;
|
|
15
16
|
readonly diagramBreadcrumb: Locator;
|
|
16
17
|
readonly renameDiagramNameButton: Locator;
|
|
@@ -135,7 +136,8 @@ declare class ModelerCreatePage {
|
|
|
135
136
|
runProcessInstance(clusterName: string, variables?: string, formVariables?: {
|
|
136
137
|
[key: string]: string;
|
|
137
138
|
}, textToVisible?: string): Promise<void>;
|
|
138
|
-
retryRunProcess(maxRetries?: number): Promise<void>;
|
|
139
|
+
retryRunProcess(clusterName: string, fillDialog?: () => Promise<void>, maxRetries?: number): Promise<void>;
|
|
140
|
+
private reopenDeployAndRunDialog;
|
|
139
141
|
clickViewProcessInstanceLink(): Promise<void>;
|
|
140
142
|
clickNameInput(): Promise<void>;
|
|
141
143
|
fillNamedInput(name: string): Promise<void>;
|
|
@@ -197,6 +199,7 @@ declare class ModelerCreatePage {
|
|
|
197
199
|
clickIdInput(): Promise<void>;
|
|
198
200
|
fillIdInput(id: string): Promise<void>;
|
|
199
201
|
setAndVerifyProcessId(processId: string): Promise<void>;
|
|
202
|
+
waitForAutosaveAfterEdits(timeout?: number): Promise<void>;
|
|
200
203
|
fillStartFormName(name: string): Promise<void>;
|
|
201
204
|
fillStartFormDate(date: string): Promise<void>;
|
|
202
205
|
checkStartFormCheckbox(): Promise<void>;
|
|
@@ -17,6 +17,7 @@ class ModelerCreatePage {
|
|
|
17
17
|
userTaskOption;
|
|
18
18
|
appendEndEventButton;
|
|
19
19
|
viewProcessInstanceLink;
|
|
20
|
+
autosaveIndicator;
|
|
20
21
|
nameInput;
|
|
21
22
|
diagramBreadcrumb;
|
|
22
23
|
renameDiagramNameButton;
|
|
@@ -150,6 +151,7 @@ class ModelerCreatePage {
|
|
|
150
151
|
name: 'View process instance',
|
|
151
152
|
});
|
|
152
153
|
this.viewProcessLink = page.getByText('view process in Operate');
|
|
154
|
+
this.autosaveIndicator = page.locator('[data-test="autosave"]');
|
|
153
155
|
this.nameInput = page.getByLabel('Name', { exact: true });
|
|
154
156
|
this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
|
|
155
157
|
this.renameDiagramNameButton = page.getByText('Rename');
|
|
@@ -558,35 +560,39 @@ class ModelerCreatePage {
|
|
|
558
560
|
await this.clickDeployAndRunMainButton();
|
|
559
561
|
await this.selectCluster(clusterName, 120000);
|
|
560
562
|
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
if (Object.keys(formVariables).length > 0) {
|
|
569
|
-
if ('formName' in formVariables && formVariables['formName']) {
|
|
570
|
-
await this.fillStartFormName(formVariables['formName']);
|
|
571
|
-
}
|
|
572
|
-
if ('checkbox' in formVariables && formVariables['checkbox'] == 'true') {
|
|
573
|
-
await this.checkStartFormCheckbox();
|
|
563
|
+
const fillDialog = async () => {
|
|
564
|
+
if (variables.length > 0) {
|
|
565
|
+
await (0, test_1.expect)(this.variableInput).toBeVisible({
|
|
566
|
+
timeout: 60000,
|
|
567
|
+
});
|
|
568
|
+
await this.clickVariableInput();
|
|
569
|
+
await this.fillVariableInput(variables);
|
|
574
570
|
}
|
|
575
|
-
if (
|
|
576
|
-
|
|
571
|
+
if (Object.keys(formVariables).length > 0) {
|
|
572
|
+
if ('formName' in formVariables && formVariables['formName']) {
|
|
573
|
+
await this.fillStartFormName(formVariables['formName']);
|
|
574
|
+
}
|
|
575
|
+
if ('checkbox' in formVariables &&
|
|
576
|
+
formVariables['checkbox'] == 'true') {
|
|
577
|
+
await this.checkStartFormCheckbox();
|
|
578
|
+
}
|
|
579
|
+
if ('startDate' in formVariables && formVariables['startDate']) {
|
|
580
|
+
await this.fillStartFormDate(formVariables['startDate']);
|
|
581
|
+
}
|
|
582
|
+
if ('startNumber' in formVariables && formVariables['startNumber']) {
|
|
583
|
+
await this.fillStartFormNumber(formVariables['startNumber']);
|
|
584
|
+
}
|
|
577
585
|
}
|
|
578
|
-
if (
|
|
579
|
-
await this.
|
|
586
|
+
if (textToVisible.length > 0) {
|
|
587
|
+
await (0, test_1.expect)(this.dialog.getByText(textToVisible)).toBeVisible({
|
|
588
|
+
timeout: 30000,
|
|
589
|
+
});
|
|
580
590
|
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
timeout: 30000,
|
|
585
|
-
});
|
|
586
|
-
}
|
|
587
|
-
await this.retryRunProcess();
|
|
591
|
+
};
|
|
592
|
+
await fillDialog();
|
|
593
|
+
await this.retryRunProcess(clusterName, fillDialog);
|
|
588
594
|
}
|
|
589
|
-
async retryRunProcess(maxRetries = 3) {
|
|
595
|
+
async retryRunProcess(clusterName, fillDialog, maxRetries = 3) {
|
|
590
596
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
591
597
|
try {
|
|
592
598
|
await this.clickDeployAndRunSubButton();
|
|
@@ -596,16 +602,38 @@ class ModelerCreatePage {
|
|
|
596
602
|
return;
|
|
597
603
|
}
|
|
598
604
|
catch (error) {
|
|
599
|
-
if (attempt
|
|
600
|
-
console.warn(`Attempt ${attempt + 1} failed for running process. Retrying...`);
|
|
601
|
-
}
|
|
602
|
-
else {
|
|
605
|
+
if (attempt === maxRetries - 1) {
|
|
603
606
|
console.error(error);
|
|
604
607
|
throw new Error(`Running process failed after ${maxRetries} attempts`);
|
|
605
608
|
}
|
|
609
|
+
console.warn(`Attempt ${attempt + 1} failed for running process. Retrying...`);
|
|
610
|
+
// The dialog closes on a successful deploy and switches to an error
|
|
611
|
+
// screen when the backend rejects one, so in both cases the sub button
|
|
612
|
+
// is gone and re-clicking it can only time out -- which is how a
|
|
613
|
+
// single rejected deploy burned all three attempts in the nightly.
|
|
614
|
+
// Drive the deploy from the top again whenever the button is no longer
|
|
615
|
+
// on offer, so the next attempt is a real one.
|
|
616
|
+
const stillOffered = await this.deployAndRunSubButton
|
|
617
|
+
.isVisible()
|
|
618
|
+
.catch(() => false);
|
|
619
|
+
if (!stillOffered) {
|
|
620
|
+
await this.reopenDeployAndRunDialog(clusterName);
|
|
621
|
+
await fillDialog?.();
|
|
622
|
+
}
|
|
606
623
|
}
|
|
607
624
|
}
|
|
608
625
|
}
|
|
626
|
+
async reopenDeployAndRunDialog(clusterName) {
|
|
627
|
+
const closeButton = this.dialog
|
|
628
|
+
.getByRole('button', { name: 'Close', exact: true })
|
|
629
|
+
.first();
|
|
630
|
+
if (await closeButton.isVisible().catch(() => false)) {
|
|
631
|
+
await closeButton.click({ timeout: 30000 });
|
|
632
|
+
}
|
|
633
|
+
await (0, test_1.expect)(this.dialog).toBeHidden({ timeout: 30000 });
|
|
634
|
+
await this.clickDeployAndRunMainButton();
|
|
635
|
+
await this.selectCluster(clusterName, 120000);
|
|
636
|
+
}
|
|
609
637
|
async clickViewProcessInstanceLink() {
|
|
610
638
|
await this.viewProcessInstanceLink.click({ timeout: 180000 });
|
|
611
639
|
}
|
|
@@ -998,6 +1026,33 @@ class ModelerCreatePage {
|
|
|
998
1026
|
}
|
|
999
1027
|
await (0, test_1.expect)(this.processIdInput).toHaveValue(processId, { timeout: 10000 });
|
|
1000
1028
|
}
|
|
1029
|
+
// Web Modeler persists editor edits on a debounced autosave, and
|
|
1030
|
+
// "Deploy & run" deploys what the server holds for the process application,
|
|
1031
|
+
// not what is on screen. The nightly trace shows the PATCH that stored the
|
|
1032
|
+
// renamed process id being acknowledged 61ms before the deploy POST, so the
|
|
1033
|
+
// still-stale file was deployed and the follow-up instance creation was
|
|
1034
|
+
// rejected with "Command 'CREATE' rejected with code 'NOT_FOUND': Expected
|
|
1035
|
+
// to find process definition with process ID '<id>', but none found". The
|
|
1036
|
+
// fixed ten-second wait this replaces raced that debounce, which is itself
|
|
1037
|
+
// about ten seconds. Wait for the modeler's own autosave indicator to report
|
|
1038
|
+
// a save that started after the last edit instead.
|
|
1039
|
+
async waitForAutosaveAfterEdits(timeout = 60000) {
|
|
1040
|
+
const readIndicator = () => this.autosaveIndicator.textContent({ timeout: 10000 }).catch(() => null);
|
|
1041
|
+
const beforeEdits = await readIndicator();
|
|
1042
|
+
try {
|
|
1043
|
+
await test_1.expect
|
|
1044
|
+
.poll(readIndicator, { timeout, intervals: [1000] })
|
|
1045
|
+
.not.toBe(beforeEdits);
|
|
1046
|
+
}
|
|
1047
|
+
catch {
|
|
1048
|
+
// The window between the last edit and the baseline read is a few
|
|
1049
|
+
// milliseconds, so the only way the label does not move within a wait
|
|
1050
|
+
// this much longer than the debounce is that the save had already landed
|
|
1051
|
+
// when the baseline was taken -- the edits are persisted either way.
|
|
1052
|
+
console.warn('Autosave indicator did not change after the diagram edits; ' +
|
|
1053
|
+
'treating the edits as already persisted.');
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1001
1056
|
async fillStartFormName(name) {
|
|
1002
1057
|
await this.payloadNameInput.fill(name);
|
|
1003
1058
|
}
|
|
@@ -302,7 +302,10 @@ async function modelDiagramFromFile(page, modelerHomePage, modelerCreatePage, pr
|
|
|
302
302
|
await modelerCreatePage.clickCanvas();
|
|
303
303
|
}
|
|
304
304
|
}
|
|
305
|
-
|
|
305
|
+
// "Deploy & run" deploys the copy of the diagram the server holds, so the
|
|
306
|
+
// rename above has to be persisted first. A fixed ten-second sleep raced the
|
|
307
|
+
// modeler's own ten-second autosave debounce; wait for the save instead.
|
|
308
|
+
await modelerCreatePage.waitForAutosaveAfterEdits();
|
|
306
309
|
}
|
|
307
310
|
exports.modelDiagramFromFile = modelDiagramFromFile;
|
|
308
311
|
async function disableRBA(clusterName, homePage, clusterPage, clusterDetailsPage, appsPage) {
|
|
@@ -29,6 +29,7 @@ declare class AppsPage {
|
|
|
29
29
|
private doClickClusterInOptimize;
|
|
30
30
|
clickOperate(clusterName: string): Promise<void>;
|
|
31
31
|
clickOptimize(clusterName: string): Promise<void>;
|
|
32
|
+
openOptimizeWithRetry(banner: Locator, clusterName: string, timeout?: number, maxRetries?: number, totalTimeout?: number): Promise<void>;
|
|
32
33
|
clickCamundaApps(maxRetries?: number): Promise<void>;
|
|
33
34
|
private dismissBlockingOverlays;
|
|
34
35
|
clickConsoleLink(): Promise<void>;
|
|
@@ -285,6 +285,49 @@ class AppsPage {
|
|
|
285
285
|
}
|
|
286
286
|
throw new Error(`Failed to click the optimize link after ${maxRetries} attempts.`);
|
|
287
287
|
}
|
|
288
|
+
// `clickOptimize` returns once it has clicked, not once Optimize has
|
|
289
|
+
// rendered, so a hand-off that lands somewhere else (an SSO bounce to
|
|
290
|
+
// `/login`, or `/login?error`) is reported as success. Retrying the click
|
|
291
|
+
// from there does not help: `clickCamundaApps` below exits its loop without
|
|
292
|
+
// throwing when none of the app-switcher buttons is visible, which is exactly
|
|
293
|
+
// the state a dead page is in, so a caller that re-clicks just re-probes the
|
|
294
|
+
// same page until its budget runs out. Going back to `/` first is the only
|
|
295
|
+
// thing that recovers, which is what this wrapper adds.
|
|
296
|
+
// `totalTimeout` is what makes the stated budget true. Retrying on a thrown
|
|
297
|
+
// hand-off (rather than escaping on the first throw) is correct, but it also
|
|
298
|
+
// multiplies the cost: a worst-case attempt here is clickCamundaApps (~188s
|
|
299
|
+
// when it has to hunt for a trigger) + clickOptimize (~456s across its own
|
|
300
|
+
// three retries) + the banner wait, so four attempts could reach ~51 min and
|
|
301
|
+
// blow the 36-minute test.slow() budget of the specs that call this. Checked
|
|
302
|
+
// between attempts, so the real ceiling is the deadline plus at most one
|
|
303
|
+
// attempt -- bounded, which "N x timeout" arithmetic alone never was.
|
|
304
|
+
async openOptimizeWithRetry(banner, clusterName, timeout = 60000, maxRetries = 3, totalTimeout = 600000) {
|
|
305
|
+
const start = Date.now();
|
|
306
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
307
|
+
try {
|
|
308
|
+
// Inside the try, not before it: `clickOptimize` throws after its own
|
|
309
|
+
// retries, and `clickCamundaApps` can throw on a click timeout. With
|
|
310
|
+
// those calls outside, the first such throw escaped this helper before
|
|
311
|
+
// it ever reached the `page.goto('/')` below -- so in exactly the dead
|
|
312
|
+
// page case this wrapper exists for, the recovery was skipped and the
|
|
313
|
+
// remaining attempts never ran.
|
|
314
|
+
await this.clickCamundaApps();
|
|
315
|
+
await this.clickOptimize(clusterName);
|
|
316
|
+
await (0, test_1.expect)(banner).toBeVisible({ timeout });
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
catch (error) {
|
|
320
|
+
const elapsed = Date.now() - start;
|
|
321
|
+
if (attempt === maxRetries - 1 || elapsed >= totalTimeout) {
|
|
322
|
+
throw error;
|
|
323
|
+
}
|
|
324
|
+
console.warn(`Optimize hand-off failed on attempt ${attempt + 1} ` +
|
|
325
|
+
`(${Math.round(elapsed / 1000)}s elapsed); re-entering through ` +
|
|
326
|
+
`the apps menu. ${error}`);
|
|
327
|
+
await this.page.goto('/', { waitUntil: 'domcontentloaded' });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
288
331
|
async clickCamundaApps(maxRetries = 3) {
|
|
289
332
|
await (0, sleep_1.sleep)(2000);
|
|
290
333
|
await this.dismissBlockingOverlays();
|
|
@@ -28,10 +28,13 @@ declare class ModelerHomePage {
|
|
|
28
28
|
readonly createIdpApplicationButton: Locator;
|
|
29
29
|
readonly dialog: Locator;
|
|
30
30
|
readonly closeButton: Locator;
|
|
31
|
+
readonly blockingDialog: Locator;
|
|
32
|
+
readonly blockingDialogDismiss: Locator;
|
|
31
33
|
readonly optionsButton: Locator;
|
|
32
34
|
readonly formNameBreadcrumb: (formName: string) => Locator;
|
|
33
35
|
constructor(page: Page);
|
|
34
36
|
dismissBlockingDialog(): Promise<void>;
|
|
37
|
+
private dismissUncataloguedDialog;
|
|
35
38
|
dismissOverlays(): Promise<void>;
|
|
36
39
|
clickCreateNewProjectButton(): Promise<void>;
|
|
37
40
|
enterNewProjectName(name: string): Promise<void>;
|
|
@@ -45,6 +45,8 @@ class ModelerHomePage {
|
|
|
45
45
|
createIdpApplicationButton;
|
|
46
46
|
dialog;
|
|
47
47
|
closeButton;
|
|
48
|
+
blockingDialog;
|
|
49
|
+
blockingDialogDismiss;
|
|
48
50
|
optionsButton;
|
|
49
51
|
formNameBreadcrumb;
|
|
50
52
|
constructor(page) {
|
|
@@ -122,6 +124,26 @@ class ModelerHomePage {
|
|
|
122
124
|
this.closeButton = page
|
|
123
125
|
.locator('button:visible', { hasText: /^Got it/ })
|
|
124
126
|
.first();
|
|
127
|
+
// Not every modal hub stacks over the projects list carries a "Got it"
|
|
128
|
+
// control. When one does not, the `closeButton` probe finds nothing and
|
|
129
|
+
// `dismissBlockingDialog` returns having dismissed *nothing*, so the caller
|
|
130
|
+
// clicks straight into an overlay that is still there. Observed on this
|
|
131
|
+
// version in run 35102229469, which lost `RBA On User Flow - Permission for
|
|
132
|
+
// All Processes with Full Permissions` and `- Permission for Selected
|
|
133
|
+
// Processes Only` to exactly that, with 9 "New project" click failures in
|
|
134
|
+
// the log.
|
|
135
|
+
//
|
|
136
|
+
// Anchored on the visible dialog so an announcement nobody has catalogued
|
|
137
|
+
// yet is still cleared, rather than on one more label. `:visible` matters --
|
|
138
|
+
// the Modeler home page keeps several `role="dialog"` containers mounted
|
|
139
|
+
// and hidden, which is why the label-first lookup above exists at all.
|
|
140
|
+
this.blockingDialog = page.locator('[role="dialog"]:visible').first();
|
|
141
|
+
// Only unambiguous dismissals. Deliberately NOT "Cancel": that is a form
|
|
142
|
+
// action on the dialogs specs open on purpose (cluster select, deploy), and
|
|
143
|
+
// clicking it would break a working test rather than unblock a stuck one.
|
|
144
|
+
this.blockingDialogDismiss = this.blockingDialog
|
|
145
|
+
.getByRole('button', { name: /^(Got it|Close|Dismiss)/i })
|
|
146
|
+
.first();
|
|
125
147
|
this.optionsButton = page.getByRole('button', { name: 'Options' });
|
|
126
148
|
this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
|
|
127
149
|
hasText: formName,
|
|
@@ -143,17 +165,41 @@ class ModelerHomePage {
|
|
|
143
165
|
.waitForLoadState('networkidle', { timeout: 15000 })
|
|
144
166
|
.catch(() => false);
|
|
145
167
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
146
|
-
const isOpen = await this.closeButton
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
168
|
+
const isOpen = await this.becameVisible(this.closeButton, 10000);
|
|
169
|
+
// `break`, not `return`: a modal without a "Got it" control is exactly
|
|
170
|
+
// the case the generic pass below exists for, and returning here is what
|
|
171
|
+
// let it through.
|
|
150
172
|
if (!isOpen)
|
|
151
|
-
|
|
173
|
+
break;
|
|
152
174
|
await this.closeButton.click({ timeout: 5000 }).catch(() => { });
|
|
153
175
|
await this.closeButton
|
|
154
176
|
.waitFor({ state: 'hidden', timeout: 5000 })
|
|
155
177
|
.catch(() => false);
|
|
156
178
|
}
|
|
179
|
+
await this.dismissUncataloguedDialog();
|
|
180
|
+
}
|
|
181
|
+
// Second pass for a modal the label-based probe above cannot see. Kept cheap
|
|
182
|
+
// because `dismissBlockingDialog` is called from a dozen places on the happy
|
|
183
|
+
// path: when nothing is open this costs one 2s probe.
|
|
184
|
+
async dismissUncataloguedDialog() {
|
|
185
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
186
|
+
if (!(await this.becameVisible(this.blockingDialog, 2000)))
|
|
187
|
+
return;
|
|
188
|
+
if (!(await this.becameVisible(this.blockingDialogDismiss, 2000))) {
|
|
189
|
+
// Nothing safe to click. Leave it rather than guess: pressing Escape
|
|
190
|
+
// blindly does not close a Carbon dialog (focus sits on the body after
|
|
191
|
+
// a load), and clicking an unrecognised footer button could confirm
|
|
192
|
+
// something. The caller's own assertion then reports the real blocker.
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
await this.blockingDialogDismiss.click({ timeout: 5000 }).catch(() => { });
|
|
196
|
+
const closed = await this.blockingDialog
|
|
197
|
+
.waitFor({ state: 'hidden', timeout: 5000 })
|
|
198
|
+
.then(() => true)
|
|
199
|
+
.catch(() => false);
|
|
200
|
+
if (closed)
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
157
203
|
}
|
|
158
204
|
async dismissOverlays() {
|
|
159
205
|
// Before the generic pass: the loop below closes whichever overlay it finds
|
|
@@ -154,7 +154,32 @@ _8_10_1.test.describe('User Roles User Flow', () => {
|
|
|
154
154
|
username: process.env.C8_USERNAME_TEST_9,
|
|
155
155
|
password: process.env.C8_PASSWORD_TEST_9,
|
|
156
156
|
});
|
|
157
|
-
|
|
157
|
+
});
|
|
158
|
+
await _8_10_1.test.step('Wait for the role change to reach Optimize', async () => {
|
|
159
|
+
// Replaces a blind `sleep(600000)`. The role change only takes effect in
|
|
160
|
+
// Optimize once its permission cache refreshes, and reaching Optimize is
|
|
161
|
+
// the single thing the rest of this test depends on -- so retry the
|
|
162
|
+
// hand-off until it succeeds rather than waiting out a fixed ten minutes.
|
|
163
|
+
// Sized against the helper's REAL per-attempt cost, not just the banner
|
|
164
|
+
// timeout: each attempt also pays clickCamundaApps() and clickOptimize()
|
|
165
|
+
// before the banner is ever asserted. Driving the real method under a
|
|
166
|
+
// virtual clock, (40000, 15) -- which looks like "15 x 40s = 600s" --
|
|
167
|
+
// actually runs 986s in the ordinary bad case and 4581s when
|
|
168
|
+
// clickCamundaApps has to hunt for its trigger, the latter alone
|
|
169
|
+
// exceeding this test's 36-minute test.slow() budget. 4 x 120s measures
|
|
170
|
+
// at 581s, which is the honest equivalent of the 600s sleep, and four
|
|
171
|
+
// attempts is ample for the page.goto('/') recovery below (the helper's
|
|
172
|
+
// own default is 3). It still rethrows once exhausted, so a cache that
|
|
173
|
+
// never refreshes fails here rather than further downstream.
|
|
174
|
+
//
|
|
175
|
+
// openOptimizeWithRetry rather than a hand-rolled poll because it carries
|
|
176
|
+
// the recovery this needs: a failed hand-off can leave the page on
|
|
177
|
+
// /login?error, where clickCamundaApps() silently returns (it exits its
|
|
178
|
+
// loop without throwing when no app-switcher button is visible), so
|
|
179
|
+
// re-clicking would retry the same dead page until the budget ran out.
|
|
180
|
+
// openOptimizeWithRetry does page.goto('/') between attempts, which is
|
|
181
|
+
// the only thing that gets back to a usable state.
|
|
182
|
+
await appsPage.openOptimizeWithRetry(optimizeHomePage.optimizeBanner, clusterName, 120000, 4);
|
|
158
183
|
});
|
|
159
184
|
await _8_10_1.test.step('Navigate to Console and Ensure Apps are Available in App Switcher', async () => {
|
|
160
185
|
await appsPage.clickCamundaApps();
|
|
@@ -245,7 +270,32 @@ _8_10_1.test.describe('User Roles User Flow', () => {
|
|
|
245
270
|
username: process.env.C8_USERNAME_TEST_10,
|
|
246
271
|
password: process.env.C8_PASSWORD_TEST_10,
|
|
247
272
|
});
|
|
248
|
-
|
|
273
|
+
});
|
|
274
|
+
await _8_10_1.test.step('Wait for the role change to reach Optimize', async () => {
|
|
275
|
+
// Replaces a blind `sleep(600000)`. The role change only takes effect in
|
|
276
|
+
// Optimize once its permission cache refreshes, and reaching Optimize is
|
|
277
|
+
// the single thing the rest of this test depends on -- so retry the
|
|
278
|
+
// hand-off until it succeeds rather than waiting out a fixed ten minutes.
|
|
279
|
+
// Sized against the helper's REAL per-attempt cost, not just the banner
|
|
280
|
+
// timeout: each attempt also pays clickCamundaApps() and clickOptimize()
|
|
281
|
+
// before the banner is ever asserted. Driving the real method under a
|
|
282
|
+
// virtual clock, (40000, 15) -- which looks like "15 x 40s = 600s" --
|
|
283
|
+
// actually runs 986s in the ordinary bad case and 4581s when
|
|
284
|
+
// clickCamundaApps has to hunt for its trigger, the latter alone
|
|
285
|
+
// exceeding this test's 36-minute test.slow() budget. 4 x 120s measures
|
|
286
|
+
// at 581s, which is the honest equivalent of the 600s sleep, and four
|
|
287
|
+
// attempts is ample for the page.goto('/') recovery below (the helper's
|
|
288
|
+
// own default is 3). It still rethrows once exhausted, so a cache that
|
|
289
|
+
// never refreshes fails here rather than further downstream.
|
|
290
|
+
//
|
|
291
|
+
// openOptimizeWithRetry rather than a hand-rolled poll because it carries
|
|
292
|
+
// the recovery this needs: a failed hand-off can leave the page on
|
|
293
|
+
// /login?error, where clickCamundaApps() silently returns (it exits its
|
|
294
|
+
// loop without throwing when no app-switcher button is visible), so
|
|
295
|
+
// re-clicking would retry the same dead page until the budget ran out.
|
|
296
|
+
// openOptimizeWithRetry does page.goto('/') between attempts, which is
|
|
297
|
+
// the only thing that gets back to a usable state.
|
|
298
|
+
await appsPage.openOptimizeWithRetry(optimizeHomePage.optimizeBanner, clusterName, 120000, 4);
|
|
249
299
|
});
|
|
250
300
|
await _8_10_1.test.step('Navigate to Console and Ensure Apps are Available in App Switcher', async () => {
|
|
251
301
|
await appsPage.clickCamundaApps();
|
|
@@ -154,7 +154,32 @@ _8_9_1.test.describe('User Roles User Flow', () => {
|
|
|
154
154
|
username: process.env.C8_USERNAME_TEST_9,
|
|
155
155
|
password: process.env.C8_PASSWORD_TEST_9,
|
|
156
156
|
});
|
|
157
|
-
|
|
157
|
+
});
|
|
158
|
+
await _8_9_1.test.step('Wait for the role change to reach Optimize', async () => {
|
|
159
|
+
// Replaces a blind `sleep(600000)`. The role change only takes effect in
|
|
160
|
+
// Optimize once its permission cache refreshes, and reaching Optimize is
|
|
161
|
+
// the single thing the rest of this test depends on -- so retry the
|
|
162
|
+
// hand-off until it succeeds rather than waiting out a fixed ten minutes.
|
|
163
|
+
// Sized against the helper's REAL per-attempt cost, not just the banner
|
|
164
|
+
// timeout: each attempt also pays clickCamundaApps() and clickOptimize()
|
|
165
|
+
// before the banner is ever asserted. Driving the real method under a
|
|
166
|
+
// virtual clock, (40000, 15) -- which looks like "15 x 40s = 600s" --
|
|
167
|
+
// actually runs 986s in the ordinary bad case and 4581s when
|
|
168
|
+
// clickCamundaApps has to hunt for its trigger, the latter alone
|
|
169
|
+
// exceeding this test's 36-minute test.slow() budget. 4 x 120s measures
|
|
170
|
+
// at 581s, which is the honest equivalent of the 600s sleep, and four
|
|
171
|
+
// attempts is ample for the page.goto('/') recovery below (the helper's
|
|
172
|
+
// own default is 3). It still rethrows once exhausted, so a cache that
|
|
173
|
+
// never refreshes fails here rather than further downstream.
|
|
174
|
+
//
|
|
175
|
+
// openOptimizeWithRetry rather than a hand-rolled poll because it carries
|
|
176
|
+
// the recovery this needs: a failed hand-off can leave the page on
|
|
177
|
+
// /login?error, where clickCamundaApps() silently returns (it exits its
|
|
178
|
+
// loop without throwing when no app-switcher button is visible), so
|
|
179
|
+
// re-clicking would retry the same dead page until the budget ran out.
|
|
180
|
+
// openOptimizeWithRetry does page.goto('/') between attempts, which is
|
|
181
|
+
// the only thing that gets back to a usable state.
|
|
182
|
+
await appsPage.openOptimizeWithRetry(optimizeHomePage.optimizeBanner, clusterName, 120000, 4);
|
|
158
183
|
});
|
|
159
184
|
await _8_9_1.test.step('Navigate to Console and Ensure Apps are Available in App Switcher', async () => {
|
|
160
185
|
await appsPage.clickCamundaApps();
|
|
@@ -245,7 +270,32 @@ _8_9_1.test.describe('User Roles User Flow', () => {
|
|
|
245
270
|
username: process.env.C8_USERNAME_TEST_10,
|
|
246
271
|
password: process.env.C8_PASSWORD_TEST_10,
|
|
247
272
|
});
|
|
248
|
-
|
|
273
|
+
});
|
|
274
|
+
await _8_9_1.test.step('Wait for the role change to reach Optimize', async () => {
|
|
275
|
+
// Replaces a blind `sleep(600000)`. The role change only takes effect in
|
|
276
|
+
// Optimize once its permission cache refreshes, and reaching Optimize is
|
|
277
|
+
// the single thing the rest of this test depends on -- so retry the
|
|
278
|
+
// hand-off until it succeeds rather than waiting out a fixed ten minutes.
|
|
279
|
+
// Sized against the helper's REAL per-attempt cost, not just the banner
|
|
280
|
+
// timeout: each attempt also pays clickCamundaApps() and clickOptimize()
|
|
281
|
+
// before the banner is ever asserted. Driving the real method under a
|
|
282
|
+
// virtual clock, (40000, 15) -- which looks like "15 x 40s = 600s" --
|
|
283
|
+
// actually runs 986s in the ordinary bad case and 4581s when
|
|
284
|
+
// clickCamundaApps has to hunt for its trigger, the latter alone
|
|
285
|
+
// exceeding this test's 36-minute test.slow() budget. 4 x 120s measures
|
|
286
|
+
// at 581s, which is the honest equivalent of the 600s sleep, and four
|
|
287
|
+
// attempts is ample for the page.goto('/') recovery below (the helper's
|
|
288
|
+
// own default is 3). It still rethrows once exhausted, so a cache that
|
|
289
|
+
// never refreshes fails here rather than further downstream.
|
|
290
|
+
//
|
|
291
|
+
// openOptimizeWithRetry rather than a hand-rolled poll because it carries
|
|
292
|
+
// the recovery this needs: a failed hand-off can leave the page on
|
|
293
|
+
// /login?error, where clickCamundaApps() silently returns (it exits its
|
|
294
|
+
// loop without throwing when no app-switcher button is visible), so
|
|
295
|
+
// re-clicking would retry the same dead page until the budget ran out.
|
|
296
|
+
// openOptimizeWithRetry does page.goto('/') between attempts, which is
|
|
297
|
+
// the only thing that gets back to a usable state.
|
|
298
|
+
await appsPage.openOptimizeWithRetry(optimizeHomePage.optimizeBanner, clusterName, 120000, 4);
|
|
249
299
|
});
|
|
250
300
|
await _8_9_1.test.step('Navigate to Console and Ensure Apps are Available in App Switcher', async () => {
|
|
251
301
|
await appsPage.clickCamundaApps();
|