@camunda/e2e-test-suite 0.0.1276 → 0.0.1278
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.8/AppsPage.d.ts +4 -0
- package/dist/pages/8.8/AppsPage.js +209 -22
- package/dist/pages/8.8/ClusterDetailsPage.d.ts +9 -0
- package/dist/pages/8.8/ClusterDetailsPage.js +154 -3
- package/dist/pages/8.8/ClusterPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterPage.js +49 -20
- package/dist/pages/8.8/ConnectorMarketplacePage.d.ts +2 -0
- package/dist/pages/8.8/ConnectorMarketplacePage.js +44 -2
- package/dist/pages/8.8/ConsoleOrganizationPage.js +63 -18
- package/dist/pages/8.8/FormJsPage.d.ts +4 -0
- package/dist/pages/8.8/FormJsPage.js +31 -1
- package/dist/pages/8.8/ModelerCreatePage.d.ts +3 -0
- package/dist/pages/8.8/ModelerCreatePage.js +167 -18
- package/dist/pages/8.8/PlayPage.js +14 -3
- package/dist/pages/8.8/SettingsPage.js +19 -2
- package/dist/pages/8.8/UtilitiesPage.js +43 -3
- package/dist/pages/8.8/WorkspaceMembersPage.d.ts +20 -0
- package/dist/pages/8.8/WorkspaceMembersPage.js +258 -0
- package/dist/tests/8.8/access-user-role-management.spec.js +95 -0
- package/dist/tests/8.8/console-user-flows.spec.js +6 -0
- package/dist/tests/8.8/web-modeler-user-flows.spec.js +6 -2
- package/dist/utils/consoleApiHelpers.js +38 -11
- package/package.json +1 -1
|
@@ -21,8 +21,12 @@ declare class AppsPage {
|
|
|
21
21
|
readonly hubSidebar: Locator;
|
|
22
22
|
constructor(page: Page);
|
|
23
23
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
24
|
+
private expandHubClusterGroup;
|
|
25
|
+
private showAllWorkspaces;
|
|
24
26
|
private clickHubClusterApp;
|
|
27
|
+
private isOnHub;
|
|
25
28
|
clickModeler(): Promise<void>;
|
|
29
|
+
private clickHubModelerHome;
|
|
26
30
|
/**
|
|
27
31
|
* Waits for the Modeler app to actually render, and recovers the one landing
|
|
28
32
|
* state a hostname check cannot see: Modeler's OAuth `/login-callback`
|
|
@@ -4,6 +4,9 @@ exports.AppsPage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const UtilitiesPage_1 = require("../8.8/UtilitiesPage");
|
|
7
|
+
function escapeRegExp(value) {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
}
|
|
7
10
|
class AppsPage {
|
|
8
11
|
page;
|
|
9
12
|
modelerLink;
|
|
@@ -85,6 +88,165 @@ class AppsPage {
|
|
|
85
88
|
// up the app link sidebar-wide rather than scoped to a "group" container
|
|
86
89
|
// that doesn't reliably exist. Ported from pages/8.9/AppsPage.ts, where
|
|
87
90
|
// this shape is already nightly-proven.
|
|
91
|
+
async expandHubClusterGroup(clusterName) {
|
|
92
|
+
const toggle = this.hubSidebar
|
|
93
|
+
.getByRole('button', { name: clusterName })
|
|
94
|
+
.first();
|
|
95
|
+
// The diagram editor (and other full-screen views reached from it, e.g.
|
|
96
|
+
// right after runProcessInstance/deployDiagram) has no "Main navigation"
|
|
97
|
+
// sidebar at all -- confirmed from a nightly trace's DOM snapshot: the
|
|
98
|
+
// page's only <nav> is aria-label="Context navigation" (the breadcrumb),
|
|
99
|
+
// and neither "Operate"/"Tasklist"/"Optimize" nor any app-switcher text
|
|
100
|
+
// appears anywhere on it. clickCamundaApps() is a no-op on Hub (there is
|
|
101
|
+
// no switcher panel to open), so a caller reaching clickOperate/
|
|
102
|
+
// clickTasklist/clickOptimize straight from a diagram was left on a page
|
|
103
|
+
// this sidebar could never resolve on, no matter how the toggle itself
|
|
104
|
+
// is matched. Navigate home first, the same way clickModeler() does,
|
|
105
|
+
// whenever the sidebar isn't already there.
|
|
106
|
+
//
|
|
107
|
+
// A freshly re-authenticated page can also land on a workspace-agnostic
|
|
108
|
+
// route (e.g. bare /workspaces right after loginWithTestUser(), which
|
|
109
|
+
// clickCamundaApps() does nothing to fix -- it is a no-op on Hub) whose
|
|
110
|
+
// sidebar renders but has not resolved this cluster's own group yet.
|
|
111
|
+
// Confirmed from a nightly trace: both the getAttribute and click below
|
|
112
|
+
// timed out (10s each) against this exact toggle while the URL sat on
|
|
113
|
+
// /workspaces, right after a mid-test re-login. Route through the same
|
|
114
|
+
// clickModeler() recovery whenever the toggle itself hasn't shown up
|
|
115
|
+
// either, not only when the whole sidebar is missing.
|
|
116
|
+
const sidebarReady = (await this.hubSidebar.isVisible({ timeout: 5000 }).catch(() => false)) &&
|
|
117
|
+
(await toggle
|
|
118
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
119
|
+
.then(() => true)
|
|
120
|
+
.catch(() => false));
|
|
121
|
+
if (!sidebarReady) {
|
|
122
|
+
await this.clickModeler();
|
|
123
|
+
await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
|
|
124
|
+
// Hub's Workspaces overview defaults to "My workspaces" -- only the
|
|
125
|
+
// ones the current user is an EXPLICIT member of -- and resets to it on
|
|
126
|
+
// every load (camunda-hub #28956). That default is what a real user
|
|
127
|
+
// sees, so leave it alone and widen only when this cluster's group is
|
|
128
|
+
// genuinely not in it: the shared workspace is created by whichever
|
|
129
|
+
// setup user won the race, so a role-flow user otherwise lands on "You
|
|
130
|
+
// don't have explicit membership in any workspace in this
|
|
131
|
+
// organization" and the group never renders.
|
|
132
|
+
const groupUnderDefaultScope = await toggle
|
|
133
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
134
|
+
.then(() => true)
|
|
135
|
+
.catch(() => false);
|
|
136
|
+
if (!groupUnderDefaultScope) {
|
|
137
|
+
await this.showAllWorkspaces(5000);
|
|
138
|
+
}
|
|
139
|
+
await (0, test_1.expect)(toggle).toBeVisible({ timeout: 30000 });
|
|
140
|
+
}
|
|
141
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
|
|
142
|
+
// shape: "the design system splits the group row into a label control
|
|
143
|
+
// and a chevron control, both labelled by the cluster name; either one
|
|
144
|
+
// opens it" -- there is no separate "Toggle <name>" control, and the
|
|
145
|
+
// child links do not exist in the DOM at all until expanded (not merely
|
|
146
|
+
// hidden), nor are they nested under any container matchable by cluster
|
|
147
|
+
// name once expanded. So: find the toggle by the cluster's own name
|
|
148
|
+
// (`.first()` because both controls share that name), click it, then
|
|
149
|
+
// look up the app link sidebar-wide rather than scoped to a "group"
|
|
150
|
+
// container that doesn't reliably exist.
|
|
151
|
+
// Check THIS cluster's own toggle state, not "is any app link with this
|
|
152
|
+
// name visible anywhere in the sidebar" -- with more than one cluster in
|
|
153
|
+
// the org, a DIFFERENT cluster's group can already be expanded (left over
|
|
154
|
+
// from an earlier action on the same page), and that cluster's own
|
|
155
|
+
// Operate/Tasklist/Optimize link would satisfy an unscoped visibility
|
|
156
|
+
// check, skipping expansion of the requested cluster entirely and then
|
|
157
|
+
// clicking the wrong cluster's link. The child links are not nested under
|
|
158
|
+
// any container matchable by cluster name once expanded (confirmed
|
|
159
|
+
// against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
|
|
160
|
+
// own aria-expanded is the only reliable per-cluster signal available.
|
|
161
|
+
// Not exact: the same suite asserts this row with a substring/regex
|
|
162
|
+
// match (`{name: /Production/}`), not an exact string -- and nightly
|
|
163
|
+
// traces confirm why: the rendered toggle's accessible name is not
|
|
164
|
+
// byte-for-byte the bare cluster name (it carries additional content,
|
|
165
|
+
// e.g. a trailing tag summary), so an exact-match lookup here finds
|
|
166
|
+
// nothing and the click below times out waiting on a locator that can
|
|
167
|
+
// never resolve.
|
|
168
|
+
const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
|
|
169
|
+
if (!alreadyExpanded) {
|
|
170
|
+
// This is the only expansion action in the Hub path -- swallowing its
|
|
171
|
+
// failure made a broken expansion look successful and left the caller
|
|
172
|
+
// to burn the full 30s below waiting on an app link that could never
|
|
173
|
+
// appear, hiding the actionable error (e.g. a toggle that doesn't
|
|
174
|
+
// exist or isn't clickable) behind an unrelated timeout.
|
|
175
|
+
await toggle.click({ timeout: 10000 });
|
|
176
|
+
}
|
|
177
|
+
// An app link resolved after this is sidebar-wide (see callers below), so
|
|
178
|
+
// if an EARLIER call in the same test left a different cluster's group
|
|
179
|
+
// expanded too, two "Operate" (say) links now exist and that becomes a
|
|
180
|
+
// strict-mode violation. Collapse every other expanded group before
|
|
181
|
+
// resolving it -- there is no container to scope the link to instead,
|
|
182
|
+
// since the child links aren't nested under anything matchable by
|
|
183
|
+
// cluster name once expanded.
|
|
184
|
+
// hasNotText filters DOM text content, not the accessible name -- the
|
|
185
|
+
// expanded chevron can be labelled via aria-label with no text content
|
|
186
|
+
// of its own, in which case hasNotText would fail to exclude the toggle
|
|
187
|
+
// we just expanded and this loop would immediately collapse it again.
|
|
188
|
+
// Match by accessible name (same algorithm getByRole's own name option
|
|
189
|
+
// uses) instead, excluding any name CONTAINING clusterName -- not just
|
|
190
|
+
// one exactly equal to it, since (per the toggle locator above) the
|
|
191
|
+
// current cluster's own toggle name is not exactly clusterName either,
|
|
192
|
+
// and an exact-equality exclusion would wrongly treat it as "other" and
|
|
193
|
+
// immediately re-collapse the group this method just expanded.
|
|
194
|
+
const otherExpandedToggles = this.hubSidebar.getByRole('button', {
|
|
195
|
+
expanded: true,
|
|
196
|
+
name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
|
|
197
|
+
});
|
|
198
|
+
for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
|
|
199
|
+
// Let a failed collapse throw rather than swallowing it: the app link
|
|
200
|
+
// is intentionally sidebar-wide, so a group left expanded here produces
|
|
201
|
+
// a strict-mode violation (or the wrong cluster's link) below anyway --
|
|
202
|
+
// failing at the actual cause is more useful than at that symptom.
|
|
203
|
+
await otherExpandedToggles.first().click({ timeout: 10000 });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// Widens Hub's Workspaces scope filter to "All workspaces". Duplicated from
|
|
207
|
+
// ModelerHomePage.showAllWorkspaces() rather than imported: ModelerHomePage
|
|
208
|
+
// constructs page objects of its own, and pulling it in here would make the
|
|
209
|
+
// Apps -> Modeler navigation path circular. The control is absent outside
|
|
210
|
+
// Hub and for non-elevated users, where a membership-scoped list is already
|
|
211
|
+
// correct, so a missing control is a no-op.
|
|
212
|
+
async showAllWorkspaces(probeTimeout) {
|
|
213
|
+
// The empty state offers an explicit "Switch to All workspaces" button
|
|
214
|
+
// next to "You don't have explicit membership in any workspace in this
|
|
215
|
+
// organization" -- prefer it. Driving the combobox alone was not enough:
|
|
216
|
+
// when its accessible name does not match, the probe below simply
|
|
217
|
+
// returns and the caller waits out its whole timeout on a page that is
|
|
218
|
+
// still scoped to "My workspaces" (observed live, twice).
|
|
219
|
+
const switchButton = this.page.getByRole('button', {
|
|
220
|
+
name: 'Switch to All workspaces',
|
|
221
|
+
});
|
|
222
|
+
const scopeFilter = this.page.getByRole('combobox', {
|
|
223
|
+
name: 'Filter workspaces',
|
|
224
|
+
});
|
|
225
|
+
const appeared = await switchButton
|
|
226
|
+
.or(scopeFilter)
|
|
227
|
+
.first()
|
|
228
|
+
.waitFor({ state: 'visible', timeout: probeTimeout })
|
|
229
|
+
.then(() => true)
|
|
230
|
+
.catch(() => false);
|
|
231
|
+
if (!appeared) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (await switchButton.isVisible().catch(() => false)) {
|
|
235
|
+
await switchButton.click({ timeout: 10000 });
|
|
236
|
+
await (0, test_1.expect)(switchButton).toBeHidden({ timeout: 10000 });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
if ((await scopeFilter.innerText()).includes('All workspaces')) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
await scopeFilter.click({ timeout: 10000 });
|
|
243
|
+
await this.page
|
|
244
|
+
.getByRole('option', { name: 'All workspaces' })
|
|
245
|
+
.click({ timeout: 10000 });
|
|
246
|
+
await (0, test_1.expect)(scopeFilter).toContainText('All workspaces', {
|
|
247
|
+
timeout: 10000,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
88
250
|
async clickHubClusterApp(clusterName, appName) {
|
|
89
251
|
// When the sidebar is collapsed (the default on a fresh session), the
|
|
90
252
|
// group toggle opens a Radix popover -- a role="dialog" labelled with the
|
|
@@ -104,12 +266,12 @@ class AppsPage {
|
|
|
104
266
|
// multi-cluster org another cluster's group can already be expanded with
|
|
105
267
|
// the same app name, and the click would then fire on the wrong
|
|
106
268
|
// cluster's link.
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
269
|
+
// Expansion, sidebar recovery and collapsing every OTHER expanded group
|
|
270
|
+
// all live in the ported helper: with more than one cluster in the org
|
|
271
|
+
// (this suite creates Test Cluster and AWS Cluster), a sidebar-wide app
|
|
272
|
+
// link can otherwise resolve to a different cluster's row, or become a
|
|
273
|
+
// strict-mode violation once two groups are open.
|
|
274
|
+
await this.expandHubClusterGroup(clusterName);
|
|
113
275
|
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
114
276
|
// Hub's cluster-app links carry target="_blank", so clicking them opens
|
|
115
277
|
// the app in a new tab while every caller keeps asserting on this.page.
|
|
@@ -122,6 +284,11 @@ class AppsPage {
|
|
|
122
284
|
}
|
|
123
285
|
await appLink.click({ timeout: 10000 });
|
|
124
286
|
}
|
|
287
|
+
// True once the tab is on a Camunda Hub host, where Modeler home is Hub's
|
|
288
|
+
// own home rather than a modeler.* hostname.
|
|
289
|
+
isOnHub() {
|
|
290
|
+
return new URL(this.page.url()).hostname.startsWith('hub.');
|
|
291
|
+
}
|
|
125
292
|
async clickModeler() {
|
|
126
293
|
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
127
294
|
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
@@ -132,22 +299,7 @@ class AppsPage {
|
|
|
132
299
|
// Navigate home through the header logo link instead, same as
|
|
133
300
|
// ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
|
|
134
301
|
// Modeler home is a harmless in-SPA no-op.
|
|
135
|
-
if (
|
|
136
|
-
const header = this.page.locator('header[data-slot="app-header"]');
|
|
137
|
-
// The href stays literally "/login" until the client's auth state
|
|
138
|
-
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
139
|
-
// clicking it in that window would log the test out instead of
|
|
140
|
-
// navigating home, so exclude that href and wait for the authenticated
|
|
141
|
-
// one rather than racing it.
|
|
142
|
-
const homeLink = header
|
|
143
|
-
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
144
|
-
.and(this.page.locator(':not([href$="/login"])'))
|
|
145
|
-
.or(header
|
|
146
|
-
.getByRole('link', { name: 'Camunda Hub home' })
|
|
147
|
-
.and(this.page.locator(':not([href$="/login"])')))
|
|
148
|
-
.first();
|
|
149
|
-
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
150
|
-
await homeLink.click({ timeout: 30000 });
|
|
302
|
+
if (await this.clickHubModelerHome()) {
|
|
151
303
|
return;
|
|
152
304
|
}
|
|
153
305
|
const maxRetries = 5;
|
|
@@ -160,6 +312,17 @@ class AppsPage {
|
|
|
160
312
|
}
|
|
161
313
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
162
314
|
try {
|
|
315
|
+
// Re-checked every iteration, not once before the loop. A caller can
|
|
316
|
+
// reach here from a cluster app (Operate/Tasklist/Identity), which is
|
|
317
|
+
// NOT a hub.* host, so the pre-loop check above takes the legacy
|
|
318
|
+
// branch -- but the reload in the catch below lands back on Hub, and
|
|
319
|
+
// the loop's own success signal (a modeler.* hostname) can never come
|
|
320
|
+
// true there. A nightly trace shows exactly that: every retry
|
|
321
|
+
// snapshot sits on hub.ultrawombat.com/workspaces, which IS Modeler
|
|
322
|
+
// home, while the loop kept failing until it ran out of attempts.
|
|
323
|
+
if (await this.clickHubModelerHome()) {
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
163
326
|
await this.clickCamundaApps();
|
|
164
327
|
await (0, test_1.expect)(this.modelerLink).toBeVisible({ timeout: 30000 });
|
|
165
328
|
await this.modelerLink.click({ timeout: 30000 });
|
|
@@ -194,6 +357,30 @@ class AppsPage {
|
|
|
194
357
|
}
|
|
195
358
|
throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
|
|
196
359
|
}
|
|
360
|
+
// Navigates to Hub's Modeler home through the header logo link. Returns
|
|
361
|
+
// false when this tab is not on a Hub host, so the caller falls through to
|
|
362
|
+
// the legacy Console app-switcher flow.
|
|
363
|
+
async clickHubModelerHome() {
|
|
364
|
+
if (!this.isOnHub()) {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
368
|
+
// The href stays literally "/login" until the client's auth state
|
|
369
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
370
|
+
// clicking it in that window would log the test out instead of
|
|
371
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
372
|
+
// one rather than racing it.
|
|
373
|
+
const homeLink = header
|
|
374
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
375
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
376
|
+
.or(header
|
|
377
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
378
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
379
|
+
.first();
|
|
380
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
381
|
+
await homeLink.click({ timeout: 30000 });
|
|
382
|
+
return true;
|
|
383
|
+
}
|
|
197
384
|
/**
|
|
198
385
|
* Waits for the Modeler app to actually render, and recovers the one landing
|
|
199
386
|
* state a hostname check cannot see: Modeler's OAuth `/login-callback`
|
|
@@ -22,8 +22,13 @@ declare class ClusterDetailsPage {
|
|
|
22
22
|
readonly clientCredentialsDialog: Locator;
|
|
23
23
|
readonly clustersLink: Locator;
|
|
24
24
|
readonly alertsTab: Locator;
|
|
25
|
+
readonly alertsPanel: Locator;
|
|
25
26
|
readonly activeTabPanel: Locator;
|
|
26
27
|
readonly alertsList: Locator;
|
|
28
|
+
readonly alertExpandRowButtons: Locator;
|
|
29
|
+
readonly alertRowActionsButtons: Locator;
|
|
30
|
+
readonly deleteAlertMenuItem: Locator;
|
|
31
|
+
readonly deleteAlertConfirmButton: Locator;
|
|
27
32
|
readonly createFirstAlertButton: Locator;
|
|
28
33
|
readonly createNewAlertButton: Locator;
|
|
29
34
|
readonly emailRadioButton: Locator;
|
|
@@ -37,6 +42,7 @@ declare class ClusterDetailsPage {
|
|
|
37
42
|
readonly expandButton: Locator;
|
|
38
43
|
readonly rbaEnabledMessage: Locator;
|
|
39
44
|
readonly authorizationsHeading: Locator;
|
|
45
|
+
readonly authorizationsSwitch: Locator;
|
|
40
46
|
readonly orchestrationClusterCheckbox: Locator;
|
|
41
47
|
readonly clientCredentialsLink: (clientCredentials: string) => Locator;
|
|
42
48
|
readonly clientRow: (name: string) => Locator;
|
|
@@ -64,6 +70,7 @@ declare class ClusterDetailsPage {
|
|
|
64
70
|
clickCloseModalButton(): Promise<void>;
|
|
65
71
|
clickSettingsTab(): Promise<void>;
|
|
66
72
|
private waitForAuthorizationsSection;
|
|
73
|
+
private setAuthorizationsViaSwitch;
|
|
67
74
|
enableAuthorizations(): Promise<void>;
|
|
68
75
|
disableAuthorizations(): Promise<void>;
|
|
69
76
|
searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
|
|
@@ -76,6 +83,8 @@ declare class ClusterDetailsPage {
|
|
|
76
83
|
selectEmailRadioButton(): Promise<void>;
|
|
77
84
|
clickCreateAlertButton(): Promise<void>;
|
|
78
85
|
deleteAlerts(): Promise<void>;
|
|
86
|
+
ensureAlertsTabSelected(): Promise<void>;
|
|
87
|
+
expandAlerts(): Promise<void>;
|
|
79
88
|
private confirmDelete;
|
|
80
89
|
private dismissDeleteDialog;
|
|
81
90
|
private doDelete;
|
|
@@ -28,8 +28,13 @@ class ClusterDetailsPage {
|
|
|
28
28
|
clientCredentialsDialog;
|
|
29
29
|
clustersLink;
|
|
30
30
|
alertsTab;
|
|
31
|
+
alertsPanel;
|
|
31
32
|
activeTabPanel;
|
|
32
33
|
alertsList;
|
|
34
|
+
alertExpandRowButtons;
|
|
35
|
+
alertRowActionsButtons;
|
|
36
|
+
deleteAlertMenuItem;
|
|
37
|
+
deleteAlertConfirmButton;
|
|
33
38
|
createFirstAlertButton;
|
|
34
39
|
createNewAlertButton;
|
|
35
40
|
emailRadioButton;
|
|
@@ -43,6 +48,7 @@ class ClusterDetailsPage {
|
|
|
43
48
|
expandButton;
|
|
44
49
|
rbaEnabledMessage;
|
|
45
50
|
authorizationsHeading;
|
|
51
|
+
authorizationsSwitch;
|
|
46
52
|
orchestrationClusterCheckbox;
|
|
47
53
|
clientCredentialsLink;
|
|
48
54
|
clientRow;
|
|
@@ -123,12 +129,39 @@ class ClusterDetailsPage {
|
|
|
123
129
|
this.alertsList = this.activeTabPanel
|
|
124
130
|
.getByRole('row')
|
|
125
131
|
.filter({ hasNotText: 'Delivery method' }); //Filter out header row
|
|
132
|
+
// Hub's Alerts tab is a design-system data table: one summary row per
|
|
133
|
+
// alert, its detail fields in a sibling expansion row that only exists
|
|
134
|
+
// once the row's "Expand row" toggle is clicked, and Delete behind the
|
|
135
|
+
// row's "Row actions" kebab -> "Delete" -> "Delete alert" dialog. A
|
|
136
|
+
// nightly trace's DOM carries aria-label="Row actions" and no row-level
|
|
137
|
+
// "Delete" button at all, which is why doDelete() below saw "no items"
|
|
138
|
+
// and silently deleted nothing. Same shape as pages/8.9, scoped to the
|
|
139
|
+
// visible tab panel.
|
|
140
|
+
this.alertExpandRowButtons = this.activeTabPanel.getByRole('button', {
|
|
141
|
+
name: 'Expand row',
|
|
142
|
+
exact: true,
|
|
143
|
+
});
|
|
144
|
+
this.alertRowActionsButtons = this.activeTabPanel.getByRole('button', {
|
|
145
|
+
name: 'Row actions',
|
|
146
|
+
exact: true,
|
|
147
|
+
});
|
|
148
|
+
this.deleteAlertMenuItem = page.getByRole('menuitem', {
|
|
149
|
+
name: 'Delete',
|
|
150
|
+
exact: true,
|
|
151
|
+
});
|
|
152
|
+
this.deleteAlertConfirmButton = page
|
|
153
|
+
.getByRole('dialog', { name: 'Delete alert' })
|
|
154
|
+
.getByRole('button', { name: 'Delete', exact: true });
|
|
126
155
|
this.createFirstAlertButton = page.getByRole('button', {
|
|
127
156
|
name: 'Create an alert',
|
|
128
157
|
});
|
|
129
158
|
this.createNewAlertButton = page.getByRole('button', {
|
|
130
159
|
name: 'Create new alert',
|
|
131
160
|
});
|
|
161
|
+
// The Alerts panel offers "Create an alert" while empty and "Create new
|
|
162
|
+
// alert" once it holds one, so either proves the panel itself mounted.
|
|
163
|
+
// Ported from pages/8.9.
|
|
164
|
+
this.alertsPanel = this.createFirstAlertButton.or(this.createNewAlertButton);
|
|
132
165
|
this.emailRadioButton = page.getByRole('radio', {
|
|
133
166
|
name: 'Email',
|
|
134
167
|
});
|
|
@@ -155,6 +188,14 @@ class ClusterDetailsPage {
|
|
|
155
188
|
});
|
|
156
189
|
this.expandButton = page.locator('button[class="cds--table-expand__button"]');
|
|
157
190
|
this.rbaEnabledMessage = page.getByText('Authorizations enabled');
|
|
191
|
+
// Hub's design-system Settings tab renders this as
|
|
192
|
+
// <button role="switch" data-slot="switch" aria-label="Switch Enable
|
|
193
|
+
// authorizations" aria-checked="true|false">, confirmed from a live
|
|
194
|
+
// trace. The Carbon `.cds--toggle__*` nodes the old code indexed into do
|
|
195
|
+
// not exist there at all.
|
|
196
|
+
this.authorizationsSwitch = this.page.getByRole('switch', {
|
|
197
|
+
name: /Enable authorizations/i,
|
|
198
|
+
});
|
|
158
199
|
this.authorizationsHeading = this.page.getByRole('heading', {
|
|
159
200
|
name: 'Authorizations',
|
|
160
201
|
});
|
|
@@ -280,9 +321,34 @@ class ClusterDetailsPage {
|
|
|
280
321
|
},
|
|
281
322
|
});
|
|
282
323
|
}
|
|
324
|
+
// Drives the design-system Authorizations switch. Returns false when this
|
|
325
|
+
// shell does not render one, so the caller can fall through to the legacy
|
|
326
|
+
// Carbon toggle.
|
|
327
|
+
async setAuthorizationsViaSwitch(enabled) {
|
|
328
|
+
if (!(await this.authorizationsSwitch.isVisible().catch(() => false))) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
|
|
332
|
+
if (isOn === enabled) {
|
|
333
|
+
return true;
|
|
334
|
+
}
|
|
335
|
+
await this.authorizationsSwitch.click({ timeout: 30000 });
|
|
336
|
+
if (await this.dialog.isVisible({ timeout: 15000 }).catch(() => false)) {
|
|
337
|
+
const confirm = enabled ? this.rbaEnableButton : this.rbaDisableButton;
|
|
338
|
+
if (await confirm.isVisible().catch(() => false)) {
|
|
339
|
+
await confirm.click({ timeout: 30000 });
|
|
340
|
+
}
|
|
341
|
+
await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
|
|
342
|
+
}
|
|
343
|
+
await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
283
346
|
async enableAuthorizations() {
|
|
284
347
|
await (0, sleep_1.sleep)(5000);
|
|
285
348
|
await this.waitForAuthorizationsSection();
|
|
349
|
+
if (await this.setAuthorizationsViaSwitch(true)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
286
352
|
// Locate all elements with class .cds--toggle__text
|
|
287
353
|
const toggleTextElements = await this.page.$$('.cds--toggle__text');
|
|
288
354
|
// Check if toggleTextElements has at least 2 elements
|
|
@@ -316,11 +382,22 @@ class ClusterDetailsPage {
|
|
|
316
382
|
}
|
|
317
383
|
}
|
|
318
384
|
else {
|
|
319
|
-
console.log
|
|
385
|
+
// Previously a console.log that returned as though the work were done.
|
|
386
|
+
// Under Hub neither `.cds--toggle__text` nor `.cds--toggle__switch`
|
|
387
|
+
// exists, so this branch was taken on every run: authorizations were
|
|
388
|
+
// never enabled, the cluster kept its "Authorizations disabled" badge,
|
|
389
|
+
// and nothing downstream noticed until a permission assertion failed
|
|
390
|
+
// somewhere far away.
|
|
391
|
+
throw new Error('Could not enable authorizations: the Settings tab rendered neither ' +
|
|
392
|
+
'the design-system "Enable authorizations" switch nor a Carbon ' +
|
|
393
|
+
'toggle pair.');
|
|
320
394
|
}
|
|
321
395
|
}
|
|
322
396
|
async disableAuthorizations() {
|
|
323
397
|
await this.waitForAuthorizationsSection();
|
|
398
|
+
if (await this.setAuthorizationsViaSwitch(false)) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
324
401
|
// Locate all elements with class .cds--toggle__text
|
|
325
402
|
const toggleTextElements = await this.page.$$('.cds--toggle__text');
|
|
326
403
|
// Check if toggleTextElements has at least 2 elements
|
|
@@ -354,7 +431,11 @@ class ClusterDetailsPage {
|
|
|
354
431
|
}
|
|
355
432
|
}
|
|
356
433
|
else {
|
|
357
|
-
|
|
434
|
+
// Same silent no-op as enableAuthorizations had: under Hub no Carbon
|
|
435
|
+
// toggle exists, so this returned as though it had disabled them.
|
|
436
|
+
throw new Error('Could not disable authorizations: the Settings tab rendered neither ' +
|
|
437
|
+
'the design-system "Enable authorizations" switch nor a Carbon ' +
|
|
438
|
+
'toggle pair.');
|
|
358
439
|
}
|
|
359
440
|
}
|
|
360
441
|
async searchAndClickClientCredentialsLink(name) {
|
|
@@ -448,7 +529,65 @@ class ClusterDetailsPage {
|
|
|
448
529
|
});
|
|
449
530
|
}
|
|
450
531
|
async deleteAlerts() {
|
|
451
|
-
await this.
|
|
532
|
+
const hasRowActions = await this.alertRowActionsButtons
|
|
533
|
+
.first()
|
|
534
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
535
|
+
.then(() => true)
|
|
536
|
+
.catch(() => false);
|
|
537
|
+
if (!hasRowActions) {
|
|
538
|
+
await this.doDelete(this.dialog, 'Alerts');
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
let remaining = await this.alertRowActionsButtons.count();
|
|
542
|
+
while (remaining > 0) {
|
|
543
|
+
// A re-render can detach the menu between opening it and the click, so
|
|
544
|
+
// open-menu-and-choose-Delete is retried as one unit.
|
|
545
|
+
await (0, test_1.expect)(async () => {
|
|
546
|
+
await this.alertRowActionsButtons.first().click({ timeout: 10000 });
|
|
547
|
+
await this.deleteAlertMenuItem.click({ timeout: 10000 });
|
|
548
|
+
await (0, test_1.expect)(this.deleteAlertConfirmButton).toBeVisible({
|
|
549
|
+
timeout: 10000,
|
|
550
|
+
});
|
|
551
|
+
}).toPass({ timeout: 60000 });
|
|
552
|
+
await this.deleteAlertConfirmButton.click({ timeout: 30000 });
|
|
553
|
+
await (0, test_1.expect)(this.alertRowActionsButtons).toHaveCount(remaining - 1, {
|
|
554
|
+
timeout: 60000,
|
|
555
|
+
});
|
|
556
|
+
remaining = await this.alertRowActionsButtons.count();
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
// Reveals each alert's detail row, where the design-system table keeps the
|
|
560
|
+
// recipient, creation date and trigger count the callers assert on. A no-op
|
|
561
|
+
// on the legacy table, whose detail rows are always rendered.
|
|
562
|
+
// Re-selects the Alerts tab when it is no longer the showing one. A
|
|
563
|
+
// page.reload() drops the tab selection and the cluster page comes back on
|
|
564
|
+
// Overview, whose Environments table supplies the very first row -- which
|
|
565
|
+
// is how `alertsList.nth(0)` came to read "NameAlert Flow Cluster" instead
|
|
566
|
+
// of "Email" (console-user-flows.spec.ts reloads and then reads the list
|
|
567
|
+
// straight away). `alertsList` is scoped to whichever tabpanel is visible,
|
|
568
|
+
// so restoring the tab is what makes those reads address the alerts table.
|
|
569
|
+
async ensureAlertsTabSelected() {
|
|
570
|
+
const selected = await this.alertsTab
|
|
571
|
+
.getAttribute('aria-selected')
|
|
572
|
+
.catch(() => null);
|
|
573
|
+
if (selected === 'true') {
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
await this.clickAlertsTab();
|
|
577
|
+
}
|
|
578
|
+
async expandAlerts() {
|
|
579
|
+
await this.ensureAlertsTabSelected();
|
|
580
|
+
for (let i = 0; i < 10; i++) {
|
|
581
|
+
const toggle = this.alertExpandRowButtons.first();
|
|
582
|
+
if (!(await toggle.isVisible().catch(() => false))) {
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const before = await this.alertExpandRowButtons.count();
|
|
586
|
+
await toggle.click({ timeout: 10000 });
|
|
587
|
+
await (0, test_1.expect)(this.alertExpandRowButtons).toHaveCount(before - 1, {
|
|
588
|
+
timeout: 10000,
|
|
589
|
+
});
|
|
590
|
+
}
|
|
452
591
|
}
|
|
453
592
|
// The confirmation modal only closes once the DELETE request resolves. When
|
|
454
593
|
// that call fails the modal stays open on an inline "We're having trouble
|
|
@@ -701,6 +840,18 @@ class ClusterDetailsPage {
|
|
|
701
840
|
console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
|
|
702
841
|
await this.page.reload();
|
|
703
842
|
await (0, sleep_1.sleep)(20000);
|
|
843
|
+
// A reload drops the tab selection, so the Alerts panel is no
|
|
844
|
+
// longer the visible one -- `alertsList` is scoped to whichever
|
|
845
|
+
// tabpanel is showing, which after a reload is Overview, whose
|
|
846
|
+
// first non-header row is the cluster-details "Name" row. That is
|
|
847
|
+
// exactly what the nightly reported: `Received string:
|
|
848
|
+
// "NameAlert Flow Cluster"`, nine times over, while the Email alert
|
|
849
|
+
// sat unread on a tab nobody had re-opened. Re-select the tab, wait
|
|
850
|
+
// for the panel to mount, and re-expand the detail rows the
|
|
851
|
+
// assertions read from.
|
|
852
|
+
await this.alertsTab.click({ timeout: 60000 });
|
|
853
|
+
await (0, test_1.expect)(this.alertsPanel).toBeVisible({ timeout: 60000 });
|
|
854
|
+
await this.expandAlerts();
|
|
704
855
|
}
|
|
705
856
|
else {
|
|
706
857
|
throw new Error(`Assertion failed after ${maxRetries} attempts`);
|
|
@@ -47,6 +47,7 @@ declare class ClusterPage {
|
|
|
47
47
|
private extractClusterIdFromUrl;
|
|
48
48
|
private logClusterCreationInfo;
|
|
49
49
|
createCluster(name: string, region?: string, tasklistV1Api?: boolean): Promise<void>;
|
|
50
|
+
clickProviderOption(region: string): Promise<void>;
|
|
50
51
|
selectSubRegion(region: string): Promise<void>;
|
|
51
52
|
determineTasklistAPI(tasklistV1Api?: boolean): Promise<void>;
|
|
52
53
|
clickClusterType(): Promise<void>;
|