@camunda/e2e-test-suite 0.0.1181 → 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.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.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/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/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.10/access-user-role-management.spec.js +52 -2
- package/dist/tests/8.8/access-user-role-management.spec.js +54 -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) {
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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();
|
|
@@ -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();
|
|
@@ -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();
|