@camunda/e2e-test-suite 0.0.944 → 0.0.945
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/8.10/AppsPage.d.ts +5 -0
- package/dist/pages/8.10/AppsPage.js +152 -0
- package/dist/pages/8.10/ModelerHomePage.d.ts +1 -0
- package/dist/pages/8.10/ModelerHomePage.js +52 -5
- package/dist/pages/8.10/OperateHomePage.js +23 -2
- package/dist/pages/8.10/OperateProcessInstancePage.d.ts +17 -3
- package/dist/pages/8.10/OperateProcessInstancePage.js +718 -16
- package/dist/pages/8.10/OperateProcessesPage.d.ts +1 -0
- package/dist/pages/8.10/OperateProcessesPage.js +38 -2
- package/dist/pages/8.10/OptimizeHomePage.d.ts +5 -0
- package/dist/pages/8.10/OptimizeHomePage.js +138 -0
- package/dist/pages/8.10/OptimizeReportPage.d.ts +1 -0
- package/dist/pages/8.10/OptimizeReportPage.js +25 -0
- package/dist/pages/8.10/TaskPanelPage.d.ts +2 -1
- package/dist/pages/8.10/TaskPanelPage.js +37 -9
- package/dist/pages/8.10/UtilitiesPage.d.ts +2 -1
- package/dist/pages/8.10/UtilitiesPage.js +155 -23
- package/dist/pages/SM-8.10/KeycloakAdminPage.js +1 -1
- package/dist/pages/SM-8.10/KeycloakLoginPage.d.ts +1 -0
- package/dist/pages/SM-8.10/KeycloakLoginPage.js +8 -1
- package/dist/pages/SM-8.10/LoginPage.d.ts +1 -1
- package/dist/pages/SM-8.10/LoginPage.js +11 -2
- package/dist/pages/SM-8.10/NavigationPage.d.ts +1 -0
- package/dist/pages/SM-8.10/NavigationPage.js +86 -8
- package/dist/pages/SM-8.10/OCIdentityHomePage.js +12 -2
- package/dist/pages/SM-8.10/OCIdentityRolesPage.d.ts +3 -0
- package/dist/pages/SM-8.10/OCIdentityRolesPage.js +110 -4
- package/dist/pages/SM-8.10/OperateHomePage.js +13 -1
- package/dist/pages/SM-8.10/OperateProcessesPage.d.ts +1 -0
- package/dist/pages/SM-8.10/OperateProcessesPage.js +37 -18
- package/dist/pages/SM-8.10/OptimizeReportPage.js +21 -1
- package/dist/pages/SM-8.10/TaskDetailsPage.js +7 -0
- package/dist/pages/SM-8.10/UtilitiesPage.js +15 -4
- package/dist/pages/SM-8.10/optimizeReportUtils.js +30 -4
- package/dist/tests/8.10/smoke-tests.spec.js +182 -44
- package/dist/utils/constants.d.ts +1 -0
- package/dist/utils/constants.js +4 -0
- package/package.json +1 -1
|
@@ -21,6 +21,7 @@ declare class OperateProcessesPage {
|
|
|
21
21
|
clickProcessIncidentsCheckbox(): Promise<void>;
|
|
22
22
|
clickRunningProcessInstancesCheckbox(): Promise<void>;
|
|
23
23
|
clickFinishedProcessInstancesCheckbox(): Promise<void>;
|
|
24
|
+
private returnToProcessesList;
|
|
24
25
|
clickProcessInstanceLink(processName: string): Promise<void>;
|
|
25
26
|
applyMoreFilters(filter: string, value: string): Promise<void>;
|
|
26
27
|
}
|
|
@@ -78,19 +78,55 @@ class OperateProcessesPage {
|
|
|
78
78
|
async clickFinishedProcessInstancesCheckbox() {
|
|
79
79
|
await this.processFinishedInstancesCheckbox.click();
|
|
80
80
|
}
|
|
81
|
+
// Puts the tab back on the processes list before the next attempt of
|
|
82
|
+
// `clickProcessInstanceLink`. A click that DID navigate to the instance page
|
|
83
|
+
// but whose "Instance History" panel was slow to render leaves the tab on
|
|
84
|
+
// `/processes/<key>`, and the instance row the loop looks for can never
|
|
85
|
+
// appear there: reloading that page spent every remaining attempt waiting
|
|
86
|
+
// 60s for a row that is not on it, which is what reported "Failed to click
|
|
87
|
+
// the link after 5 attempts" while the instance was perfectly fine.
|
|
88
|
+
// Re-opening the list URL also restores the filters the caller applied
|
|
89
|
+
// (they live in the query string), which a `goBack()` would not guarantee.
|
|
90
|
+
//
|
|
91
|
+
// Navigation errors are logged and retried rather than thrown: a reload that
|
|
92
|
+
// races the click's still-in-flight navigation fails with
|
|
93
|
+
// `net::ERR_ABORTED; maybe frame was detached?` and a DNS blip with
|
|
94
|
+
// `net::ERR_NAME_NOT_RESOLVED`. Both used to escape this loop's catch block
|
|
95
|
+
// and fail the test outright instead of being retried like every other
|
|
96
|
+
// recovery step here.
|
|
97
|
+
async returnToProcessesList(listUrl) {
|
|
98
|
+
try {
|
|
99
|
+
if (this.page.url() === listUrl) {
|
|
100
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
await this.page.goto(listUrl, { waitUntil: 'domcontentloaded' });
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
console.warn(`Could not return to the processes list: ${error}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
81
109
|
async clickProcessInstanceLink(processName) {
|
|
82
110
|
// Increased from 3 to 5 retries: process-instance indexing in Operate can take
|
|
83
111
|
// longer than 3 × (60 s visible + 10 s reload) on busy SaaS environments,
|
|
84
112
|
// causing intermittent failures in the webhook connector nightly tests.
|
|
85
113
|
const maxRetries = 5;
|
|
114
|
+
// Captured before the first click so a retry can re-open the list with the
|
|
115
|
+
// caller's filters instead of reloading whatever page the click landed on.
|
|
116
|
+
const listUrl = this.page.url();
|
|
86
117
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
87
118
|
try {
|
|
88
119
|
await (0, test_1.expect)(this.processInstanceLink(processName)).toBeVisible({
|
|
89
120
|
timeout: 60000,
|
|
90
121
|
});
|
|
91
122
|
await this.processInstanceLink(processName).click({ timeout: 60000 });
|
|
123
|
+
// The instance page is rendered from the same Zeebe -> Operate import
|
|
124
|
+
// this list was filtered on, so on a busy cluster the panel regularly
|
|
125
|
+
// needs longer than 10s to paint. That short budget turned a slow
|
|
126
|
+
// render into a full re-attempt — and every re-attempt then looked for
|
|
127
|
+
// the row on the instance page it had just opened.
|
|
92
128
|
await (0, test_1.expect)(this.page.getByText('Instance History').first()).toBeVisible({
|
|
93
|
-
timeout:
|
|
129
|
+
timeout: 60000,
|
|
94
130
|
});
|
|
95
131
|
if (await this.whatsNewPopUp.isVisible()) {
|
|
96
132
|
await this.gotItButton.click();
|
|
@@ -99,7 +135,7 @@ class OperateProcessesPage {
|
|
|
99
135
|
}
|
|
100
136
|
catch (error) {
|
|
101
137
|
console.error(`Click attempt ${retries + 1} failed: ${error}`);
|
|
102
|
-
await this.
|
|
138
|
+
await this.returnToProcessesList(listUrl);
|
|
103
139
|
await (0, sleep_1.sleep)(10000);
|
|
104
140
|
}
|
|
105
141
|
}
|
|
@@ -6,7 +6,12 @@ declare class OptimizeHomePage {
|
|
|
6
6
|
readonly modalCloseButton: Locator;
|
|
7
7
|
readonly optimizeBanner: Locator;
|
|
8
8
|
readonly noPermissionsMessage: Locator;
|
|
9
|
+
readonly filterTableInput: Locator;
|
|
9
10
|
constructor(page: Page);
|
|
11
|
+
private waitForOptimizeToRender;
|
|
12
|
+
private filterTableToProcess;
|
|
13
|
+
closeModalIfVisible(): Promise<void>;
|
|
14
|
+
assertProcessImportedWithRetry(processName: string, timeout: number, maxRetries?: number): Promise<void>;
|
|
10
15
|
clickCollectionsLink(): Promise<void>;
|
|
11
16
|
clickDashboardLink(): Promise<void>;
|
|
12
17
|
}
|
|
@@ -3,6 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.OptimizeHomePage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
|
+
// Optimize's SSO hand-off can fail on the way into the app and leave the tab on
|
|
7
|
+
// `/login?error`, which is not an Optimize page at all: nothing on it ever
|
|
8
|
+
// renders a process definition, and `page.reload()` only re-requests the very
|
|
9
|
+
// same static error page. A polling loop that reloads is therefore stuck there
|
|
10
|
+
// for its whole budget -- that is what reported "Assertion of process <name> in
|
|
11
|
+
// Optimize failed after 1200000ms of polling
|
|
12
|
+
// (url=<region>.optimize.ultrawombat.com/login?error)". Re-entering the app
|
|
13
|
+
// root instead restarts the hand-off, so the poll can get back onto the app.
|
|
14
|
+
const OPTIMIZE_LOGIN_PATH = /^\/login\/?$/;
|
|
15
|
+
function isOptimizeLoginPage(rawUrl) {
|
|
16
|
+
try {
|
|
17
|
+
return OPTIMIZE_LOGIN_PATH.test(new URL(rawUrl).pathname);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// Not a parsable URL yet (e.g. a tab still on about:blank).
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
6
24
|
class OptimizeHomePage {
|
|
7
25
|
page;
|
|
8
26
|
collectionsLink;
|
|
@@ -10,8 +28,10 @@ class OptimizeHomePage {
|
|
|
10
28
|
modalCloseButton;
|
|
11
29
|
optimizeBanner;
|
|
12
30
|
noPermissionsMessage;
|
|
31
|
+
filterTableInput;
|
|
13
32
|
constructor(page) {
|
|
14
33
|
this.page = page;
|
|
34
|
+
this.filterTableInput = page.getByPlaceholder('Filter table');
|
|
15
35
|
this.collectionsLink = page
|
|
16
36
|
.getByRole('banner')
|
|
17
37
|
.getByRole('link', { name: 'Collections' });
|
|
@@ -22,6 +42,124 @@ class OptimizeHomePage {
|
|
|
22
42
|
});
|
|
23
43
|
this.noPermissionsMessage = page.getByText('default backend - 404');
|
|
24
44
|
}
|
|
45
|
+
// The Optimize process overview renders a bounded slice of the definition
|
|
46
|
+
// table, so on an org that already holds the definitions every sibling spec
|
|
47
|
+
// deployed that night a freshly imported one can sit outside the rendered
|
|
48
|
+
// rows entirely: `getByRole('link', {name})` then never becomes visible no
|
|
49
|
+
// matter how long the loop below waits, which is exactly what reported
|
|
50
|
+
// "Assertion of process <name> in Optimize failed after 1200000ms of
|
|
51
|
+
// polling" from the app root — a full 20 minutes of reload-polling for a
|
|
52
|
+
// definition that had long since been imported. Narrowing the table to the
|
|
53
|
+
// name being waited for is what `OptimizeDashboardPage.processLinkAssertion`
|
|
54
|
+
// already does before this identical link assertion.
|
|
55
|
+
// Best effort by design: when the input is not rendered (the app is still
|
|
56
|
+
// coming up, or a round landed on a page without the table) the round falls
|
|
57
|
+
// through and asserts against the unfiltered table exactly as before.
|
|
58
|
+
// Every probe a polling round makes runs immediately after that round's
|
|
59
|
+
// `reload()` + `waitForLoadState('domcontentloaded')`, which returns while
|
|
60
|
+
// the Optimize SPA is still mounting and the document is effectively empty.
|
|
61
|
+
// `isVisible()` answers instantly, so both probes below — the onboarding
|
|
62
|
+
// modal and the table filter — answered "not there" on essentially every
|
|
63
|
+
// round: the modal was never dismissed and the table was never narrowed, and
|
|
64
|
+
// the round then spent its whole slice asserting against a list that had not
|
|
65
|
+
// rendered. Waiting for the app shell first is what makes the rest of the
|
|
66
|
+
// round mean anything. It costs nothing once Optimize is up (the wait
|
|
67
|
+
// resolves in milliseconds) and is bounded, so a round that lands on a page
|
|
68
|
+
// which never renders Optimize still falls through and reloads as before.
|
|
69
|
+
async waitForOptimizeToRender(timeout) {
|
|
70
|
+
await this.filterTableInput
|
|
71
|
+
.or(this.optimizeBanner)
|
|
72
|
+
.first()
|
|
73
|
+
.waitFor({ state: 'visible', timeout })
|
|
74
|
+
.catch(() => { });
|
|
75
|
+
}
|
|
76
|
+
async filterTableToProcess(processName) {
|
|
77
|
+
const hasFilter = await this.filterTableInput
|
|
78
|
+
.isVisible()
|
|
79
|
+
.catch(() => false);
|
|
80
|
+
if (!hasFilter) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
await this.filterTableInput.fill(processName, { timeout: 15000 });
|
|
85
|
+
await this.filterTableInput.press('Enter');
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.warn(`Could not filter the Optimize process table: ${error}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async closeModalIfVisible() {
|
|
92
|
+
if (await this.modalCloseButton.isVisible()) {
|
|
93
|
+
await this.modalCloseButton.click({ timeout: 60000 });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Optimize pulls process definitions on its own import cycle, so the only
|
|
97
|
+
// lever is patience — but the patience has to be spent re-asking, not idling.
|
|
98
|
+
// This list does not live-update: a definition that lands mid-wait is only
|
|
99
|
+
// rendered by the next reload, so a long `toBeVisible` per round just sits on
|
|
100
|
+
// a page that already answered. `timeout` x `maxRetries` is kept as the total
|
|
101
|
+
// budget and spent in short slices that each reload, which turns 10 re-asks
|
|
102
|
+
// over 20 minutes into one every ~20s. That is the same "re-ask instead of
|
|
103
|
+
// idling" change already made for the Operate import waits, and it is what
|
|
104
|
+
// ran out as "Assertion of process <name> in Optimize failed after 10
|
|
105
|
+
// attempts" whenever the import lagged behind the exporter.
|
|
106
|
+
// Each round also dismisses the onboarding modal Optimize shows on this
|
|
107
|
+
// page — it is re-rendered by every reload and covers the process list, so
|
|
108
|
+
// without closing it the remaining rounds all assert against a modal.
|
|
109
|
+
async assertProcessImportedWithRetry(processName, timeout, maxRetries = 8) {
|
|
110
|
+
const processLink = this.page.getByRole('link', { name: processName });
|
|
111
|
+
const deadline = Date.now() + timeout * maxRetries;
|
|
112
|
+
// Short enough that a definition imported mid-wait is picked up by the next
|
|
113
|
+
// reload within seconds, long enough to keep the reload rate modest.
|
|
114
|
+
const checkSlice = 20000;
|
|
115
|
+
let lastError;
|
|
116
|
+
while (Date.now() < deadline) {
|
|
117
|
+
const remaining = deadline - Date.now();
|
|
118
|
+
if (remaining <= 0) {
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
await this.waitForOptimizeToRender(Math.min(checkSlice, remaining));
|
|
123
|
+
await this.closeModalIfVisible();
|
|
124
|
+
await this.filterTableToProcess(processName);
|
|
125
|
+
await (0, test_1.expect)(processLink).toBeVisible({
|
|
126
|
+
timeout: Math.min(checkSlice, remaining),
|
|
127
|
+
});
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
lastError = error;
|
|
132
|
+
}
|
|
133
|
+
if (Date.now() >= deadline) {
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
// A reload that races an in-flight navigation fails with
|
|
137
|
+
// `net::ERR_ABORTED; maybe frame was detached?`, a DNS blip with
|
|
138
|
+
// `net::ERR_NAME_NOT_RESOLVED`. Both used to escape this polling loop
|
|
139
|
+
// and fail the test outright instead of being retried on the next round;
|
|
140
|
+
// an import that genuinely never lands still fails below.
|
|
141
|
+
try {
|
|
142
|
+
if (isOptimizeLoginPage(this.page.url())) {
|
|
143
|
+
// Off the app entirely (see the note above the class): re-enter the
|
|
144
|
+
// app root so the SSO hand-off is retried. Reloading the login page
|
|
145
|
+
// can only ever render the login page again.
|
|
146
|
+
await this.page.goto(new URL('/', this.page.url()).toString(), {
|
|
147
|
+
waitUntil: 'domcontentloaded',
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
await this.page.reload();
|
|
152
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
console.warn(`Could not reload the Optimize process list: ${error}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Assertion of process ${processName} in Optimize failed after ` +
|
|
160
|
+
`${timeout * maxRetries}ms of polling (url=${this.page.url()}): ` +
|
|
161
|
+
`${lastError}`);
|
|
162
|
+
}
|
|
25
163
|
async clickCollectionsLink() {
|
|
26
164
|
if (await this.modalCloseButton.isVisible({ timeout: 60000 })) {
|
|
27
165
|
await this.modalCloseButton.click();
|
|
@@ -40,6 +40,7 @@ declare class OptimizeReportPage {
|
|
|
40
40
|
clearReportName(): Promise<void>;
|
|
41
41
|
fillReportName(name: string): Promise<void>;
|
|
42
42
|
clickSaveButton(): Promise<void>;
|
|
43
|
+
assertUserTaskInstanceCountWithRetry(expectedCount: number, tab: Page, maxRetries?: number, timeout?: number): Promise<void>;
|
|
43
44
|
waitUntilLocatorIsVisible(locator: Locator, tab: Page): Promise<void>;
|
|
44
45
|
waitUntilProcessIsVisible(locator: Locator, tab: Page): Promise<void>;
|
|
45
46
|
clickBlankReportButton(): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.OptimizeReportPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
4
5
|
class OptimizeReportPage {
|
|
5
6
|
page;
|
|
6
7
|
processSelectionButton;
|
|
@@ -117,6 +118,30 @@ class OptimizeReportPage {
|
|
|
117
118
|
async clickSaveButton() {
|
|
118
119
|
await this.saveButton.click();
|
|
119
120
|
}
|
|
121
|
+
// Optimize imports user-task *instance* data on a later cycle than the
|
|
122
|
+
// process definition, so a report saved right after the definition lands can
|
|
123
|
+
// render with a table that is still empty or only partly filled. Poll the
|
|
124
|
+
// expected row count across reloads instead of probing visibility once and
|
|
125
|
+
// then asserting the count against whatever the first render contained --
|
|
126
|
+
// that ordering reports `toHaveCount` received 0 while the import is simply
|
|
127
|
+
// still in flight.
|
|
128
|
+
async assertUserTaskInstanceCountWithRetry(expectedCount, tab, maxRetries = 4, timeout = 60000) {
|
|
129
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
130
|
+
try {
|
|
131
|
+
await (0, test_1.expect)(this.oneUserTaskInstance).toHaveCount(expectedCount, {
|
|
132
|
+
timeout: timeout,
|
|
133
|
+
});
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
if (attempt === maxRetries - 1) {
|
|
138
|
+
throw error;
|
|
139
|
+
}
|
|
140
|
+
await tab.reload();
|
|
141
|
+
await tab.waitForLoadState('domcontentloaded');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
120
145
|
async waitUntilLocatorIsVisible(locator, tab) {
|
|
121
146
|
let elapsedTime = 0;
|
|
122
147
|
const maxWaitTimeSeconds = 120000;
|
|
@@ -10,7 +10,8 @@ declare class TaskPanelPage {
|
|
|
10
10
|
readonly expandButton: Locator;
|
|
11
11
|
readonly processesInfoText: Locator;
|
|
12
12
|
constructor(page: Page);
|
|
13
|
-
|
|
13
|
+
taskInList(name: string): Locator;
|
|
14
|
+
openTask(name: string, totalTimeout?: number): Promise<void>;
|
|
14
15
|
filterBy(option: 'All open' | 'Unassigned' | 'Assigned to me' | 'Completed'): Promise<void>;
|
|
15
16
|
taskCount(name: string): Promise<number>;
|
|
16
17
|
clickTasksTab(): Promise<void>;
|
|
@@ -30,24 +30,52 @@ class TaskPanelPage {
|
|
|
30
30
|
});
|
|
31
31
|
this.processesInfoText = page.getByText('Browse and run processes published by your organization');
|
|
32
32
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
// Tasklist's importer can lag well behind Zeebe under heavy concurrent load
|
|
34
|
+
// from the sibling chromium-v1/chromium-v2 projects sharing one cluster --
|
|
35
|
+
// trace evidence from failing runs showed a smoke-test user task still
|
|
36
|
+
// absent from "Available tasks" more than 5 minutes after the instance was
|
|
37
|
+
// already ACTIVE in Operate. The budget is therefore generous, but it is a
|
|
38
|
+
// *total* deadline rather than a fixed attempt count: an attempt count
|
|
39
|
+
// multiplies with any outer retry loop (`completeTaskWithRetry` re-enters
|
|
40
|
+
// this method 3 times), which gave one step a worst case larger than the
|
|
41
|
+
// whole test timeout, so a lagging importer killed the test on the harness
|
|
42
|
+
// clock instead of failing here with a usable error.
|
|
43
|
+
// The row for a named task inside the task list. Exposed so callers that
|
|
44
|
+
// assert on the details panel can re-select the row when a click did not
|
|
45
|
+
// take effect, rather than re-deriving the locator.
|
|
46
|
+
taskInList(name) {
|
|
47
|
+
return this.availableTasks.getByText(name, { exact: true }).first();
|
|
48
|
+
}
|
|
49
|
+
async openTask(name, totalTimeout = 1200000) {
|
|
50
|
+
const deadline = Date.now() + totalTimeout;
|
|
51
|
+
let lastError;
|
|
52
|
+
while (Date.now() < deadline) {
|
|
37
53
|
try {
|
|
38
|
-
const task = this.
|
|
39
|
-
await task.waitFor({
|
|
54
|
+
const task = this.taskInList(name);
|
|
55
|
+
await task.waitFor({
|
|
56
|
+
state: 'visible',
|
|
57
|
+
// Clamped away from 0: Playwright reads `timeout: 0` as "no timeout".
|
|
58
|
+
timeout: Math.max(1000, Math.min(120000, deadline - Date.now())),
|
|
59
|
+
});
|
|
40
60
|
await task.click({ timeout: 120000 });
|
|
41
61
|
return;
|
|
42
62
|
}
|
|
43
63
|
catch (error) {
|
|
44
|
-
|
|
45
|
-
if (
|
|
46
|
-
|
|
64
|
+
lastError = error;
|
|
65
|
+
if (Date.now() >= deadline) {
|
|
66
|
+
break;
|
|
47
67
|
}
|
|
48
68
|
await this.page.reload();
|
|
69
|
+
// Tasklist polls continuously for new tasks, so `networkidle` never
|
|
70
|
+
// fires and the wait times out at 60s -- burning the retry budget
|
|
71
|
+
// without ever re-attempting the task click. `domcontentloaded`
|
|
72
|
+
// resolves once the reloaded HTML has parsed, which is the actual
|
|
73
|
+
// signal we need before trying to find the task again.
|
|
74
|
+
await this.page.waitForLoadState('domcontentloaded', { timeout: 60000 });
|
|
49
75
|
}
|
|
50
76
|
}
|
|
77
|
+
throw new Error(`Task ${name} did not open from "Available tasks" within ` +
|
|
78
|
+
`${totalTimeout}ms: ${lastError}`);
|
|
51
79
|
}
|
|
52
80
|
async filterBy(option) {
|
|
53
81
|
try {
|
|
@@ -24,8 +24,9 @@ export declare function modelRestConnector(modelerCreatePage: ModelerCreatePage,
|
|
|
24
24
|
password: string;
|
|
25
25
|
}): Promise<void>;
|
|
26
26
|
export declare function assertLocatorVisibleWithRetry(page: Page | OperateProcessInstancePage | ModelerHomePage, locator: Locator, text: string, timeout?: number, notVisible?: boolean, maxRetries?: number, clickLocator?: Locator): Promise<void>;
|
|
27
|
+
export declare function openAppTabWithRetry(page: Page, link: Locator, timeout?: number, maxRetries?: number, clickTimeout?: number): Promise<Page>;
|
|
27
28
|
export declare function assertPageTextWithRetry(page: Page, text: string, notVisible?: boolean, timeout?: number, maxRetries?: number): Promise<void>;
|
|
28
|
-
export declare function completeTaskWithRetry(taskPanelPage: TaskPanelPage, taskDetailsPage: TaskDetailsPage, taskName: string, taskPriority: string, maxRetries?: number): Promise<void>;
|
|
29
|
+
export declare function completeTaskWithRetry(taskPanelPage: TaskPanelPage, taskDetailsPage: TaskDetailsPage, taskName: string, taskPriority: string, maxRetries?: number, totalTimeout?: number): Promise<void>;
|
|
29
30
|
export declare function runProcess(clusterName: string, page: Page, modelerHomePage: ModelerHomePage, modelerCreatePage: ModelerCreatePage, processName: string, processId?: string, uploadFromFile?: string): Promise<void>;
|
|
30
31
|
export declare function runMultipleProcesses(clusterName: string, page: Page, modelerHomePage: ModelerHomePage, modelerCreatePage: ModelerCreatePage, options?: {
|
|
31
32
|
numberOfProcesses?: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.assertLatestAlertEmail = exports.clickInvitationLinkInEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.modelDiagramFromFile = exports.loginWithRetry = void 0;
|
|
3
|
+
exports.waitForLoadingToFinish = exports.assertTestUsesCorrectOrganizationFromModeler = exports.expectCountToBeOneOf = exports.createAPIClient = exports.modelAndRunConnectorsTimerEventDiagram = exports.modelAndRunConnectorsDocHandlingDiagram = exports.deleteCluster = exports.deleteAllUserGroups = exports.assertTestUsesCorrectOrganization = exports.assertLatestAlertEmail = exports.clickInvitationLinkInEmail = exports.enableAuthorizations = exports.disableRBA = exports.runMultipleProcesses = exports.runProcess = exports.completeTaskWithRetry = exports.assertPageTextWithRetry = exports.openAppTabWithRetry = exports.assertLocatorVisibleWithRetry = exports.modelRestConnector = exports.modelDiagramFromFile = exports.loginWithRetry = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const ModelerHomePage_1 = require("./ModelerHomePage");
|
|
6
6
|
const HomePage_1 = require("./HomePage");
|
|
@@ -8,6 +8,85 @@ const sleep_1 = require("../../utils/sleep");
|
|
|
8
8
|
const randomSleep_1 = require("../../utils/randomSleep");
|
|
9
9
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
10
10
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
11
|
+
async function clearWebStorage(page) {
|
|
12
|
+
await page
|
|
13
|
+
.evaluate(() => {
|
|
14
|
+
try {
|
|
15
|
+
localStorage.clear();
|
|
16
|
+
}
|
|
17
|
+
catch (_) {
|
|
18
|
+
// ignore — storage may be unavailable in some contexts
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
sessionStorage.clear();
|
|
22
|
+
}
|
|
23
|
+
catch (_) {
|
|
24
|
+
// ignore — storage may be unavailable in some contexts
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
.catch(() => { });
|
|
28
|
+
}
|
|
29
|
+
// Waits for the auth0 login form, re-asking Console for the auth redirect
|
|
30
|
+
// between checks instead of idling on whatever page the browser landed on.
|
|
31
|
+
//
|
|
32
|
+
// A single passive `expect.poll` cannot recover the two ways this hand-off
|
|
33
|
+
// fails: Console swallowing the redirect (the tab stays on a page that will
|
|
34
|
+
// never render a login form), or a session that outlived the cookie wipe (the
|
|
35
|
+
// authenticated shell renders instead). In both cases nothing changes on its
|
|
36
|
+
// own, so the poll burns its whole budget and the login attempt is lost --
|
|
37
|
+
// three of those in a row is exactly the reported "Login failed after 3
|
|
38
|
+
// attempts: ... Timeout 60000ms exceeded while waiting on the predicate".
|
|
39
|
+
// Re-navigating to '/' re-triggers the redirect, and dropping cookies +
|
|
40
|
+
// storage first evicts a surviving session, so the hand-off self-heals inside
|
|
41
|
+
// the attempt rather than costing one.
|
|
42
|
+
async function waitForLoginForm(page, loginPage, totalTimeout = 90000, roundTimeout = 20000) {
|
|
43
|
+
const deadline = Date.now() + totalTimeout;
|
|
44
|
+
const homePage = new HomePage_1.HomePage(page);
|
|
45
|
+
let lastState = 'no login form';
|
|
46
|
+
while (Date.now() < deadline) {
|
|
47
|
+
const round = Math.min(roundTimeout, deadline - Date.now());
|
|
48
|
+
try {
|
|
49
|
+
// Confirm we reached the login page before filling credentials.
|
|
50
|
+
// Without this check the locator for 'Email address' can match a hidden
|
|
51
|
+
// invite-user input on the Console page when the logout redirect has not
|
|
52
|
+
// yet completed, causing a 200 s wait before failing.
|
|
53
|
+
await test_1.expect
|
|
54
|
+
.poll(async () => {
|
|
55
|
+
const [loginMessageVisible, passwordHeadingVisible, usernameVisible, passwordVisible,] = await Promise.all([
|
|
56
|
+
loginPage.loginMessage.isVisible().catch(() => false),
|
|
57
|
+
loginPage.passwordHeading.isVisible().catch(() => false),
|
|
58
|
+
loginPage.usernameInput.locator.isVisible().catch(() => false),
|
|
59
|
+
loginPage.passwordInput.isVisible().catch(() => false),
|
|
60
|
+
]);
|
|
61
|
+
return (loginMessageVisible ||
|
|
62
|
+
passwordHeadingVisible ||
|
|
63
|
+
usernameVisible ||
|
|
64
|
+
passwordVisible);
|
|
65
|
+
}, { timeout: round })
|
|
66
|
+
.toBe(true);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// No login form this round — establish why, then re-ask for the redirect.
|
|
71
|
+
lastState = `no login form on ${page.url()}`;
|
|
72
|
+
}
|
|
73
|
+
const authenticated = await homePage.camundaComponentsButton
|
|
74
|
+
.isVisible()
|
|
75
|
+
.catch(() => false);
|
|
76
|
+
if (authenticated) {
|
|
77
|
+
lastState = 'an authenticated Console shell — no auth0 hand-off happened';
|
|
78
|
+
// The session survived the cookie wipe done before this wait; drop it
|
|
79
|
+
// again so the navigation below has to go through auth0.
|
|
80
|
+
await page.context().clearCookies();
|
|
81
|
+
await clearWebStorage(page);
|
|
82
|
+
}
|
|
83
|
+
if (Date.now() >= deadline) {
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
await page.goto('/', { waitUntil: 'domcontentloaded', timeout: 60000 });
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`Auth0 login form did not render within ${totalTimeout}ms: ${lastState}`);
|
|
89
|
+
}
|
|
11
90
|
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3) {
|
|
12
91
|
let lastError;
|
|
13
92
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
@@ -59,24 +138,7 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 3
|
|
|
59
138
|
.waitForLoadState('networkidle', { timeout: 30000 })
|
|
60
139
|
.catch(() => { });
|
|
61
140
|
await (0, sleep_1.sleep)(timeout);
|
|
62
|
-
|
|
63
|
-
// Without this check the locator for 'Email address' can match a hidden
|
|
64
|
-
// invite-user input on the Console page when the logout redirect has not
|
|
65
|
-
// yet completed, causing a 200 s wait before failing.
|
|
66
|
-
await test_1.expect
|
|
67
|
-
.poll(async () => {
|
|
68
|
-
const [loginMessageVisible, passwordHeadingVisible, usernameVisible, passwordVisible,] = await Promise.all([
|
|
69
|
-
loginPage.loginMessage.isVisible().catch(() => false),
|
|
70
|
-
loginPage.passwordHeading.isVisible().catch(() => false),
|
|
71
|
-
loginPage.usernameInput.locator.isVisible().catch(() => false),
|
|
72
|
-
loginPage.passwordInput.isVisible().catch(() => false),
|
|
73
|
-
]);
|
|
74
|
-
return (loginMessageVisible ||
|
|
75
|
-
passwordHeadingVisible ||
|
|
76
|
-
usernameVisible ||
|
|
77
|
-
passwordVisible);
|
|
78
|
-
}, { timeout: 60000 })
|
|
79
|
-
.toBe(true);
|
|
141
|
+
await waitForLoginForm(page, loginPage);
|
|
80
142
|
await loginPage.loginWithTestUser(testUser);
|
|
81
143
|
return;
|
|
82
144
|
}
|
|
@@ -125,8 +187,14 @@ async function modelRestConnector(modelerCreatePage, connectorSettingsPage, proc
|
|
|
125
187
|
username: '',
|
|
126
188
|
password: '',
|
|
127
189
|
}) {
|
|
190
|
+
// Same gate as `modelJobWorkerDiagram` (120s) and
|
|
191
|
+
// `modelCamundaUserTaskDiagram` (180s): the properties panel renders once the
|
|
192
|
+
// new BPMN template has finished opening. 15s was the outlier here, and a
|
|
193
|
+
// slow template load failed with
|
|
194
|
+
// `locator('[data-group-id="group-general"]') ... element(s) not found`
|
|
195
|
+
// before any modelling had started.
|
|
128
196
|
await (0, test_1.expect)(modelerCreatePage.generalPanel).toBeVisible({
|
|
129
|
-
timeout:
|
|
197
|
+
timeout: 120000,
|
|
130
198
|
});
|
|
131
199
|
await modelerCreatePage.enterDiagramName(processName);
|
|
132
200
|
await (0, sleep_1.sleep)(10000);
|
|
@@ -209,7 +277,16 @@ async function assertLocatorVisibleWithRetry(page, locator, text, timeout = 6000
|
|
|
209
277
|
catch (error) {
|
|
210
278
|
if (attempt < maxRetries - 1) {
|
|
211
279
|
console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
|
|
212
|
-
|
|
280
|
+
// The reload is only the recovery step: when it races an in-flight
|
|
281
|
+
// navigation (`net::ERR_ABORTED; maybe frame was detached?`) or hits a
|
|
282
|
+
// DNS blip (`net::ERR_NAME_NOT_RESOLVED`) it used to escape this catch
|
|
283
|
+
// and fail the test instead of letting the next attempt re-assert.
|
|
284
|
+
try {
|
|
285
|
+
await page.reload();
|
|
286
|
+
}
|
|
287
|
+
catch (reloadError) {
|
|
288
|
+
console.warn(`Could not reload while asserting ${text}: ${reloadError}`);
|
|
289
|
+
}
|
|
213
290
|
await (0, sleep_1.sleep)(10000);
|
|
214
291
|
}
|
|
215
292
|
else {
|
|
@@ -219,6 +296,53 @@ async function assertLocatorVisibleWithRetry(page, locator, text, timeout = 6000
|
|
|
219
296
|
}
|
|
220
297
|
}
|
|
221
298
|
exports.assertLocatorVisibleWithRetry = assertLocatorVisibleWithRetry;
|
|
299
|
+
// Clicks a link/button that opens another Camunda app in a new tab and returns
|
|
300
|
+
// that tab, retrying the click when no tab appears.
|
|
301
|
+
//
|
|
302
|
+
// The wait is registered BEFORE each click because Playwright only reports
|
|
303
|
+
// events raised after it is called and the tab opens while the click is still
|
|
304
|
+
// resolving. That alone is not enough though: the "View process instance"
|
|
305
|
+
// control lives in Modeler's post-deployment notification, and a click that
|
|
306
|
+
// lands while that notification is being torn down resolves without ever
|
|
307
|
+
// running its handler — no tab is opened, and the wait then sits out its full
|
|
308
|
+
// budget on a click that silently did nothing ("page.waitForEvent: Timeout
|
|
309
|
+
// 60000ms exceeded while waiting for event \"popup\""). Re-clicking recovers
|
|
310
|
+
// that, so long as the control is still on screen; when it is gone the loop
|
|
311
|
+
// stops immediately rather than burning another click timeout on it.
|
|
312
|
+
async function openAppTabWithRetry(page, link, timeout = 60000, maxRetries = 3, clickTimeout = 180000) {
|
|
313
|
+
const context = page.context();
|
|
314
|
+
const knownPages = new Set(context.pages());
|
|
315
|
+
let lastError;
|
|
316
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
317
|
+
if (attempt > 1 && !(await link.isVisible().catch(() => false))) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
const appTabPromise = page.waitForEvent('popup', { timeout });
|
|
321
|
+
let clickError;
|
|
322
|
+
try {
|
|
323
|
+
await link.click({ timeout: clickTimeout });
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
clickError = error;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
// Always awaited, even when the click threw, so the wait can never
|
|
330
|
+
// reject unobserved.
|
|
331
|
+
return await appTabPromise;
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
lastError = clickError ?? error;
|
|
335
|
+
// The click may still have opened the tab without the wait observing it.
|
|
336
|
+
const opened = context.pages().find((tab) => !knownPages.has(tab));
|
|
337
|
+
if (opened !== undefined) {
|
|
338
|
+
return opened;
|
|
339
|
+
}
|
|
340
|
+
console.warn(`Attempt ${attempt} to open the app tab failed: ${error}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
throw new Error(`Failed to open the app tab after ${maxRetries} attempts: ${lastError}`);
|
|
344
|
+
}
|
|
345
|
+
exports.openAppTabWithRetry = openAppTabWithRetry;
|
|
222
346
|
async function assertPageTextWithRetry(page, text, notVisible = false, timeout = 60000, maxRetries = 10) {
|
|
223
347
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
224
348
|
try {
|
|
@@ -248,14 +372,22 @@ async function assertPageTextWithRetry(page, text, notVisible = false, timeout =
|
|
|
248
372
|
}
|
|
249
373
|
}
|
|
250
374
|
exports.assertPageTextWithRetry = assertPageTextWithRetry;
|
|
251
|
-
|
|
375
|
+
// `totalTimeout` bounds the whole "complete this task" hop, including the wait
|
|
376
|
+
// for Tasklist to import the task. It is shared across the attempts below
|
|
377
|
+
// rather than granted to each of them: `openTask` polls for minutes on its
|
|
378
|
+
// own, so a per-attempt budget multiplied out to a worst case longer than the
|
|
379
|
+
// test timeout and the harness killed the test mid-wait, with no assertion
|
|
380
|
+
// error to diagnose. One deadline keeps the retries (which recover a lost
|
|
381
|
+
// click or a stale assign button) without inflating the ceiling.
|
|
382
|
+
async function completeTaskWithRetry(taskPanelPage, taskDetailsPage, taskName, taskPriority, maxRetries = 3, totalTimeout = 1200000) {
|
|
383
|
+
const deadline = Date.now() + totalTimeout;
|
|
252
384
|
const openTask = taskPanelPage.availableTasks
|
|
253
385
|
.getByText(taskName, { exact: true })
|
|
254
386
|
.first();
|
|
255
387
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
256
388
|
let completionRequested = false;
|
|
257
389
|
try {
|
|
258
|
-
await taskPanelPage.openTask(taskName);
|
|
390
|
+
await taskPanelPage.openTask(taskName, Math.max(30000, deadline - Date.now()));
|
|
259
391
|
await (0, test_1.expect)(taskDetailsPage.detailsInfo.getByText(taskName, { exact: true })).toBeVisible();
|
|
260
392
|
if (!(await taskDetailsPage.assignedToMeText.isVisible())) {
|
|
261
393
|
await taskDetailsPage.clickAssignToMeButton();
|
|
@@ -36,7 +36,7 @@ class KeycloakAdminPage {
|
|
|
36
36
|
await (0, test_1.expect)(realmResult).toBeVisible({ timeout: 30000 });
|
|
37
37
|
await realmResult.click();
|
|
38
38
|
const mainLocator = this.page.locator('#kc-main-content-page-container');
|
|
39
|
-
await (0, test_1.expect)(mainLocator).toBeVisible();
|
|
39
|
+
await (0, test_1.expect)(mainLocator).toBeVisible({ timeout: 60000 });
|
|
40
40
|
}
|
|
41
41
|
async clickUsersTab() {
|
|
42
42
|
await this.page.click('a[id="nav-item-users"]');
|
|
@@ -18,8 +18,15 @@ class KeycloakLoginPage {
|
|
|
18
18
|
async fillPassword(password) {
|
|
19
19
|
await this.page.fill('#password', password);
|
|
20
20
|
}
|
|
21
|
+
async clearSession() {
|
|
22
|
+
await this.page.context().clearCookies();
|
|
23
|
+
}
|
|
21
24
|
async clickLogin() {
|
|
22
|
-
|
|
25
|
+
// Submitting the Keycloak form starts a redirect chain that page.click
|
|
26
|
+
// implicitly waits on ("waiting for scheduled navigations to finish"), so
|
|
27
|
+
// the action inherits the 10s actionTimeout to cover a navigation. Give it
|
|
28
|
+
// the navigation budget the rest of the suite uses instead.
|
|
29
|
+
await this.page.click('button[type="submit"]', { timeout: 60000 });
|
|
23
30
|
}
|
|
24
31
|
}
|
|
25
32
|
exports.KeycloakLoginPage = KeycloakLoginPage;
|