@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
|
@@ -16,6 +16,6 @@ declare class LoginPage {
|
|
|
16
16
|
clickUsername(): Promise<void>;
|
|
17
17
|
fillPassword(password: string): Promise<void>;
|
|
18
18
|
clickLoginButton(): Promise<void>;
|
|
19
|
-
login(username: string, password: string): Promise<
|
|
19
|
+
login(username: string, password: string): Promise<boolean>;
|
|
20
20
|
}
|
|
21
21
|
export { LoginPage };
|
|
@@ -79,7 +79,11 @@ class LoginPage {
|
|
|
79
79
|
}
|
|
80
80
|
async clickLoginButton() {
|
|
81
81
|
if (await this.page.locator('button[type="submit"]').isVisible()) {
|
|
82
|
-
|
|
82
|
+
// The Keycloak submit path was the only click here without an explicit
|
|
83
|
+
// timeout, so it inherited the 10s actionTimeout while page.click waits
|
|
84
|
+
// out the OAuth redirect chain it triggers. Match the 60s budget the
|
|
85
|
+
// sibling branches already use.
|
|
86
|
+
await this.page.click('button[type="submit"]', { timeout: 60000 });
|
|
83
87
|
}
|
|
84
88
|
else {
|
|
85
89
|
await (this.useCore
|
|
@@ -87,17 +91,22 @@ class LoginPage {
|
|
|
87
91
|
: this.loginButton.click({ timeout: 60000 }));
|
|
88
92
|
}
|
|
89
93
|
}
|
|
94
|
+
// Resolves to whether credentials were actually submitted. A missing form is
|
|
95
|
+
// not an error -- the caller may already hold a session, in which case there
|
|
96
|
+
// is nothing to fill -- but the caller has to be able to tell that apart from
|
|
97
|
+
// a submitted form, because only the latter leaves a page transition pending.
|
|
90
98
|
async login(username, password) {
|
|
91
99
|
try {
|
|
92
100
|
await this.detectLoginForm();
|
|
93
101
|
}
|
|
94
102
|
catch (error) {
|
|
95
|
-
return;
|
|
103
|
+
return false;
|
|
96
104
|
}
|
|
97
105
|
await this.clickUsername();
|
|
98
106
|
await this.fillUsername(username);
|
|
99
107
|
await this.fillPassword(password);
|
|
100
108
|
await this.clickLoginButton();
|
|
109
|
+
return true;
|
|
101
110
|
}
|
|
102
111
|
}
|
|
103
112
|
exports.LoginPage = LoginPage;
|
|
@@ -10,6 +10,7 @@ declare class NavigationPage {
|
|
|
10
10
|
readonly managementIdentityPageBanner: Locator;
|
|
11
11
|
constructor(page: Page);
|
|
12
12
|
private isGatewayErrorPage;
|
|
13
|
+
private waitForBanner;
|
|
13
14
|
goTo(url: string, banner: Locator, sleepTimeout?: number, { username, password, }?: {
|
|
14
15
|
username?: string;
|
|
15
16
|
password?: string;
|
|
@@ -58,6 +58,52 @@ class NavigationPage {
|
|
|
58
58
|
.isVisible()
|
|
59
59
|
.catch(() => false);
|
|
60
60
|
}
|
|
61
|
+
// Waits for the app shell's banner, but treats the two states that cannot
|
|
62
|
+
// resolve on their own as a reason to act rather than to keep waiting.
|
|
63
|
+
//
|
|
64
|
+
// The passive `expect(banner).toBeVisible({timeout: 60000})` this replaces
|
|
65
|
+
// could not tell a shell that is merely slow (worth waiting for) from one
|
|
66
|
+
// where nothing is coming at all: `login` swallowed a missing form and
|
|
67
|
+
// submitted nothing, so no transition is pending, or the ingress served a
|
|
68
|
+
// 50x page for the OAuth callback, which never becomes the app shell. Both
|
|
69
|
+
// spend the whole 60s and lose the attempt, and the only surviving evidence
|
|
70
|
+
// is "toBeVisible ... <element(s) not found>" -- repeated once per attempt,
|
|
71
|
+
// that is exactly the reported "Login (/modeler) failed after 8 attempts".
|
|
72
|
+
// The 60s budget is unchanged: a genuinely slow shell still gets all of it,
|
|
73
|
+
// and a sustained outage still fails.
|
|
74
|
+
async waitForBanner(url, banner, submitted) {
|
|
75
|
+
const totalTimeout = constants_1._1_SECOND_IN_MS * 60;
|
|
76
|
+
const deadline = Date.now() + totalTimeout;
|
|
77
|
+
let lastState = 'no banner';
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
const round = Math.min(constants_1._1_SECOND_IN_MS * 20, deadline - Date.now());
|
|
80
|
+
try {
|
|
81
|
+
await (0, test_1.expect)(banner).toBeVisible({ timeout: round });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
lastState = `no banner on ${this.page.url()}`;
|
|
86
|
+
}
|
|
87
|
+
if (!submitted) {
|
|
88
|
+
// Nothing was submitted and no banner rendered, so there is no pending
|
|
89
|
+
// transition for the rest of the budget to wait on. Hand back to the
|
|
90
|
+
// caller's retry loop now — it re-navigates and logs in again, which
|
|
91
|
+
// is the only thing that can change this state.
|
|
92
|
+
throw new Error(`${url} rendered no banner and no credentials were submitted: ${lastState}`);
|
|
93
|
+
}
|
|
94
|
+
if (await this.isGatewayErrorPage()) {
|
|
95
|
+
lastState = `a 50x gateway page on ${this.page.url()}`;
|
|
96
|
+
if (Date.now() >= deadline) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
await this.page.goto(url, {
|
|
100
|
+
timeout: 60000,
|
|
101
|
+
waitUntil: 'domcontentloaded',
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
throw new Error(`${url} did not render its banner within ${totalTimeout}ms: ${lastState}`);
|
|
106
|
+
}
|
|
61
107
|
async goTo(url, banner, sleepTimeout, { username = IDENTITY_FIRSTUSER_USERNAME, password = IDENTITY_FIRSTUSER_PASSWORD, } = {}, maxRetries = 8) {
|
|
62
108
|
const startTime = Date.now();
|
|
63
109
|
let timeout = constants_1._1_SECOND_IN_MS * 10;
|
|
@@ -77,8 +123,15 @@ class NavigationPage {
|
|
|
77
123
|
// serves an nginx 50x gateway page in place of the app. Reload with a
|
|
78
124
|
// bounded budget so a short blip self-heals; a sustained outage
|
|
79
125
|
// exhausts the budget and still surfaces as a failed navigation.
|
|
126
|
+
// The bound is wall clock as well as count: 8 reloads that each wait
|
|
127
|
+
// out the 60s goto budget are ~520s of the 720s test timeout inside a
|
|
128
|
+
// single attempt, which starves the retry loop below of the attempts
|
|
129
|
+
// it needs to ride the outage out.
|
|
130
|
+
const gatewayDeadline = Date.now() + constants_1._1_SECOND_IN_MS * 60;
|
|
80
131
|
let gatewayReloads = 0;
|
|
81
|
-
while (gatewayReloads < 8 &&
|
|
132
|
+
while (gatewayReloads < 8 &&
|
|
133
|
+
Date.now() < gatewayDeadline &&
|
|
134
|
+
(await this.isGatewayErrorPage())) {
|
|
82
135
|
await (0, sleep_1.sleep)(constants_1._1_SECOND_IN_MS * 5);
|
|
83
136
|
await this.page.goto(url, {
|
|
84
137
|
timeout: 60000,
|
|
@@ -100,17 +153,27 @@ class NavigationPage {
|
|
|
100
153
|
// authenticated — wait for the real login form to appear.
|
|
101
154
|
const currentUrl = this.page.url();
|
|
102
155
|
const onLoginPath = currentUrl.includes('/login') || currentUrl.includes('/auth/');
|
|
156
|
+
let submitted = false;
|
|
103
157
|
if (isBannerVisible && !onLoginPath) {
|
|
104
|
-
|
|
158
|
+
// Probe with the same #username signal detectLoginForm uses: the
|
|
159
|
+
// KC26 default theme does not expose the 'Username or email' label,
|
|
160
|
+
// so the old locator reported "no form" on a rendered Keycloak login
|
|
161
|
+
// page and left the attempt unauthenticated.
|
|
162
|
+
const loginVisible = await this.page
|
|
163
|
+
.locator('#username')
|
|
164
|
+
.or(loginPage.usernameInput)
|
|
165
|
+
.first()
|
|
166
|
+
.isVisible()
|
|
167
|
+
.catch(() => false);
|
|
105
168
|
if (loginVisible) {
|
|
106
|
-
await loginPage.login(username, password);
|
|
169
|
+
submitted = await loginPage.login(username, password);
|
|
107
170
|
}
|
|
108
171
|
// If login selector not visible, banner is real - navigation succeeded
|
|
109
172
|
}
|
|
110
173
|
else {
|
|
111
|
-
await loginPage.login(username, password);
|
|
174
|
+
submitted = await loginPage.login(username, password);
|
|
112
175
|
}
|
|
113
|
-
await (
|
|
176
|
+
await this.waitForBanner(url, banner, submitted);
|
|
114
177
|
return;
|
|
115
178
|
}
|
|
116
179
|
catch (error) {
|
|
@@ -142,8 +205,16 @@ class NavigationPage {
|
|
|
142
205
|
}
|
|
143
206
|
}
|
|
144
207
|
else {
|
|
145
|
-
console.error(`[${now.toISOString()}] (+${elapsed}ms) [NavigationPage] Login (${url}) failed after ${maxRetries} attempts`);
|
|
146
|
-
|
|
208
|
+
console.error(`[${now.toISOString()}] (+${elapsed}ms) [NavigationPage] Login (${url}) failed after ${maxRetries} attempts: ${error}`);
|
|
209
|
+
// Carry the final attempt's error into the thrown message. Without
|
|
210
|
+
// it the only surviving signal is the attempt count, which says
|
|
211
|
+
// nothing about WHY all 8 attempts failed -- a Keycloak 5xx, a
|
|
212
|
+
// gateway page, a missing banner and a closed page all collapse to
|
|
213
|
+
// the same string, so triage of this signature has to guess. Every
|
|
214
|
+
// sibling helper already does this (goToKeycloak below rethrows
|
|
215
|
+
// lastError; KeycloakUtils and pages/8.10/UtilitiesPage interpolate
|
|
216
|
+
// the cause).
|
|
217
|
+
throw new Error(`Login (${url}) failed after ${maxRetries} attempts: ${error}`);
|
|
147
218
|
}
|
|
148
219
|
}
|
|
149
220
|
}
|
|
@@ -204,7 +275,14 @@ class NavigationPage {
|
|
|
204
275
|
: new Error(`goToKeycloak failed after ${maxRetries} attempts`);
|
|
205
276
|
}
|
|
206
277
|
async goToOCAdmin(sleepTimeout, credentials, maxRetries) {
|
|
207
|
-
await this.goTo(`${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/admin`, this.identityPageBanner, sleepTimeout, credentials,
|
|
278
|
+
await this.goTo(`${NORMALIZED_ORCHESTRATION_CONTEXT_PATH}/admin`, this.identityPageBanner, sleepTimeout, credentials,
|
|
279
|
+
// 8, not 5, to match goTo's own default: the comment there records that
|
|
280
|
+
// ~20s of backoff (all 5 attempts buy) was not consistently enough to
|
|
281
|
+
// outlast Keycloak's warm-up, and /admin additionally serves a
|
|
282
|
+
// forbidden redirect while the user's admin authorization propagates.
|
|
283
|
+
// Call sites that wrap goToOCAdmin in their own toPass loop still pass
|
|
284
|
+
// maxRetries explicitly (see document-handling-user-flows).
|
|
285
|
+
maxRetries ?? (env_1.isOpenSearch ? 10 : 8));
|
|
208
286
|
}
|
|
209
287
|
async goToClusters(sleepTimeout, credentials) {
|
|
210
288
|
await this.goTo(`${MODELER_CONTEXT_PATH}/clusters`, this.modelerPageBanner, sleepTimeout, credentials);
|
|
@@ -67,8 +67,18 @@ class OCIdentityHomePage {
|
|
|
67
67
|
await this.authorizationsTab.click();
|
|
68
68
|
}
|
|
69
69
|
async clickRolesTab() {
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
// Reload between attempts: the Admin shell can render its branding link
|
|
71
|
+
// (which is all goToOCAdmin waits for) before the nav hydrates, and /admin
|
|
72
|
+
// serves a no-permission view while the user's admin authorization is
|
|
73
|
+
// still propagating. A flat wait on the same DOM can recover from neither.
|
|
74
|
+
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.rolesTab, {
|
|
75
|
+
visibilityTimeout: 30000,
|
|
76
|
+
totalTimeout: 120000,
|
|
77
|
+
postAction: async () => {
|
|
78
|
+
await this.page.reload();
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
await this.rolesTab.click({ timeout: 30000 });
|
|
72
82
|
}
|
|
73
83
|
async clickGroupsTab() {
|
|
74
84
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.groupsTab);
|
|
@@ -21,6 +21,7 @@ declare class OCIdentityRolesPage {
|
|
|
21
21
|
readonly deleteRoleModalCancelButton: Locator;
|
|
22
22
|
readonly deleteRoleModalDeleteButton: Locator;
|
|
23
23
|
readonly adminRole: Locator;
|
|
24
|
+
readonly rolesNavTab: Locator;
|
|
24
25
|
readonly assignUserButton: Locator;
|
|
25
26
|
readonly usernameTextBox: Locator;
|
|
26
27
|
readonly assignUserSubButton: Locator;
|
|
@@ -40,6 +41,8 @@ declare class OCIdentityRolesPage {
|
|
|
40
41
|
deleteRole(name: string, options?: {
|
|
41
42
|
throwError?: boolean;
|
|
42
43
|
}): Promise<void>;
|
|
44
|
+
private isRolesListOpen;
|
|
45
|
+
private openRolesList;
|
|
43
46
|
clickAdminRole(): Promise<void>;
|
|
44
47
|
assignUserToRole(userId: string): Promise<void>;
|
|
45
48
|
clickAssignUserButton(): Promise<void>;
|
|
@@ -4,6 +4,7 @@ exports.OCIdentityRolesPage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const UtilitiesPage_1 = require("../SM-8.10/UtilitiesPage");
|
|
6
6
|
const expectLocatorWithPagination_1 = require("../../utils/assertionHelpers/expectLocatorWithPagination");
|
|
7
|
+
const findLocatorWithPagination_1 = require("../../utils/assertionHelpers/findLocatorWithPagination");
|
|
7
8
|
const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
|
|
8
9
|
const sleep_1 = require("../../utils/sleep");
|
|
9
10
|
class OCIdentityRolesPage {
|
|
@@ -28,6 +29,7 @@ class OCIdentityRolesPage {
|
|
|
28
29
|
deleteRoleModalCancelButton;
|
|
29
30
|
deleteRoleModalDeleteButton;
|
|
30
31
|
adminRole;
|
|
32
|
+
rolesNavTab;
|
|
31
33
|
assignUserButton;
|
|
32
34
|
usernameTextBox;
|
|
33
35
|
assignUserSubButton;
|
|
@@ -84,6 +86,11 @@ class OCIdentityRolesPage {
|
|
|
84
86
|
name: /delete role/i,
|
|
85
87
|
});
|
|
86
88
|
this.adminRole = page.getByRole('cell', { name: 'admin', exact: true });
|
|
89
|
+
this.rolesNavTab = page
|
|
90
|
+
.getByRole('banner')
|
|
91
|
+
.locator('a')
|
|
92
|
+
.filter({ hasText: /^Roles$/ })
|
|
93
|
+
.first();
|
|
87
94
|
this.assignUserButton = page.getByRole('button', { name: 'Assign user' });
|
|
88
95
|
this.usernameTextBox = page.getByRole('textbox', { name: 'Username' });
|
|
89
96
|
this.assignUserSubButton = page
|
|
@@ -181,8 +188,97 @@ class OCIdentityRolesPage {
|
|
|
181
188
|
}
|
|
182
189
|
}
|
|
183
190
|
}
|
|
191
|
+
async isRolesListOpen(timeout) {
|
|
192
|
+
return this.createRoleButton
|
|
193
|
+
.first()
|
|
194
|
+
.waitFor({ state: 'visible', timeout })
|
|
195
|
+
.then(() => true)
|
|
196
|
+
.catch(() => false);
|
|
197
|
+
}
|
|
198
|
+
// Re-enters the roles list through the in-app nav rather than reloading.
|
|
199
|
+
// `page.reload()` re-runs the OIDC round-trip and its post-login redirect
|
|
200
|
+
// drops the deep path, so a reload-based retry re-ran against whatever the
|
|
201
|
+
// Admin root rendered: a view with no table at all, or the wrong table.
|
|
202
|
+
// Clicking the banner's Roles link is a client-side route change, so it
|
|
203
|
+
// cannot lose the session; the reload is kept only for the case where the
|
|
204
|
+
// app shell itself is gone.
|
|
205
|
+
async openRolesList(timeout) {
|
|
206
|
+
if (await this.isRolesListOpen(Math.min(timeout, 15000))) {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const shellVisible = await this.rolesNavTab
|
|
210
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
211
|
+
.then(() => true)
|
|
212
|
+
.catch(() => false);
|
|
213
|
+
if (!shellVisible) {
|
|
214
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
215
|
+
await (0, test_1.expect)(this.rolesNavTab).toBeVisible({ timeout });
|
|
216
|
+
}
|
|
217
|
+
await this.rolesNavTab.click({ timeout });
|
|
218
|
+
await (0, test_1.expect)(this.createRoleButton.first()).toBeVisible({ timeout });
|
|
219
|
+
}
|
|
184
220
|
async clickAdminRole() {
|
|
185
|
-
|
|
221
|
+
// The roles table is fetched after the Roles tab click, and other specs in
|
|
222
|
+
// the nightly add roles that can push `admin` onto a later page, so a bare
|
|
223
|
+
// click had only the 10s actionTimeout to cover both the load and the
|
|
224
|
+
// lookup. Use the same pagination-aware lookup createRole/clickRole use.
|
|
225
|
+
//
|
|
226
|
+
// Bounded overall: this runs in the smoke suite's beforeEach, whose time is
|
|
227
|
+
// charged to the test, and three attempts of the previous per-step budgets
|
|
228
|
+
// could occupy 540s of the 720s test timeout on their own — which is why a
|
|
229
|
+
// stuck recovery here surfaced as a timeout in the test body rather than as
|
|
230
|
+
// this method's own error.
|
|
231
|
+
const deadline = Date.now() + 300000;
|
|
232
|
+
const budget = (max) => Math.max(5000, Math.min(max, deadline - Date.now()));
|
|
233
|
+
const adminCell = this.adminRole.first();
|
|
234
|
+
const maxRetries = 3;
|
|
235
|
+
let lastError;
|
|
236
|
+
for (let attempt = 0; attempt < maxRetries && Date.now() < deadline; attempt++) {
|
|
237
|
+
try {
|
|
238
|
+
// Every attempt starts from a confirmed roles list, so a swallowed nav
|
|
239
|
+
// click or a redirect back to the Admin root is recovered instead of
|
|
240
|
+
// being scanned again.
|
|
241
|
+
await this.openRolesList(budget(60000));
|
|
242
|
+
// Wait for the admin row itself before scanning: the scan gives each
|
|
243
|
+
// page only 5s, so against a table that is still fetching it reports
|
|
244
|
+
// the row missing and — with no next page to walk to — gives up in 5s.
|
|
245
|
+
// That is the shape of the run log's 52 "No more pages available after
|
|
246
|
+
// page 1" against 600 finds on page 1.
|
|
247
|
+
const onCurrentPage = await adminCell
|
|
248
|
+
.waitFor({ state: 'visible', timeout: budget(30000) })
|
|
249
|
+
.then(() => true)
|
|
250
|
+
.catch(() => false);
|
|
251
|
+
if (!onCurrentPage) {
|
|
252
|
+
// Scanned directly rather than through expectLocatorWithPagination:
|
|
253
|
+
// that wrapper hard-codes the scan to 30s (it forwards no options),
|
|
254
|
+
// which is ~5 pages, and the roles table reached 5 pages in the same
|
|
255
|
+
// run. Its retry also reloads, which is the recovery openRolesList
|
|
256
|
+
// exists to avoid.
|
|
257
|
+
const found = await (0, findLocatorWithPagination_1.findLocatorWithPagination)(this.page, adminCell, {
|
|
258
|
+
timeout: budget(90000),
|
|
259
|
+
});
|
|
260
|
+
if (!found) {
|
|
261
|
+
throw new Error('admin role row not found on any page of the roles table');
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await adminCell.click({ timeout: budget(30000) });
|
|
265
|
+
// The row click is the only thing that navigates to the role detail
|
|
266
|
+
// page, and nothing confirmed it landed — a click the table swallowed
|
|
267
|
+
// mid-render left the caller waiting out assignUserButton's whole
|
|
268
|
+
// budget on a page that never had one.
|
|
269
|
+
await (0, test_1.expect)(this.assignUserButton).toBeVisible({
|
|
270
|
+
timeout: budget(30000),
|
|
271
|
+
});
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
lastError = error;
|
|
276
|
+
if (attempt < maxRetries - 1) {
|
|
277
|
+
console.warn(`Attempt ${attempt + 1}: admin role detail page did not open, retrying...`);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
throw new Error(`Admin role detail page did not open after ${maxRetries} attempts: ${String(lastError)}`);
|
|
186
282
|
}
|
|
187
283
|
async assignUserToRole(userId) {
|
|
188
284
|
await this.clickAssignUserButton();
|
|
@@ -191,16 +287,26 @@ class OCIdentityRolesPage {
|
|
|
191
287
|
await this.clickAssignUserSubButton();
|
|
192
288
|
}
|
|
193
289
|
async clickAssignUserButton() {
|
|
194
|
-
|
|
290
|
+
// Rendered only once the role detail page has loaded its users tab, which
|
|
291
|
+
// outlasts the 10s actionTimeout a bare click inherits. Same budget the
|
|
292
|
+
// SaaS 8.10 doAssignUserToRole already gives this button.
|
|
293
|
+
await (0, test_1.expect)(this.assignUserButton).toBeVisible({ timeout: 60000 });
|
|
294
|
+
await this.assignUserButton.click({ timeout: 60000 });
|
|
195
295
|
}
|
|
196
296
|
async clickUsernameTextbox() {
|
|
197
|
-
|
|
297
|
+
// Waiting for the textbox is what confirms the modal finished opening.
|
|
298
|
+
await (0, test_1.expect)(this.usernameTextBox).toBeVisible({ timeout: 60000 });
|
|
299
|
+
await this.usernameTextBox.click({ timeout: 60000 });
|
|
198
300
|
}
|
|
199
301
|
async fillUsernameTextbox(userId) {
|
|
200
302
|
await this.usernameTextBox.fill(userId);
|
|
201
303
|
}
|
|
202
304
|
async clickAssignUserSubButton() {
|
|
203
|
-
|
|
305
|
+
// The modal's submit button stays disabled until the username field
|
|
306
|
+
// validates, so wait for it to be actionable rather than racing the 10s
|
|
307
|
+
// actionTimeout.
|
|
308
|
+
await (0, test_1.expect)(this.assignUserSubButton).toBeEnabled({ timeout: 60000 });
|
|
309
|
+
await this.assignUserSubButton.click({ timeout: 60000 });
|
|
204
310
|
}
|
|
205
311
|
async clickClientsTab() {
|
|
206
312
|
await this.clientsTab.click();
|
|
@@ -49,10 +49,22 @@ class OperateHomePage {
|
|
|
49
49
|
.getByLabel('Open');
|
|
50
50
|
}
|
|
51
51
|
async clickProcessesTab() {
|
|
52
|
+
// Called right after navigating into Operate, a Vite SPA whose nav bar can
|
|
53
|
+
// take longer than clickLocatorWithRetry's default 10s visibilityTimeout to
|
|
54
|
+
// render on a loaded cluster — so all 3 attempts could burn inside 30s and
|
|
55
|
+
// throw while the link was still on its way. Give each attempt the 30s the
|
|
56
|
+
// callers that pre-wait this same link already assume.
|
|
52
57
|
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.processesTab, {
|
|
53
|
-
totalTimeout:
|
|
58
|
+
totalTimeout: 120000,
|
|
59
|
+
visibilityTimeout: 30000,
|
|
54
60
|
});
|
|
55
61
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.processPageHeading, {
|
|
62
|
+
// Defaults (10s per attempt, 60s total) gave the Processes panel less
|
|
63
|
+
// budget than the tab click above already assumes it needs on a loaded
|
|
64
|
+
// cluster, so all three attempts could expire while the view was still
|
|
65
|
+
// rendering. Match the 30s/120s the click uses.
|
|
66
|
+
visibilityTimeout: 30000,
|
|
67
|
+
totalTimeout: 120000,
|
|
56
68
|
preAction: async () => {
|
|
57
69
|
await (0, test_1.expect)(this.processesTab).toBeVisible({
|
|
58
70
|
timeout: constants_1._1_SECOND_IN_MS * 4,
|
|
@@ -23,6 +23,7 @@ declare class OperateProcessesPage {
|
|
|
23
23
|
private toggleActiveCheckboxOn;
|
|
24
24
|
private toggleCompletedCheckbox;
|
|
25
25
|
private applyProcessStateFilter;
|
|
26
|
+
private dismissWhatsNewPopUp;
|
|
26
27
|
private checkTableForProcess;
|
|
27
28
|
clickProcessActiveCheckbox(): Promise<void>;
|
|
28
29
|
clickProcessCompletedCheckbox(): Promise<void>;
|
|
@@ -98,7 +98,10 @@ class OperateProcessesPage {
|
|
|
98
98
|
}
|
|
99
99
|
async toggleActiveCheckboxOn() {
|
|
100
100
|
await this.uncheckCompletedCheckbox();
|
|
101
|
-
|
|
101
|
+
// Set — never toggle — the Incidents filter: applyProcessStateFilter is
|
|
102
|
+
// re-applied after every reload in clickProcessInstanceLink, so a blind
|
|
103
|
+
// flip oscillates the filter between passes and the lookup never converges.
|
|
104
|
+
await this.checkCheckbox(this.processIncidentsCheckbox);
|
|
102
105
|
await this.processActiveCheckbox.waitFor({
|
|
103
106
|
state: 'attached',
|
|
104
107
|
timeout: constants_1._1_SECOND_IN_MS * 10,
|
|
@@ -114,7 +117,7 @@ class OperateProcessesPage {
|
|
|
114
117
|
}
|
|
115
118
|
async toggleCompletedCheckbox() {
|
|
116
119
|
await this.uncheckActiveCheckbox();
|
|
117
|
-
await this.
|
|
120
|
+
await this.checkCheckbox(this.processIncidentsCheckbox);
|
|
118
121
|
await this.processCompletedCheckbox.waitFor({
|
|
119
122
|
state: 'attached',
|
|
120
123
|
timeout: constants_1._1_SECOND_IN_MS * 10,
|
|
@@ -137,6 +140,31 @@ class OperateProcessesPage {
|
|
|
137
140
|
await this.toggleCompletedCheckbox();
|
|
138
141
|
}
|
|
139
142
|
}
|
|
143
|
+
// Operate raises the "Here's what moved in Operate" modal on a first page
|
|
144
|
+
// load, and every test here logs in as a freshly created user, so it is up on
|
|
145
|
+
// the Processes view. Its Carbon overlay swallows clicks on the Processes tab
|
|
146
|
+
// link and on the filter checkbox labels, which is why a reload-recovery pass
|
|
147
|
+
// could make no progress at all. Dismiss it before interacting with the view,
|
|
148
|
+
// not only after an instance page has opened.
|
|
149
|
+
async dismissWhatsNewPopUp() {
|
|
150
|
+
// isVisible() returns immediately — Playwright ignores its `timeout`
|
|
151
|
+
// option — so this probe got none of the 2s grace it reads as having. Right
|
|
152
|
+
// after a reload Operate has usually not mounted the modal yet, the probe
|
|
153
|
+
// reported "not open", and the Carbon overlay then intercepted every click
|
|
154
|
+
// in the recovery loop below until the 720s test timeout. waitFor spends
|
|
155
|
+
// the 2s the probe was written to spend.
|
|
156
|
+
const isOpen = await this.whatsNewPopUp
|
|
157
|
+
.waitFor({ state: 'visible', timeout: constants_1._1_SECOND_IN_MS * 2 })
|
|
158
|
+
.then(() => true)
|
|
159
|
+
.catch(() => false);
|
|
160
|
+
if (!isOpen) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
await this.gotItButton.click({ timeout: constants_1._1_SECOND_IN_MS * 10 });
|
|
164
|
+
await (0, test_1.expect)(this.whatsNewPopUp).toBeHidden({
|
|
165
|
+
timeout: constants_1._1_SECOND_IN_MS * 10,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
140
168
|
async checkTableForProcess(processName) {
|
|
141
169
|
try {
|
|
142
170
|
const processTable = this.page.locator('[aria-label="Process Instances Panel"]');
|
|
@@ -149,9 +177,7 @@ class OperateProcessesPage {
|
|
|
149
177
|
await link.scrollIntoViewIfNeeded();
|
|
150
178
|
await link.click({ timeout: constants_1._1_SECOND_IN_MS * 2 });
|
|
151
179
|
await (0, test_1.expect)(this.page.getByText('Instance History').first()).toBeVisible({ timeout: constants_1._1_SECOND_IN_MS * 10 });
|
|
152
|
-
|
|
153
|
-
await this.gotItButton.click();
|
|
154
|
-
}
|
|
180
|
+
await this.dismissWhatsNewPopUp();
|
|
155
181
|
return true;
|
|
156
182
|
}
|
|
157
183
|
catch (err) {
|
|
@@ -190,19 +216,7 @@ class OperateProcessesPage {
|
|
|
190
216
|
timeout: constants_1._1_SECOND_IN_MS * 10,
|
|
191
217
|
});
|
|
192
218
|
const wasChecked = await this.processIncidentsCheckbox.isChecked();
|
|
193
|
-
|
|
194
|
-
await this.processIncidentsCheckbox.uncheck({
|
|
195
|
-
force: true,
|
|
196
|
-
timeout: 90000,
|
|
197
|
-
});
|
|
198
|
-
}
|
|
199
|
-
else {
|
|
200
|
-
await this.processIncidentsCheckbox.check({ force: true, timeout: 90000 });
|
|
201
|
-
}
|
|
202
|
-
await (0, test_1.expect)(this.processIncidentsCheckbox).toBeChecked({
|
|
203
|
-
checked: !wasChecked,
|
|
204
|
-
timeout: constants_1._1_SECOND_IN_MS * 5,
|
|
205
|
-
});
|
|
219
|
+
await this.setCheckboxState(this.processIncidentsCheckbox, !wasChecked);
|
|
206
220
|
}
|
|
207
221
|
async clickRunningProcessInstancesCheckbox() {
|
|
208
222
|
await this.processRunningInstancesCheckbox.waitFor({
|
|
@@ -240,6 +254,7 @@ class OperateProcessesPage {
|
|
|
240
254
|
});
|
|
241
255
|
}
|
|
242
256
|
async clickProcessInstanceLink(processName, type = 'active') {
|
|
257
|
+
await this.dismissWhatsNewPopUp();
|
|
243
258
|
await this.applyProcessStateFilter(type);
|
|
244
259
|
if (await this.checkTableForProcess(processName)) {
|
|
245
260
|
return;
|
|
@@ -278,6 +293,10 @@ class OperateProcessesPage {
|
|
|
278
293
|
.waitForLoadState('networkidle', { timeout: 30000 })
|
|
279
294
|
.catch(() => console.log('waitForLoadState after Operate navigation timed out, continuing...'));
|
|
280
295
|
}
|
|
296
|
+
// The reload re-raises the what's-new modal, and its overlay would
|
|
297
|
+
// otherwise intercept both the Processes tab click and the checkbox
|
|
298
|
+
// labels below, so no attempt in this loop could ever converge.
|
|
299
|
+
await this.dismissWhatsNewPopUp();
|
|
281
300
|
// Operate may land on the Dashboard after reload even when the captured
|
|
282
301
|
// URL pointed at the Processes view. The filter checkboxes only exist on
|
|
283
302
|
// the Processes tab, so navigate there explicitly before applying filters.
|
|
@@ -67,7 +67,27 @@ class OptimizeReportPage {
|
|
|
67
67
|
await this.processSelectionButton.click({ timeout: 180000 });
|
|
68
68
|
}
|
|
69
69
|
async clickUserTaskProcess(processName) {
|
|
70
|
-
|
|
70
|
+
const processOption = this.page
|
|
71
|
+
.getByText(processName, { exact: true })
|
|
72
|
+
.first();
|
|
73
|
+
const maxRetries = 3;
|
|
74
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
75
|
+
await (0, test_1.expect)(processOption).toBeVisible({ timeout: 30000 });
|
|
76
|
+
await processOption.click();
|
|
77
|
+
// Optimize keeps refetching the definition list while the modal is open.
|
|
78
|
+
// A refetch that lands right after the click remounts the version
|
|
79
|
+
// list-box and drops the selection with it, so Version stays disabled and
|
|
80
|
+
// the caller's toBeEnabled assertion times out. Confirm the selection
|
|
81
|
+
// committed and re-pick when a remount swallowed it. Deliberately no
|
|
82
|
+
// throw here: the caller's assertion stays the gate on this state.
|
|
83
|
+
try {
|
|
84
|
+
await (0, test_1.expect)(this.versionSelection).toBeEnabled({ timeout: 15000 });
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
console.warn(`Attempt ${attempt} to select process ${processName} left the version selection disabled, retrying...`, error instanceof Error ? error.message : error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
71
91
|
}
|
|
72
92
|
async clickVersionSelection() {
|
|
73
93
|
await this.versionSelection.click();
|
|
@@ -105,6 +105,13 @@ class TaskDetailsPage {
|
|
|
105
105
|
await (0, test_1.expect)(this.assignToMeButton).toBeVisible({ timeout: 60000 });
|
|
106
106
|
await this.assignToMeButton.click({ timeout: 60000 });
|
|
107
107
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.assignedToMeText, {
|
|
108
|
+
// Assignment is a round-trip through the Zeebe user-task backend, which
|
|
109
|
+
// on a loaded cluster outlasts the default 10s per attempt. Every failure
|
|
110
|
+
// here aborts the caller's whole task-completion attempt, so it retries
|
|
111
|
+
// the search, the click and the panel load as well — the run log shows
|
|
112
|
+
// that churn repeating up to attempt 7.
|
|
113
|
+
visibilityTimeout: 30000,
|
|
114
|
+
totalTimeout: 60000,
|
|
108
115
|
postAction: async () => {
|
|
109
116
|
await this.page.reload();
|
|
110
117
|
},
|
|
@@ -82,7 +82,12 @@ async function completeTaskWithRetry(page, taskPanelPage, taskDetailsPage, taskN
|
|
|
82
82
|
}
|
|
83
83
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
84
84
|
try {
|
|
85
|
-
|
|
85
|
+
// Tasklist keeps polling for task updates, so 'networkidle' never
|
|
86
|
+
// settles here. This wait carried no timeout, so it consumed whatever
|
|
87
|
+
// was left of the 12-minute test budget and the run died inside
|
|
88
|
+
// waitForLoadState. The task panel rendering is the precondition this
|
|
89
|
+
// step actually needs, so wait for that instead.
|
|
90
|
+
await (0, test_1.expect)(taskPanelPage.availableTasks).toBeVisible({ timeout: 60000 });
|
|
86
91
|
const taskLocator = taskPanelPage.availableTasks
|
|
87
92
|
.getByText(taskName, { exact: true })
|
|
88
93
|
.first();
|
|
@@ -150,7 +155,8 @@ async function completeTaskWithRetry(page, taskPanelPage, taskDetailsPage, taskN
|
|
|
150
155
|
catch {
|
|
151
156
|
await page.reload();
|
|
152
157
|
}
|
|
153
|
-
|
|
158
|
+
// No settle wait here either — the goto already waited for
|
|
159
|
+
// domcontentloaded and the next attempt waits for the task panel.
|
|
154
160
|
}
|
|
155
161
|
else {
|
|
156
162
|
console.error(`All ${maxRetries} attempts failed for task ${taskName}`);
|
|
@@ -343,7 +349,12 @@ async function assertLocatorVisibleWithRetry(page, locator, text, timeout = 3000
|
|
|
343
349
|
console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
|
|
344
350
|
}
|
|
345
351
|
else {
|
|
346
|
-
|
|
352
|
+
// Keep the cause. Callers branch on the original error to tell a real
|
|
353
|
+
// failure apart from the test timeout aborting an in-flight wait (see
|
|
354
|
+
// assertReportWithRefreshes), and a bare attempt count told them
|
|
355
|
+
// nothing — so they retried into an already-dead test budget and
|
|
356
|
+
// reported their own generic "after N attempts" instead.
|
|
357
|
+
throw new Error(`Assertion failed after ${maxRetries} attempts: ${String(error)}`);
|
|
347
358
|
}
|
|
348
359
|
}
|
|
349
360
|
}
|
|
@@ -371,7 +382,7 @@ async function assertPageTextWithRetry(page, text, notVisible, timeout = 30000,
|
|
|
371
382
|
console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
|
|
372
383
|
}
|
|
373
384
|
else {
|
|
374
|
-
throw new Error(`Assertion failed after ${maxRetries} attempts`);
|
|
385
|
+
throw new Error(`Assertion failed after ${maxRetries} attempts: ${String(error)}`);
|
|
375
386
|
}
|
|
376
387
|
}
|
|
377
388
|
}
|