@camunda/e2e-test-suite 0.0.1231 → 0.0.1233
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/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/8.10/ModelerCreatePage.js +30 -12
- package/dist/pages/8.10/OptimizeCollectionsPage.js +3 -1
- package/dist/pages/8.10/OptimizeDashboardPage.js +7 -1
- package/dist/pages/8.10/OptimizeHomePage.js +12 -2
- package/dist/pages/8.8/FormJsPage.js +5 -0
- package/dist/pages/8.8/ModelerCreatePage.js +13 -0
- package/dist/pages/8.8/ModelerHomePage.js +46 -3
- package/dist/pages/8.8/ProcessApplicationPage.js +12 -0
- package/dist/resources/agentic_ai/ai-agent-chat-with-tools_gemini_vertex.bpmn +1 -1
- package/dist/utils/sessionRecovery.d.ts +40 -0
- package/dist/utils/sessionRecovery.js +83 -1
- package/package.json +1 -1
- package/resources/agentic_ai/ai-agent-chat-with-tools_gemini_vertex.bpmn +1 -1
|
@@ -225,6 +225,7 @@ declare class ModelerCreatePage {
|
|
|
225
225
|
embedForm(formName: string): Promise<void>;
|
|
226
226
|
openDetailsPanelIfCollapsed(): Promise<void>;
|
|
227
227
|
fillStartEventVariablesForDocHandling(zeebeUrl: string, zeebeClientId: string, zeebeClientSecret: string): Promise<void>;
|
|
228
|
+
private expandOutputMappingEntry;
|
|
228
229
|
fillZeebeClientId(clientId: string): Promise<void>;
|
|
229
230
|
fillZeebeSecretValue(zeebeSecret: string): Promise<void>;
|
|
230
231
|
fillZeebeRestValue(zeebeUrl: string): Promise<void>;
|
|
@@ -1856,11 +1856,35 @@ class ModelerCreatePage {
|
|
|
1856
1856
|
await this.fillZeebeClientId(zeebeClientId);
|
|
1857
1857
|
await (0, sleep_1.sleep)(1000);
|
|
1858
1858
|
}
|
|
1859
|
+
// Each Output mapping row is a collapsible properties-panel entry whose
|
|
1860
|
+
// header *toggles* on click, so a single blind click is not idempotent: when
|
|
1861
|
+
// the panel re-renders while the click is in flight — it does, right after
|
|
1862
|
+
// the preceding FEEL editor commits its value — the entry ends up collapsed
|
|
1863
|
+
// again and its editor keeps a zero-size box. That is what reported
|
|
1864
|
+
// `expect(#bio-properties-panel-StartEvent_1-output-2-source div)
|
|
1865
|
+
// .toBeVisible()` timing out with "unexpected value hidden" for a full 60s
|
|
1866
|
+
// while the trace screenshot showed zeebeRestAdress and zeebeSecret open and
|
|
1867
|
+
// zeebeClientId collapsed. Ask for the state instead of assuming it: only
|
|
1868
|
+
// click while the editor is still hidden, and re-check after each click.
|
|
1869
|
+
async expandOutputMappingEntry(header, editor) {
|
|
1870
|
+
const maxAttempts = 5;
|
|
1871
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
1872
|
+
if (await editor.isVisible().catch(() => false)) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
await header.click();
|
|
1876
|
+
const opened = await editor
|
|
1877
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
1878
|
+
.then(() => true)
|
|
1879
|
+
.catch(() => false);
|
|
1880
|
+
if (opened) {
|
|
1881
|
+
return;
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
await (0, test_1.expect)(editor).toBeVisible({ timeout: 60000 });
|
|
1885
|
+
}
|
|
1859
1886
|
async fillZeebeClientId(clientId) {
|
|
1860
|
-
await this.zeebeClientIdVariable.
|
|
1861
|
-
await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toBeVisible({
|
|
1862
|
-
timeout: 60000,
|
|
1863
|
-
});
|
|
1887
|
+
await this.expandOutputMappingEntry(this.zeebeClientIdVariable, this.variableAssignmentValueTextboxThirdVariable);
|
|
1864
1888
|
await this.variableAssignmentValueTextboxThirdVariable.click();
|
|
1865
1889
|
await (0, sleep_1.sleep)(1000);
|
|
1866
1890
|
await this.variableAssignmentValueTextboxThirdVariable.fill('');
|
|
@@ -1868,10 +1892,7 @@ class ModelerCreatePage {
|
|
|
1868
1892
|
await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toHaveText(clientId);
|
|
1869
1893
|
}
|
|
1870
1894
|
async fillZeebeSecretValue(zeebeSecret) {
|
|
1871
|
-
await this.zeebeSecretVariable.
|
|
1872
|
-
await (0, test_1.expect)(this.variableAssignmentValueTextboxSecondVariable).toBeVisible({
|
|
1873
|
-
timeout: 60000,
|
|
1874
|
-
});
|
|
1895
|
+
await this.expandOutputMappingEntry(this.zeebeSecretVariable, this.variableAssignmentValueTextboxSecondVariable);
|
|
1875
1896
|
await this.variableAssignmentValueTextboxSecondVariable.click();
|
|
1876
1897
|
await (0, sleep_1.sleep)(1000);
|
|
1877
1898
|
await this.variableAssignmentValueTextboxSecondVariable.fill('');
|
|
@@ -1879,10 +1900,7 @@ class ModelerCreatePage {
|
|
|
1879
1900
|
await (0, test_1.expect)(this.variableAssignmentValueTextboxSecondVariable).toHaveText(zeebeSecret);
|
|
1880
1901
|
}
|
|
1881
1902
|
async fillZeebeRestValue(zeebeUrl) {
|
|
1882
|
-
await this.zeebeRestAddressVariable.
|
|
1883
|
-
await (0, test_1.expect)(this.variableAssignmentValueTextbox).toBeVisible({
|
|
1884
|
-
timeout: 60000,
|
|
1885
|
-
});
|
|
1903
|
+
await this.expandOutputMappingEntry(this.zeebeRestAddressVariable, this.variableAssignmentValueTextbox);
|
|
1886
1904
|
await this.variableAssignmentValueTextbox.click();
|
|
1887
1905
|
await (0, sleep_1.sleep)(1000);
|
|
1888
1906
|
await this.variableAssignmentValueTextbox.fill('');
|
|
@@ -33,8 +33,10 @@ class OptimizeCollectionsPage {
|
|
|
33
33
|
.or(page.getByRole('button', { name: 'Collection', exact: true }));
|
|
34
34
|
this.modalCloseButton = page.locator('button').filter({ hasText: /^Close$/ });
|
|
35
35
|
this.processReport = page.getByText('Process Report').first();
|
|
36
|
+
// See OptimizeHomePage: Optimize's Design System navigation moved this
|
|
37
|
+
// link out of the top banner into the left sidebar.
|
|
36
38
|
this.collectionsLink = page
|
|
37
|
-
.getByRole('
|
|
39
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
38
40
|
.getByRole('link', { name: 'Collections' });
|
|
39
41
|
this.collectionNameInput = page
|
|
40
42
|
.getByRole('dialog')
|
|
@@ -7,7 +7,13 @@ class OptimizeDashboardPage {
|
|
|
7
7
|
filterTable;
|
|
8
8
|
constructor(page) {
|
|
9
9
|
this.page = page;
|
|
10
|
-
|
|
10
|
+
// Optimize's Design System EntityList replaced the Carbon table toolbar
|
|
11
|
+
// search (placeholder "Filter table") with a plain input whose placeholder
|
|
12
|
+
// is the `home.search.name` translation, "Search for name...". With the
|
|
13
|
+
// old placeholder `clickFilterTable` waited out its 120s on every one of
|
|
14
|
+
// the five attempts, which is what reported "Process link in Optimize
|
|
15
|
+
// assertion failed after 5 attempts".
|
|
16
|
+
this.filterTable = page.getByPlaceholder(/Search for name/i);
|
|
11
17
|
}
|
|
12
18
|
async clickFilterTable() {
|
|
13
19
|
await this.filterTable.click({ timeout: 120000 });
|
|
@@ -31,9 +31,19 @@ class OptimizeHomePage {
|
|
|
31
31
|
filterTableInput;
|
|
32
32
|
constructor(page) {
|
|
33
33
|
this.page = page;
|
|
34
|
-
|
|
34
|
+
// Optimize's Design System EntityList replaced the Carbon table toolbar
|
|
35
|
+
// search (placeholder "Filter table") with a plain input whose placeholder
|
|
36
|
+
// is the `home.search.name` translation, "Search for name...". Matching on
|
|
37
|
+
// the old placeholder resolved to nothing, so `filterTableToProcess` below
|
|
38
|
+
// silently fell through and every polling round asserted against the
|
|
39
|
+
// unfiltered — and now paginated — definition table.
|
|
40
|
+
this.filterTableInput = page.getByPlaceholder(/Search for name/i);
|
|
41
|
+
// The same navigation rollout moved Dashboards/Collections out of the top
|
|
42
|
+
// banner into a left sidebar (`<nav aria-label="Main navigation">`), and
|
|
43
|
+
// the legacy banner navigation was deleted outright, so a banner-scoped
|
|
44
|
+
// link can never resolve again.
|
|
35
45
|
this.collectionsLink = page
|
|
36
|
-
.getByRole('
|
|
46
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
37
47
|
.getByRole('link', { name: 'Collections' });
|
|
38
48
|
this.dashboardLink = page.getByRole('link', { name: 'Dashboards' });
|
|
39
49
|
this.modalCloseButton = page.locator('button').filter({ hasText: /^Close$/ });
|
|
@@ -4,6 +4,7 @@ exports.FormJsPage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
7
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
7
8
|
class FormJsPage {
|
|
8
9
|
page;
|
|
9
10
|
processApplicationPage;
|
|
@@ -171,6 +172,10 @@ class FormJsPage {
|
|
|
171
172
|
});
|
|
172
173
|
}
|
|
173
174
|
async clickDeployButton() {
|
|
175
|
+
// ensureStageCluster navigates away and back before every deploy, so the
|
|
176
|
+
// action bar this button lives in may belong to a page that never
|
|
177
|
+
// rendered. Recover before spending the click budget on it.
|
|
178
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
174
179
|
await this.deployButton.click({ timeout: 60000 });
|
|
175
180
|
}
|
|
176
181
|
async clickDeploySubButton() {
|
|
@@ -6,6 +6,7 @@ const sleep_1 = require("../../utils/sleep");
|
|
|
6
6
|
const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
|
|
7
7
|
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
8
8
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
9
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
9
10
|
class ModelerCreatePage {
|
|
10
11
|
page;
|
|
11
12
|
processApplicationPage;
|
|
@@ -583,6 +584,12 @@ class ModelerCreatePage {
|
|
|
583
584
|
await (0, test_1.expect)(this.viewProcessLink).toBeHidden({ timeout: 30000 });
|
|
584
585
|
}
|
|
585
586
|
async clickStartInstanceMainButton() {
|
|
587
|
+
// ensureStageCluster runs immediately before this on every runProcessInstance
|
|
588
|
+
// attempt and navigates twice, so the diagram's action bar may belong to a
|
|
589
|
+
// page Modeler never rendered. Without this, each of the 10 retries burned
|
|
590
|
+
// 60s of probes here and the run surfaced as "View process instance" never
|
|
591
|
+
// appearing -- which reads as a failed deploy rather than a dead page.
|
|
592
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
586
593
|
if (await this.startInstanceMainButton.isVisible({ timeout: 30000 })) {
|
|
587
594
|
await this.startInstanceMainButton.click({ timeout: 30000 });
|
|
588
595
|
}
|
|
@@ -754,6 +761,11 @@ class ModelerCreatePage {
|
|
|
754
761
|
await this.nameInput.fill(name, { timeout: 60000 });
|
|
755
762
|
}
|
|
756
763
|
async clickDiagramBreadcrumb() {
|
|
764
|
+
// Reached straight after opening a diagram, so the navigation that got us
|
|
765
|
+
// here is the one Modeler can answer with its hung /login. The breadcrumb
|
|
766
|
+
// never renders there, and at 10s this was the first thing to notice --
|
|
767
|
+
// as "breadcrumb not found", which says nothing about the real cause.
|
|
768
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
757
769
|
await this.diagramBreadcrumb.click({ timeout: 10000 });
|
|
758
770
|
}
|
|
759
771
|
// Opening the breadcrumb menu leaves the pointer on the breadcrumb button,
|
|
@@ -1631,6 +1643,7 @@ class ModelerCreatePage {
|
|
|
1631
1643
|
}
|
|
1632
1644
|
async fillStartEventVariablesForDocHandling(zeebeUrl, zeebeClientId, zeebeClientSecret) {
|
|
1633
1645
|
console.log(`URL: ${zeebeUrl}, ClientId: ${zeebeClientId}, Secret: ${zeebeClientSecret}`);
|
|
1646
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
1634
1647
|
await (0, test_1.expect)(this.connectorsDocHandlingStartEventElement).toBeVisible({
|
|
1635
1648
|
timeout: 60000,
|
|
1636
1649
|
});
|
|
@@ -282,11 +282,27 @@ class ModelerHomePage {
|
|
|
282
282
|
// lands mid-outage used to lose the rest of its budget), and when the backend
|
|
283
283
|
// stays down the wait is spent on the thing the test is actually waiting for.
|
|
284
284
|
async recoverModelerSession() {
|
|
285
|
-
|
|
285
|
+
// Two distinct ways Modeler loses its session, and only one of them shows
|
|
286
|
+
// the identity provider. When POST /api/internal/login 5xx's during the
|
|
287
|
+
// SPA's own sign-in it never logs out at all -- it parks on Modeler's
|
|
288
|
+
// /login route behind a spinner that nothing retries -- so the
|
|
289
|
+
// credentials path below cannot see it. Un-stick that first.
|
|
290
|
+
const unstuck = await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
291
|
+
if (!(await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page)) && !unstuck) {
|
|
286
292
|
this.consecutiveSessionRecoveries = 0;
|
|
287
293
|
return false;
|
|
288
294
|
}
|
|
289
|
-
|
|
295
|
+
// Counted, not just reset: sessionDropError reports this number, and
|
|
296
|
+
// without the increment the counter read 0 forever, so the `> 0` guard at
|
|
297
|
+
// every call site was unreachable and the dropped-session diagnosis it
|
|
298
|
+
// exists to surface never once printed.
|
|
299
|
+
this.consecutiveSessionRecoveries++;
|
|
300
|
+
// The banner alone does not prove the session came back: Modeler renders
|
|
301
|
+
// its whole app shell, banner included, over the spinner on the hung
|
|
302
|
+
// /login route. Requiring the browser to have LEFT that route is what
|
|
303
|
+
// makes "recovered" mean recovered.
|
|
304
|
+
const shellVisible = !(0, sessionRecovery_1.isAtModelerLoginRoute)(this.page) &&
|
|
305
|
+
(await this.becameVisible(this.modelerPageBanner, 120000));
|
|
290
306
|
if (shellVisible) {
|
|
291
307
|
this.consecutiveSessionRecoveries = 0;
|
|
292
308
|
}
|
|
@@ -386,6 +402,10 @@ class ModelerHomePage {
|
|
|
386
402
|
// Reload so the check reflects the PERSISTED server state, not the
|
|
387
403
|
// optimistic client render that can silently revert to "New project".
|
|
388
404
|
await this.page.reload();
|
|
405
|
+
// A reload is also how the browser ends up on Modeler's hung /login, and
|
|
406
|
+
// the committed name can never render there -- which would read as "the
|
|
407
|
+
// rename did not persist" rather than "the page never loaded".
|
|
408
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
389
409
|
await (0, test_1.expect)(committedProjectName.first()).toBeVisible({ timeout: 15000 });
|
|
390
410
|
}).toPass({ timeout: 120000 });
|
|
391
411
|
}
|
|
@@ -413,6 +433,7 @@ class ModelerHomePage {
|
|
|
413
433
|
// resource is one level down, so re-enter the container first.
|
|
414
434
|
async clickProcessDiagram(name) {
|
|
415
435
|
await this.ensureInResourceContainer();
|
|
436
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
416
437
|
const process = this.rows.filter({ hasText: name }).getByTitle(name).first();
|
|
417
438
|
await (0, test_1.expect)(process).toBeVisible({ timeout: 90000 });
|
|
418
439
|
await process.click();
|
|
@@ -426,6 +447,7 @@ class ModelerHomePage {
|
|
|
426
447
|
const dropdown = this.processApplicationPage.createNewDropdown;
|
|
427
448
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
428
449
|
try {
|
|
450
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
429
451
|
await this.dismissOverlays();
|
|
430
452
|
await (0, test_1.expect)(dropdown).toBeVisible({
|
|
431
453
|
timeout: 45000,
|
|
@@ -452,6 +474,7 @@ class ModelerHomePage {
|
|
|
452
474
|
async clickWorkspaceDiagramTypeDropdown() {
|
|
453
475
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
454
476
|
try {
|
|
477
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
455
478
|
await this.dismissOverlays();
|
|
456
479
|
await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
|
|
457
480
|
timeout: 45000,
|
|
@@ -483,7 +506,21 @@ class ModelerHomePage {
|
|
|
483
506
|
if (await this.becameVisible(this.diagramPropertiesPanel, 1000)) {
|
|
484
507
|
return;
|
|
485
508
|
}
|
|
486
|
-
|
|
509
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
510
|
+
const menuOpen = await this.becameVisible(this.bpmnTemplateOption, 5000);
|
|
511
|
+
// Hub disables this item while its own create-file request is in flight
|
|
512
|
+
// (`disabled={creatingType === BPMN}` in CreateActionDs). A request that
|
|
513
|
+
// never answers leaves it disabled for good -- yet still visible, so the
|
|
514
|
+
// probe above passed and the click below then spent its whole 30s on
|
|
515
|
+
// "element is not enabled" on every single attempt until the test timed
|
|
516
|
+
// out. Only a reload clears that React state; the menu is closed by it,
|
|
517
|
+
// which is why clickDiagramTypeDropdown has to reopen it.
|
|
518
|
+
const stuckDisabled = menuOpen &&
|
|
519
|
+
!(await this.bpmnTemplateOption.isEnabled().catch(() => false));
|
|
520
|
+
if (stuckDisabled) {
|
|
521
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
522
|
+
}
|
|
523
|
+
if (!menuOpen || stuckDisabled) {
|
|
487
524
|
await this.clickDiagramTypeDropdown();
|
|
488
525
|
}
|
|
489
526
|
// 30s rather than the old 120s: the option is proven visible above, so a
|
|
@@ -493,9 +530,15 @@ class ModelerHomePage {
|
|
|
493
530
|
}).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
|
|
494
531
|
}
|
|
495
532
|
async clickFormOption() {
|
|
533
|
+
// The create menu only exists on a rendered container page. When the
|
|
534
|
+
// navigation that got us here bounced to Modeler's hung /login, this
|
|
535
|
+
// option is not merely late -- it is on a page that never renders -- so
|
|
536
|
+
// recover before spending the click budget looking for it.
|
|
537
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
496
538
|
await this.formTemplateOption.click();
|
|
497
539
|
}
|
|
498
540
|
async enterFormName(name) {
|
|
541
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
499
542
|
await (0, test_1.expect)(this.formNameInput).toBeVisible({ timeout: 120000 });
|
|
500
543
|
await this.formNameInput.click({ timeout: 60000, force: true });
|
|
501
544
|
await this.formNameInput.fill(name, { timeout: 60000 });
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ProcessApplicationPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
|
|
6
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
6
7
|
// Which process application holds the current test's resources, and which
|
|
7
8
|
// cluster its development stage currently points at. Keyed by the Playwright
|
|
8
9
|
// Page because the object that creates the container (ModelerHomePage) and the
|
|
@@ -92,6 +93,12 @@ class ProcessApplicationPage {
|
|
|
92
93
|
return this.folderExplorer.or(this.breadcrumb).first();
|
|
93
94
|
}
|
|
94
95
|
async waitForProcessApplication(timeout = 90000) {
|
|
96
|
+
// Every route into this page is a navigation, and a navigation is exactly
|
|
97
|
+
// when Modeler can bounce the browser onto its own hung /login. Neither
|
|
98
|
+
// the folder explorer nor the breadcrumb can ever render there, so waiting
|
|
99
|
+
// the full timeout out first would only delay the recovery that has to
|
|
100
|
+
// happen anyway. No-ops when the page is where it should be.
|
|
101
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
95
102
|
await (0, test_1.expect)(this.pageReady()).toBeVisible({ timeout });
|
|
96
103
|
}
|
|
97
104
|
async clickCreateProcessApplicationButton() {
|
|
@@ -208,11 +215,16 @@ class ProcessApplicationPage {
|
|
|
208
215
|
await this.page.goto(`${paUrl}/connected-clusters`, {
|
|
209
216
|
waitUntil: 'domcontentloaded',
|
|
210
217
|
});
|
|
218
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
211
219
|
await this.openStageDefinition();
|
|
212
220
|
await this.defineDevelopmentStage(clusterName);
|
|
213
221
|
}).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
|
|
214
222
|
container.developmentCluster = clusterName;
|
|
215
223
|
await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
|
|
224
|
+
// Hand the caller back a page it can act on: this navigation can bounce to
|
|
225
|
+
// the hung /login just as easily as the one above, and the caller goes
|
|
226
|
+
// straight into a deploy dialog that would never open there.
|
|
227
|
+
await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
216
228
|
return true;
|
|
217
229
|
}
|
|
218
230
|
// The /connected-clusters deep link opens the Define stages dialog by itself
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
source="You are a helpful, generic chat agent which can answer a wide amount of questions based on your knowledge and an optional set of available tools. If tools are provided, you should prefer them instead of guessing an answer. You can call the same tool multiple times by providing different input values. Don't guess any tools which were not explicitely configured. If no tool matches the request, try to generate an answer. If you're not able to find a good answer, return with a message stating why you're not able to. If you are prompted to interact with a person, never guess contact details, but use available user/person lookup tools instead and return with an error if you're not able to look up appropriate data. Thinking, step by step, before you execute your tools, you think using the template `<thinking><context></context><reflection></reflection></thinking>`"
|
|
44
44
|
target="data.systemPrompt.prompt" />
|
|
45
45
|
<zeebe:input
|
|
46
|
-
source="="Generate a single sentence in the following exact format, using
|
|
46
|
+
source="="Generate a single sentence in the following exact format, always using a new random city and country each time: The current date and time in {City, Country}, is approximately {h:mm AM/PM} on {Month day, year}. But only with the sentence/format as above, nothing else""
|
|
47
47
|
target="data.userPrompt.prompt" />
|
|
48
48
|
<zeebe:input
|
|
49
49
|
source="=if (is defined(followUpInput) or is defined(followUpDocuments)) then followUpDocuments else inputDocuments"
|
|
@@ -31,3 +31,43 @@ export declare function isAtIdentityProvider(page: Page): boolean;
|
|
|
31
31
|
* needed here.
|
|
32
32
|
*/
|
|
33
33
|
export declare function restoreSessionIfLoggedOut(page: Page): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* Whether the browser is parked on Web Modeler's OWN `/login` route, as
|
|
36
|
+
* opposed to the identity provider's login form.
|
|
37
|
+
*
|
|
38
|
+
* Two different failure states that look alike in a screenshot and need
|
|
39
|
+
* opposite recoveries, so they are kept apart deliberately. The identity
|
|
40
|
+
* provider is a separate HOST (`weblogin.`/`login.`) and wants credentials
|
|
41
|
+
* typed into it -- that is {@link restoreSessionIfLoggedOut}. This one is a
|
|
42
|
+
* PATH on the application's own host, and wants a re-navigation.
|
|
43
|
+
*/
|
|
44
|
+
export declare function isAtModelerLoginRoute(page: Page): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Un-sticks Web Modeler when its own `/login` route has hung, and reports
|
|
47
|
+
* whether it had to.
|
|
48
|
+
*
|
|
49
|
+
* Modeler establishes its session by POSTing `/api/internal/login` from
|
|
50
|
+
* `prepareSession()`. When that call 5xx's the promise rejects and nothing
|
|
51
|
+
* catches it: `history.push(returnTo)` never runs, the app never becomes
|
|
52
|
+
* ready, and the SPA sits on `/login?returnUrl=...` rendering its shell over
|
|
53
|
+
* an infinite spinner. There is no client-side retry and no error state, so
|
|
54
|
+
* the page cannot recover on its own -- every Modeler locator the test waits
|
|
55
|
+
* for simply never appears, and the test spends its entire budget on a page
|
|
56
|
+
* that will never render. The 2026-09-22 nightly lost 22 tests across nine
|
|
57
|
+
* specs to one intermittent backend this way, each reported as an unrelated
|
|
58
|
+
* "locator not found".
|
|
59
|
+
*
|
|
60
|
+
* Reloading re-runs `prepareSession()`, so it re-issues the POST; when that
|
|
61
|
+
* one succeeds the SPA pushes the `returnUrl` it kept and the browser lands on
|
|
62
|
+
* the page the caller was navigating to. Retried because the backend fails
|
|
63
|
+
* intermittently rather than continuously -- on that nightly roughly half the
|
|
64
|
+
* calls answered 200, so a few spaced attempts are what get through. The
|
|
65
|
+
* spacing is the `waitForURL` budget itself rather than a sleep: it returns
|
|
66
|
+
* the moment the session is accepted, and otherwise spends the wait backing
|
|
67
|
+
* off.
|
|
68
|
+
*
|
|
69
|
+
* Cheap to call speculatively -- a page that is not on `/login` costs one URL
|
|
70
|
+
* comparison and no DOM access, which is what lets the Modeler page objects
|
|
71
|
+
* guard their navigations with it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function recoverStuckModelerLogin(page: Page, attempts?: number, settleTimeout?: number): Promise<boolean>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.restoreSessionIfLoggedOut = exports.isAtIdentityProvider = exports.trackSignedInUser = void 0;
|
|
3
|
+
exports.recoverStuckModelerLogin = exports.isAtModelerLoginRoute = exports.restoreSessionIfLoggedOut = exports.isAtIdentityProvider = exports.trackSignedInUser = void 0;
|
|
4
4
|
// Only the username is kept -- no password, no cookie, no token. Keyed by the
|
|
5
5
|
// Playwright Page because the object that logs in (LoginPage) and the objects
|
|
6
6
|
// that later have to recover a dropped session (ModelerHomePage) are separate
|
|
@@ -142,3 +142,85 @@ async function restoreSessionIfLoggedOut(page) {
|
|
|
142
142
|
return true;
|
|
143
143
|
}
|
|
144
144
|
exports.restoreSessionIfLoggedOut = restoreSessionIfLoggedOut;
|
|
145
|
+
const LOGIN_ROUTE = /\/login\/?$/;
|
|
146
|
+
/**
|
|
147
|
+
* Whether the browser is parked on Web Modeler's OWN `/login` route, as
|
|
148
|
+
* opposed to the identity provider's login form.
|
|
149
|
+
*
|
|
150
|
+
* Two different failure states that look alike in a screenshot and need
|
|
151
|
+
* opposite recoveries, so they are kept apart deliberately. The identity
|
|
152
|
+
* provider is a separate HOST (`weblogin.`/`login.`) and wants credentials
|
|
153
|
+
* typed into it -- that is {@link restoreSessionIfLoggedOut}. This one is a
|
|
154
|
+
* PATH on the application's own host, and wants a re-navigation.
|
|
155
|
+
*/
|
|
156
|
+
function isAtModelerLoginRoute(page) {
|
|
157
|
+
if (isAtIdentityProvider(page)) {
|
|
158
|
+
return false;
|
|
159
|
+
}
|
|
160
|
+
try {
|
|
161
|
+
return LOGIN_ROUTE.test(new URL(page.url()).pathname);
|
|
162
|
+
}
|
|
163
|
+
catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.isAtModelerLoginRoute = isAtModelerLoginRoute;
|
|
168
|
+
/**
|
|
169
|
+
* Un-sticks Web Modeler when its own `/login` route has hung, and reports
|
|
170
|
+
* whether it had to.
|
|
171
|
+
*
|
|
172
|
+
* Modeler establishes its session by POSTing `/api/internal/login` from
|
|
173
|
+
* `prepareSession()`. When that call 5xx's the promise rejects and nothing
|
|
174
|
+
* catches it: `history.push(returnTo)` never runs, the app never becomes
|
|
175
|
+
* ready, and the SPA sits on `/login?returnUrl=...` rendering its shell over
|
|
176
|
+
* an infinite spinner. There is no client-side retry and no error state, so
|
|
177
|
+
* the page cannot recover on its own -- every Modeler locator the test waits
|
|
178
|
+
* for simply never appears, and the test spends its entire budget on a page
|
|
179
|
+
* that will never render. The 2026-09-22 nightly lost 22 tests across nine
|
|
180
|
+
* specs to one intermittent backend this way, each reported as an unrelated
|
|
181
|
+
* "locator not found".
|
|
182
|
+
*
|
|
183
|
+
* Reloading re-runs `prepareSession()`, so it re-issues the POST; when that
|
|
184
|
+
* one succeeds the SPA pushes the `returnUrl` it kept and the browser lands on
|
|
185
|
+
* the page the caller was navigating to. Retried because the backend fails
|
|
186
|
+
* intermittently rather than continuously -- on that nightly roughly half the
|
|
187
|
+
* calls answered 200, so a few spaced attempts are what get through. The
|
|
188
|
+
* spacing is the `waitForURL` budget itself rather than a sleep: it returns
|
|
189
|
+
* the moment the session is accepted, and otherwise spends the wait backing
|
|
190
|
+
* off.
|
|
191
|
+
*
|
|
192
|
+
* Cheap to call speculatively -- a page that is not on `/login` costs one URL
|
|
193
|
+
* comparison and no DOM access, which is what lets the Modeler page objects
|
|
194
|
+
* guard their navigations with it.
|
|
195
|
+
*/
|
|
196
|
+
async function recoverStuckModelerLogin(page, attempts = 4, settleTimeout = 25000) {
|
|
197
|
+
if (!isAtModelerLoginRoute(page)) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
201
|
+
console.warn(`recoverStuckModelerLogin: Modeler is stuck on ${page.url()} -- its ` +
|
|
202
|
+
`POST /api/internal/login is failing. Reload ${attempt}/${attempts}.`);
|
|
203
|
+
await page
|
|
204
|
+
.reload({ waitUntil: 'domcontentloaded', timeout: 60000 })
|
|
205
|
+
.catch(() => undefined);
|
|
206
|
+
// The reload can land on the identity provider instead -- Modeler calls
|
|
207
|
+
// Auth0 logout on some of these failures -- and there credentials, not
|
|
208
|
+
// another reload, are what gets us back.
|
|
209
|
+
await restoreSessionIfLoggedOut(page);
|
|
210
|
+
// Leaving /login is the signal that the session was accepted: the SPA only
|
|
211
|
+
// pushes the returnUrl once prepareSession() resolved.
|
|
212
|
+
const recovered = await page
|
|
213
|
+
.waitForURL((url) => !LOGIN_ROUTE.test(url.pathname), {
|
|
214
|
+
timeout: settleTimeout,
|
|
215
|
+
})
|
|
216
|
+
.then(() => true)
|
|
217
|
+
.catch(() => false);
|
|
218
|
+
if (recovered) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
console.warn('recoverStuckModelerLogin: Modeler never left /login -- its backend is ' +
|
|
223
|
+
'still failing the session call.');
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
exports.recoverStuckModelerLogin = recoverStuckModelerLogin;
|
package/package.json
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
source="You are a helpful, generic chat agent which can answer a wide amount of questions based on your knowledge and an optional set of available tools. If tools are provided, you should prefer them instead of guessing an answer. You can call the same tool multiple times by providing different input values. Don't guess any tools which were not explicitely configured. If no tool matches the request, try to generate an answer. If you're not able to find a good answer, return with a message stating why you're not able to. If you are prompted to interact with a person, never guess contact details, but use available user/person lookup tools instead and return with an error if you're not able to look up appropriate data. Thinking, step by step, before you execute your tools, you think using the template `<thinking><context></context><reflection></reflection></thinking>`"
|
|
44
44
|
target="data.systemPrompt.prompt" />
|
|
45
45
|
<zeebe:input
|
|
46
|
-
source="="Generate a single sentence in the following exact format, using
|
|
46
|
+
source="="Generate a single sentence in the following exact format, always using a new random city and country each time: The current date and time in {City, Country}, is approximately {h:mm AM/PM} on {Month day, year}. But only with the sentence/format as above, nothing else""
|
|
47
47
|
target="data.userPrompt.prompt" />
|
|
48
48
|
<zeebe:input
|
|
49
49
|
source="=if (is defined(followUpInput) or is defined(followUpDocuments)) then followUpDocuments else inputDocuments"
|