@camunda/e2e-test-suite 0.0.1287 → 0.0.1288
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.9/AppsPage.d.ts +9 -3
- package/dist/pages/8.9/AppsPage.js +262 -47
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +92 -9
- package/dist/pages/8.9/ClusterSecretsPage.js +5 -1
- package/dist/pages/8.9/ConnectorMarketplacePage.d.ts +1 -0
- package/dist/pages/8.9/ConnectorMarketplacePage.js +46 -3
- package/dist/pages/8.9/ConsoleOrganizationPage.d.ts +3 -2
- package/dist/pages/8.9/ConsoleOrganizationPage.js +47 -22
- package/dist/pages/8.9/FormJsPage.d.ts +4 -0
- package/dist/pages/8.9/FormJsPage.js +53 -19
- package/dist/pages/8.9/IdpCreatePage.js +5 -0
- package/dist/pages/8.9/ModelerCreatePage.d.ts +5 -0
- package/dist/pages/8.9/ModelerCreatePage.js +153 -21
- package/dist/pages/8.9/ModelerHomePage.d.ts +1 -0
- package/dist/pages/8.9/ModelerHomePage.js +29 -4
- package/dist/pages/8.9/PlayPage.js +11 -1
- package/dist/pages/8.9/ProcessApplicationPage.js +48 -4
- package/dist/pages/8.9/UtilitiesPage.js +57 -17
- package/dist/pages/8.9/WorkspaceMembersPage.d.ts +21 -0
- package/dist/pages/8.9/WorkspaceMembersPage.js +264 -0
- package/dist/tests/8.9/access-user-role-management.spec.js +111 -26
- package/dist/tests/8.9/console-user-flows.spec.js +4 -4
- package/dist/tests/8.9/idp-user-flows.spec.js +2 -1
- package/dist/tests/8.9/rba-enabled-v1-user-flows.spec.js +13 -4
- package/dist/tests/8.9/rba-enabled-v2-user-flows.spec.js +13 -4
- package/dist/tests/8.9/test-setup.spec.js +57 -4
- package/dist/tests/8.9/utr-enabled-user-flows.spec.js +18 -0
- package/dist/tests/8.9/web-modeler-user-flows.spec.js +6 -2
- package/dist/utils/consoleApiHelpers.d.ts +2 -0
- package/dist/utils/consoleApiHelpers.js +50 -4
- package/dist/utils/resetSession.js +6 -1
- package/package.json +1 -1
|
@@ -19,9 +19,13 @@ declare class AppsPage {
|
|
|
19
19
|
readonly camundaComponentsButton: Locator;
|
|
20
20
|
readonly camundaAppsButton: Locator;
|
|
21
21
|
readonly hubSidebar: Locator;
|
|
22
|
+
private hubReturnUrl?;
|
|
22
23
|
constructor(page: Page);
|
|
23
24
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
25
|
+
private expandHubClusterGroup;
|
|
26
|
+
private showAllWorkspaces;
|
|
24
27
|
private clickHubClusterApp;
|
|
28
|
+
goBackToHub(): Promise<void>;
|
|
25
29
|
clickModeler(): Promise<void>;
|
|
26
30
|
clickTasklist(clusterName: string): Promise<void>;
|
|
27
31
|
clickAdmin(clusterName: string): Promise<void>;
|
|
@@ -35,8 +39,10 @@ declare class AppsPage {
|
|
|
35
39
|
clickCamundaApps(maxRetries?: number): Promise<void>;
|
|
36
40
|
private dismissBlockingOverlays;
|
|
37
41
|
clickConsoleLink(): Promise<void>;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
42
|
+
private hubClusterAppLink;
|
|
43
|
+
private assertHubClusterAppPresent;
|
|
44
|
+
assertOperatePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
|
|
45
|
+
assertTasklistPresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
|
|
46
|
+
assertOptimizePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
|
|
41
47
|
}
|
|
42
48
|
export { AppsPage };
|
|
@@ -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.9/UtilitiesPage");
|
|
7
|
+
function escapeRegExp(value) {
|
|
8
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
9
|
+
}
|
|
7
10
|
class AppsPage {
|
|
8
11
|
page;
|
|
9
12
|
modelerLink;
|
|
@@ -24,9 +27,18 @@ class AppsPage {
|
|
|
24
27
|
camundaComponentsButton;
|
|
25
28
|
camundaAppsButton;
|
|
26
29
|
hubSidebar;
|
|
30
|
+
hubReturnUrl;
|
|
27
31
|
constructor(page) {
|
|
28
32
|
this.page = page;
|
|
29
|
-
|
|
33
|
+
// Hub has no "Modeler" switcher entry; Modeler is Hub's home, reached
|
|
34
|
+
// through the header logo link clickModeler() uses
|
|
35
|
+
// (NavShellDs.common.tsx).
|
|
36
|
+
this.modelerLink = page
|
|
37
|
+
.getByRole('link', { name: 'Modeler', exact: true })
|
|
38
|
+
.or(page
|
|
39
|
+
.locator('header[data-slot="app-header"]')
|
|
40
|
+
.getByRole('link', { name: /^Camunda (Modeler|Hub) home$/ }))
|
|
41
|
+
.first();
|
|
30
42
|
// Camunda Hub's persistent "Main navigation" sidebar lists each cluster
|
|
31
43
|
// with its own apps (Operate/Tasklist/Optimize/Identity) as child links
|
|
32
44
|
// -- there is no per-app switcher panel to open on Hub (see
|
|
@@ -83,15 +95,186 @@ class AppsPage {
|
|
|
83
95
|
}
|
|
84
96
|
// Camunda Hub's sidebar groups by cluster, not by app: each cluster row
|
|
85
97
|
// expands to reveal its own per-app child links (Operate/Tasklist/...).
|
|
86
|
-
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape:
|
|
98
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: the
|
|
87
99
|
// design system splits the group row into a label control and a chevron
|
|
88
|
-
// control, both labelled by the cluster name; either one opens it
|
|
100
|
+
// control, both labelled by the cluster name; either one opens it -- and
|
|
89
101
|
// the child links do not exist in the DOM at all until expanded (not just
|
|
90
102
|
// hidden), and are not nested under any container matchable by cluster
|
|
91
103
|
// name once expanded. So: find the toggle by the cluster's own name
|
|
92
|
-
// (`.first()` because both controls share that name), click it, then
|
|
93
|
-
//
|
|
94
|
-
//
|
|
104
|
+
// (`.first()` because both controls share that name), click it, then look
|
|
105
|
+
// up the app link sidebar-wide rather than scoped to a "group" container
|
|
106
|
+
// that doesn't reliably exist. Ported back from pages/8.8/AppsPage.ts.
|
|
107
|
+
async expandHubClusterGroup(clusterName) {
|
|
108
|
+
// Anchored at the start: the expanded row's accessible name is its text
|
|
109
|
+
// ("Test Cluster dev" -- name plus environment badge), so a plain
|
|
110
|
+
// substring match would also hit "UI Test Cluster", which this suite
|
|
111
|
+
// creates too and which can precede it in the sidebar.
|
|
112
|
+
const toggle = this.hubSidebar
|
|
113
|
+
.getByRole('button', { name: new RegExp(`^${escapeRegExp(clusterName)}`) })
|
|
114
|
+
.first();
|
|
115
|
+
// The diagram editor (and other full-screen views reached from it, e.g.
|
|
116
|
+
// right after runProcessInstance/deployDiagram) has no "Main navigation"
|
|
117
|
+
// sidebar at all -- confirmed from a nightly trace's DOM snapshot: the
|
|
118
|
+
// page's only <nav> is aria-label="Context navigation" (the breadcrumb),
|
|
119
|
+
// and neither "Operate"/"Tasklist"/"Optimize" nor any app-switcher text
|
|
120
|
+
// appears anywhere on it. clickCamundaApps() is a no-op on Hub (there is
|
|
121
|
+
// no switcher panel to open), so a caller reaching clickOperate/
|
|
122
|
+
// clickTasklist/clickOptimize straight from a diagram was left on a page
|
|
123
|
+
// this sidebar could never resolve on, no matter how the toggle itself
|
|
124
|
+
// is matched. Navigate home first, the same way clickModeler() does,
|
|
125
|
+
// whenever the sidebar isn't already there.
|
|
126
|
+
//
|
|
127
|
+
// A freshly re-authenticated page can also land on a workspace-agnostic
|
|
128
|
+
// route (e.g. bare /workspaces right after loginWithTestUser(), which
|
|
129
|
+
// clickCamundaApps() does nothing to fix -- it is a no-op on Hub) whose
|
|
130
|
+
// sidebar renders but has not resolved this cluster's own group yet.
|
|
131
|
+
// Confirmed from a nightly trace: both the getAttribute and click below
|
|
132
|
+
// timed out (10s each) against this exact toggle while the URL sat on
|
|
133
|
+
// /workspaces, right after a mid-test re-login. Route through the same
|
|
134
|
+
// clickModeler() recovery whenever the toggle itself hasn't shown up
|
|
135
|
+
// either, not only when the whole sidebar is missing.
|
|
136
|
+
// Leave the rail as Hub renders it: collapsed, every group is a visible
|
|
137
|
+
// flyout trigger. Opening the rail here made the group click that follows
|
|
138
|
+
// miss, likely because each label is re-wrapped in a truncation tooltip
|
|
139
|
+
// while the rail widens (8.8 reverted the same step in 3ae39d3).
|
|
140
|
+
const sidebarReady = (await this.hubSidebar.isVisible({ timeout: 5000 }).catch(() => false)) &&
|
|
141
|
+
(await toggle
|
|
142
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
143
|
+
.then(() => true)
|
|
144
|
+
.catch(() => false));
|
|
145
|
+
if (!sidebarReady) {
|
|
146
|
+
await this.clickModeler();
|
|
147
|
+
await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
|
|
148
|
+
// Since camunda-hub #28956 the Workspaces overview defaults to "My
|
|
149
|
+
// workspaces" -- only the ones the current user is an EXPLICIT member
|
|
150
|
+
// of -- and resets to it on every load. The shared workspace is created
|
|
151
|
+
// by whichever setup user won the race, so a role-flow user lands on
|
|
152
|
+
// "No workspaces match this filter / You don't have explicit membership
|
|
153
|
+
// in any workspace in this organization" (confirmed live) and the
|
|
154
|
+
// sidebar never renders this cluster's group. Widening the scope is
|
|
155
|
+
// what makes it appear; without it the wait below can only time out.
|
|
156
|
+
// ModelerHomePage.showAllWorkspaces() covers the project-folder paths,
|
|
157
|
+
// but nothing covered the app-switcher path until here.
|
|
158
|
+
await this.showAllWorkspaces(5000);
|
|
159
|
+
await (0, test_1.expect)(toggle).toBeVisible({ timeout: 30000 });
|
|
160
|
+
}
|
|
161
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
|
|
162
|
+
// shape: "the design system splits the group row into a label control
|
|
163
|
+
// and a chevron control, both labelled by the cluster name; either one
|
|
164
|
+
// opens it" -- there is no separate "Toggle <name>" control, and the
|
|
165
|
+
// child links do not exist in the DOM at all until expanded (not merely
|
|
166
|
+
// hidden), nor are they nested under any container matchable by cluster
|
|
167
|
+
// name once expanded. So: find the toggle by the cluster's own name
|
|
168
|
+
// (`.first()` because both controls share that name), click it, then
|
|
169
|
+
// look up the app link sidebar-wide rather than scoped to a "group"
|
|
170
|
+
// container that doesn't reliably exist.
|
|
171
|
+
// Check THIS cluster's own toggle state, not "is any app link with this
|
|
172
|
+
// name visible anywhere in the sidebar" -- with more than one cluster in
|
|
173
|
+
// the org, a DIFFERENT cluster's group can already be expanded (left over
|
|
174
|
+
// from an earlier action on the same page), and that cluster's own
|
|
175
|
+
// Operate/Tasklist/Optimize link would satisfy an unscoped visibility
|
|
176
|
+
// check, skipping expansion of the requested cluster entirely and then
|
|
177
|
+
// clicking the wrong cluster's link. The child links are not nested under
|
|
178
|
+
// any container matchable by cluster name once expanded (confirmed
|
|
179
|
+
// against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
|
|
180
|
+
// own aria-expanded is the only reliable per-cluster signal available.
|
|
181
|
+
// Not exact: the same suite asserts this row with a substring/regex
|
|
182
|
+
// match (`{name: /Production/}`), not an exact string -- and nightly
|
|
183
|
+
// traces confirm why: the rendered toggle's accessible name is not
|
|
184
|
+
// byte-for-byte the bare cluster name (it carries additional content,
|
|
185
|
+
// e.g. a trailing tag summary), so an exact-match lookup here finds
|
|
186
|
+
// nothing and the click below times out waiting on a locator that can
|
|
187
|
+
// never resolve.
|
|
188
|
+
const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
|
|
189
|
+
if (!alreadyExpanded) {
|
|
190
|
+
// This is the only expansion action in the Hub path -- swallowing its
|
|
191
|
+
// failure made a broken expansion look successful and left the caller
|
|
192
|
+
// to burn the full 30s below waiting on an app link that could never
|
|
193
|
+
// appear, hiding the actionable error (e.g. a toggle that doesn't
|
|
194
|
+
// exist or isn't clickable) behind an unrelated timeout.
|
|
195
|
+
await toggle.click({ timeout: 10000 });
|
|
196
|
+
}
|
|
197
|
+
// An app link resolved after this is sidebar-wide (see callers below), so
|
|
198
|
+
// if an EARLIER call in the same test left a different cluster's group
|
|
199
|
+
// expanded too, two "Operate" (say) links now exist and that becomes a
|
|
200
|
+
// strict-mode violation. Collapse every other expanded group before
|
|
201
|
+
// resolving it -- there is no container to scope the link to instead,
|
|
202
|
+
// since the child links aren't nested under anything matchable by
|
|
203
|
+
// cluster name once expanded.
|
|
204
|
+
// hasNotText filters DOM text content, not the accessible name -- the
|
|
205
|
+
// expanded chevron can be labelled via aria-label with no text content
|
|
206
|
+
// of its own, in which case hasNotText would fail to exclude the toggle
|
|
207
|
+
// we just expanded and this loop would immediately collapse it again.
|
|
208
|
+
// Match by accessible name (same algorithm getByRole's own name option
|
|
209
|
+
// uses) instead, excluding any name CONTAINING clusterName -- not just
|
|
210
|
+
// one exactly equal to it, since (per the toggle locator above) the
|
|
211
|
+
// current cluster's own toggle name is not exactly clusterName either,
|
|
212
|
+
// and an exact-equality exclusion would wrongly treat it as "other" and
|
|
213
|
+
// immediately re-collapse the group this method just expanded.
|
|
214
|
+
// The sidebar's own rail toggle is also a button inside "Main navigation"
|
|
215
|
+
// with aria-expanded="true" once the rail is open ("Collapse sidebar").
|
|
216
|
+
// Without excluding it this loop clicked it right after the group was
|
|
217
|
+
// expanded, collapsing the rail and taking the app links with it -- the
|
|
218
|
+
// 8.8 nightly trace after #3746 shows exactly that sequence (expand
|
|
219
|
+
// "Test Cluster", count 1, click, app link never appears).
|
|
220
|
+
const otherExpandedToggles = this.hubSidebar
|
|
221
|
+
.getByRole('button', {
|
|
222
|
+
expanded: true,
|
|
223
|
+
name: new RegExp(`^(?!.*${escapeRegExp(clusterName)}).+$`),
|
|
224
|
+
})
|
|
225
|
+
.and(this.page.locator(':not([data-slot="app-sidebar-collapse-toggle"])'));
|
|
226
|
+
for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
|
|
227
|
+
// Let a failed collapse throw rather than swallowing it: the app link
|
|
228
|
+
// is intentionally sidebar-wide, so a group left expanded here produces
|
|
229
|
+
// a strict-mode violation (or the wrong cluster's link) below anyway --
|
|
230
|
+
// failing at the actual cause is more useful than at that symptom.
|
|
231
|
+
await otherExpandedToggles.first().click({ timeout: 10000 });
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// Widens Hub's Workspaces scope filter to "All workspaces". Duplicated from
|
|
235
|
+
// ModelerHomePage.showAllWorkspaces() rather than imported: ModelerHomePage
|
|
236
|
+
// constructs page objects of its own, and pulling it in here would make the
|
|
237
|
+
// Apps -> Modeler navigation path circular. The control is absent outside
|
|
238
|
+
// Hub and for non-elevated users, where a membership-scoped list is already
|
|
239
|
+
// correct, so a missing control is a no-op.
|
|
240
|
+
async showAllWorkspaces(probeTimeout) {
|
|
241
|
+
// The empty state offers an explicit "Switch to All workspaces" button
|
|
242
|
+
// next to "You don't have explicit membership in any workspace in this
|
|
243
|
+
// organization" -- prefer it. Driving the combobox alone was not enough:
|
|
244
|
+
// when its accessible name does not match, the probe below simply
|
|
245
|
+
// returns and the caller waits out its whole timeout on a page that is
|
|
246
|
+
// still scoped to "My workspaces" (observed live, twice).
|
|
247
|
+
const switchButton = this.page.getByRole('button', {
|
|
248
|
+
name: 'Switch to All workspaces',
|
|
249
|
+
});
|
|
250
|
+
const scopeFilter = this.page.getByRole('combobox', {
|
|
251
|
+
name: 'Filter workspaces',
|
|
252
|
+
});
|
|
253
|
+
const appeared = await switchButton
|
|
254
|
+
.or(scopeFilter)
|
|
255
|
+
.first()
|
|
256
|
+
.waitFor({ state: 'visible', timeout: probeTimeout })
|
|
257
|
+
.then(() => true)
|
|
258
|
+
.catch(() => false);
|
|
259
|
+
if (!appeared) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (await switchButton.isVisible().catch(() => false)) {
|
|
263
|
+
await switchButton.click({ timeout: 10000 });
|
|
264
|
+
await (0, test_1.expect)(switchButton).toBeHidden({ timeout: 10000 });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if ((await scopeFilter.innerText()).includes('All workspaces')) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
await scopeFilter.click({ timeout: 10000 });
|
|
271
|
+
await this.page
|
|
272
|
+
.getByRole('option', { name: 'All workspaces' })
|
|
273
|
+
.click({ timeout: 10000 });
|
|
274
|
+
await (0, test_1.expect)(scopeFilter).toContainText('All workspaces', {
|
|
275
|
+
timeout: 10000,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
95
278
|
async clickHubClusterApp(clusterName, appName) {
|
|
96
279
|
// When the sidebar is collapsed (the default on a fresh session), the
|
|
97
280
|
// group toggle opens a Radix popover -- a role="dialog" labelled with the
|
|
@@ -101,39 +284,14 @@ class AppsPage {
|
|
|
101
284
|
// Cluster (opens in a new tab)" et al., yet the nav-scoped link never
|
|
102
285
|
// resolves). Look in that popover first; keep the sidebar-scoped lookup
|
|
103
286
|
// for the expanded-sidebar layout, where the links render inline.
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
// trace, UTR Candidate Group).
|
|
113
|
-
const appLink = this.page
|
|
114
|
-
.getByRole('dialog', { name: clusterName, exact: true })
|
|
115
|
-
.getByRole('link', { name: appName })
|
|
116
|
-
.or(this.hubSidebar
|
|
117
|
-
.getByRole('link', { name: appName })
|
|
118
|
-
.and(this.hubSidebar.getByRole('link', { name: ` in ${clusterName}` })))
|
|
119
|
-
.first();
|
|
120
|
-
// Decide whether to expand from clusterName's OWN toggle state, not from
|
|
121
|
-
// whether "some" appLink is already visible sidebar-wide: in a
|
|
122
|
-
// multi-cluster org another cluster's group can already be expanded with
|
|
123
|
-
// the same app name, which made this check pass while clusterName's own
|
|
124
|
-
// group stayed collapsed -- appLink.click() below then fired on the
|
|
125
|
-
// wrong cluster's link (see PR #3627 review).
|
|
126
|
-
const collapsedToggle = this.hubSidebar
|
|
127
|
-
.getByRole('button', { name: clusterName, expanded: false })
|
|
128
|
-
.first();
|
|
129
|
-
if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
130
|
-
// This is the only expansion action in the Hub path -- swallowing its
|
|
131
|
-
// failure made a broken expansion look successful and left the caller
|
|
132
|
-
// to burn the full 30s below waiting on an app link that could never
|
|
133
|
-
// appear, hiding the actionable error (e.g. a toggle that doesn't
|
|
134
|
-
// exist or isn't clickable) behind an unrelated timeout.
|
|
135
|
-
await collapsedToggle.click({ timeout: 10000 });
|
|
136
|
-
}
|
|
287
|
+
const appLink = this.hubClusterAppLink(clusterName, appName);
|
|
288
|
+
// Expansion, sidebar recovery and collapsing every OTHER expanded group
|
|
289
|
+
// all live in the ported helper. Every caller in this suite reaches here
|
|
290
|
+
// straight from the diagram editor after "Deploy & run", a view with no
|
|
291
|
+
// "Main navigation" sidebar at all -- so the toggle lookup the old inline
|
|
292
|
+
// check did could never resolve there (nightly trace: the page's only
|
|
293
|
+
// chrome is the breadcrumb and the "Instance started!" toast).
|
|
294
|
+
await this.expandHubClusterGroup(clusterName);
|
|
137
295
|
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
138
296
|
// Hub's cluster-app links carry target="_blank", so clicking them opens
|
|
139
297
|
// the app in a new tab while every caller keeps asserting on this.page.
|
|
@@ -141,11 +299,22 @@ class AppsPage {
|
|
|
141
299
|
// navigation the Console app switcher gave these callers.
|
|
142
300
|
const href = await appLink.getAttribute('href');
|
|
143
301
|
if (href) {
|
|
302
|
+
this.hubReturnUrl = this.page.url();
|
|
144
303
|
await this.page.goto(new URL(href, this.page.url()).toString());
|
|
145
304
|
return;
|
|
146
305
|
}
|
|
147
306
|
await appLink.click({ timeout: 10000 });
|
|
148
307
|
}
|
|
308
|
+
// Returns from an app that clickHubClusterApp opened. page.goBack() alone
|
|
309
|
+
// can stop on one of the app's own login-redirect entries, and the next
|
|
310
|
+
// Hub sidebar lookup then runs against the app instead of Hub.
|
|
311
|
+
async goBackToHub() {
|
|
312
|
+
await this.page.goBack();
|
|
313
|
+
if (this.hubReturnUrl &&
|
|
314
|
+
!new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
315
|
+
await this.page.goto(this.hubReturnUrl);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
149
318
|
async clickModeler() {
|
|
150
319
|
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
151
320
|
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
@@ -247,10 +416,6 @@ class AppsPage {
|
|
|
247
416
|
}
|
|
248
417
|
async clickAdmin(clusterName) {
|
|
249
418
|
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
250
|
-
// Non-anchored, matching the substring lookup clickHubClusterApp uses
|
|
251
|
-
// for Operate/Tasklist/Optimize -- an anchored pattern fails the same
|
|
252
|
-
// way an exact-string match did for those apps if the rendered link's
|
|
253
|
-
// accessible name carries any extra content (icon/badge).
|
|
254
419
|
await this.clickHubClusterApp(clusterName, /Admin|Identity/);
|
|
255
420
|
return;
|
|
256
421
|
}
|
|
@@ -488,8 +653,14 @@ class AppsPage {
|
|
|
488
653
|
// separate app to switch into. Its header banner (homePage.consoleBanner,
|
|
489
654
|
// via hubHeader) is already visible from any Hub page, so there is no
|
|
490
655
|
// navigation action needed here -- mirrors the same hostname guard in
|
|
491
|
-
// clickModeler().
|
|
656
|
+
// clickModeler(). The diagram editor is the exception: it has no "Main
|
|
657
|
+
// navigation" sidebar, so a caller arriving from Deploy & run would find
|
|
658
|
+
// no Clusters/Environments link. Go home first there.
|
|
492
659
|
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
660
|
+
if (!(await this.hubSidebar.isVisible().catch(() => false))) {
|
|
661
|
+
await this.clickModeler();
|
|
662
|
+
await (0, test_1.expect)(this.hubSidebar).toBeVisible({ timeout: 30000 });
|
|
663
|
+
}
|
|
493
664
|
return;
|
|
494
665
|
}
|
|
495
666
|
const maxRetries = 3;
|
|
@@ -527,7 +698,41 @@ class AppsPage {
|
|
|
527
698
|
}
|
|
528
699
|
throw new Error(`Failed to click the console link after ${maxRetries} attempts.`);
|
|
529
700
|
}
|
|
530
|
-
|
|
701
|
+
// Hub's replacement for the app switcher: a cluster's apps are the child
|
|
702
|
+
// links of its sidebar group. Hub names every app link "<app> in
|
|
703
|
+
// <environment>", plus " (opens in a new tab)" when it leads to the live app
|
|
704
|
+
// (camunda-hub environment-sidebar-rows.ts). Anchor both halves: the
|
|
705
|
+
// flyout's first link is "Details of <environment>", so for "Admin Role
|
|
706
|
+
// Cluster" a bare /Admin|Identity/ picked that link, and inline it picked
|
|
707
|
+
// "Operate in Admin Role Cluster (opens in a new tab)".
|
|
708
|
+
hubClusterAppLink(clusterName, appName) {
|
|
709
|
+
const appPattern = typeof appName === 'string' ? escapeRegExp(appName) : appName.source;
|
|
710
|
+
const cluster = escapeRegExp(clusterName);
|
|
711
|
+
const appLinkName = new RegExp(`^(?:${appPattern}) in ${cluster}(?: \\(opens in a new tab\\))?$`);
|
|
712
|
+
return this.page
|
|
713
|
+
.getByRole('dialog', { name: clusterName, exact: true })
|
|
714
|
+
.getByRole('link', { name: appLinkName })
|
|
715
|
+
.or(this.hubSidebar.getByRole('link', { name: appLinkName }))
|
|
716
|
+
.first();
|
|
717
|
+
}
|
|
718
|
+
async assertHubClusterAppPresent(clusterName, appName, shouldBeVisible) {
|
|
719
|
+
const appLink = this.hubClusterAppLink(clusterName, appName);
|
|
720
|
+
// Not expanded for an absence check: expandHubClusterGroup throws when
|
|
721
|
+
// the user cannot see the environment at all, which is a pass here.
|
|
722
|
+
if (!shouldBeVisible) {
|
|
723
|
+
await (0, test_1.expect)(appLink).toBeHidden({ timeout: 30000 });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
await this.expandHubClusterGroup(clusterName);
|
|
727
|
+
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
728
|
+
}
|
|
729
|
+
// On Hub the cluster name is what scopes the lookup: there is no app
|
|
730
|
+
// switcher, only each cluster's own sidebar group.
|
|
731
|
+
async assertOperatePresent(clusterName, shouldBeVisible = true) {
|
|
732
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
733
|
+
await this.assertHubClusterAppPresent(clusterName, 'Operate', shouldBeVisible);
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
531
736
|
const maxRetries = 3;
|
|
532
737
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
533
738
|
try {
|
|
@@ -556,7 +761,12 @@ class AppsPage {
|
|
|
556
761
|
}
|
|
557
762
|
throw new Error(`Failed to assert the operate link visibility after ${maxRetries} attempts.`);
|
|
558
763
|
}
|
|
559
|
-
|
|
764
|
+
// See assertOperatePresent for the clusterName/Hub note.
|
|
765
|
+
async assertTasklistPresent(clusterName, shouldBeVisible = true) {
|
|
766
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
767
|
+
await this.assertHubClusterAppPresent(clusterName, 'Tasklist', shouldBeVisible);
|
|
768
|
+
return;
|
|
769
|
+
}
|
|
560
770
|
const maxRetries = 3;
|
|
561
771
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
562
772
|
try {
|
|
@@ -585,7 +795,12 @@ class AppsPage {
|
|
|
585
795
|
}
|
|
586
796
|
throw new Error(`Failed to assert the tasklist link visibility after ${maxRetries} attempts.`);
|
|
587
797
|
}
|
|
588
|
-
|
|
798
|
+
// See assertOperatePresent for the clusterName/Hub note.
|
|
799
|
+
async assertOptimizePresent(clusterName, shouldBeVisible = true) {
|
|
800
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
801
|
+
await this.assertHubClusterAppPresent(clusterName, 'Optimize', shouldBeVisible);
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
589
804
|
const maxRetries = 3;
|
|
590
805
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
591
806
|
try {
|
|
@@ -4,6 +4,7 @@ import { McpCredentials } from '../../utils/mcpSecrets';
|
|
|
4
4
|
declare class ClusterDetailsPage {
|
|
5
5
|
private page;
|
|
6
6
|
private pendingClientScopeCheck;
|
|
7
|
+
private clusterDetailsUrl;
|
|
7
8
|
readonly apiTab: Locator;
|
|
8
9
|
readonly createFirstClientButton: Locator;
|
|
9
10
|
readonly clientNameTextbox: Locator;
|
|
@@ -24,6 +25,7 @@ declare class ClusterDetailsPage {
|
|
|
24
25
|
readonly clientCredentialsDialog: Locator;
|
|
25
26
|
readonly clustersLink: Locator;
|
|
26
27
|
readonly alertsTab: Locator;
|
|
28
|
+
readonly alertsTabPanel: Locator;
|
|
27
29
|
readonly alertsList: Locator;
|
|
28
30
|
readonly alertExpandRowButtons: Locator;
|
|
29
31
|
readonly alertRowActionsButtons: Locator;
|
|
@@ -43,6 +45,7 @@ declare class ClusterDetailsPage {
|
|
|
43
45
|
readonly expandButton: Locator;
|
|
44
46
|
readonly rbaEnabledMessage: Locator;
|
|
45
47
|
readonly authorizationsHeading: Locator;
|
|
48
|
+
readonly authorizationsSwitch: Locator;
|
|
46
49
|
readonly orchestrationClusterCheckbox: Locator;
|
|
47
50
|
readonly clientCredentialsLink: (clientCredentials: string) => Locator;
|
|
48
51
|
readonly clientRow: (name: string) => Locator;
|
|
@@ -74,11 +77,13 @@ declare class ClusterDetailsPage {
|
|
|
74
77
|
clickCloseModalButton(): Promise<void>;
|
|
75
78
|
clickSettingsTab(): Promise<void>;
|
|
76
79
|
private waitForAuthorizationsSection;
|
|
80
|
+
private setAuthorizationsViaSwitch;
|
|
77
81
|
enableAuthorizations(): Promise<void>;
|
|
78
82
|
disableAuthorizations(): Promise<void>;
|
|
79
83
|
searchAndClickClientCredentialsLink(name: string): Promise<ClientCredentialsDetailsPage>;
|
|
80
84
|
deleteAPIClientsIfExist(name?: string): Promise<void>;
|
|
81
85
|
clickAlertsTab(): Promise<void>;
|
|
86
|
+
reopenAlertsTab(): Promise<void>;
|
|
82
87
|
clickExpandButton(): Promise<void>;
|
|
83
88
|
clickCreateFirstAlertButton(): Promise<void>;
|
|
84
89
|
clickCreateNewAlertButton(): Promise<void>;
|
|
@@ -6,11 +6,17 @@ const sleep_1 = require("../../utils/sleep");
|
|
|
6
6
|
const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
|
|
7
7
|
const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
|
|
8
8
|
const ClientCredentialsDetailsPage_1 = require("./ClientCredentialsDetailsPage");
|
|
9
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
10
|
+
// Hub's own /login and /login-callback routes, which render only a spinner
|
|
11
|
+
// while the Auth0 hand-off of a full page load is in flight.
|
|
12
|
+
const HUB_LOGIN_ROUTE = /^\/login(-callback)?\/?$/;
|
|
9
13
|
class ClusterDetailsPage {
|
|
10
14
|
page;
|
|
11
15
|
// Name of a client whose scopes still have to be checked against the
|
|
12
16
|
// clients list — see createAPIClient() / clickCloseModalButton().
|
|
13
17
|
pendingClientScopeCheck = null;
|
|
18
|
+
// The cluster page reopenAlertsTab() goes back to -- see there.
|
|
19
|
+
clusterDetailsUrl;
|
|
14
20
|
apiTab;
|
|
15
21
|
createFirstClientButton;
|
|
16
22
|
clientNameTextbox;
|
|
@@ -31,6 +37,7 @@ class ClusterDetailsPage {
|
|
|
31
37
|
clientCredentialsDialog;
|
|
32
38
|
clustersLink;
|
|
33
39
|
alertsTab;
|
|
40
|
+
alertsTabPanel;
|
|
34
41
|
alertsList;
|
|
35
42
|
alertExpandRowButtons;
|
|
36
43
|
alertRowActionsButtons;
|
|
@@ -50,6 +57,7 @@ class ClusterDetailsPage {
|
|
|
50
57
|
expandButton;
|
|
51
58
|
rbaEnabledMessage;
|
|
52
59
|
authorizationsHeading;
|
|
60
|
+
authorizationsSwitch;
|
|
53
61
|
orchestrationClusterCheckbox;
|
|
54
62
|
clientCredentialsLink;
|
|
55
63
|
clientRow;
|
|
@@ -134,7 +142,13 @@ class ClusterDetailsPage {
|
|
|
134
142
|
.getByRole('banner')
|
|
135
143
|
.getByRole('link', { name: 'Clusters' });
|
|
136
144
|
this.alertsTab = page.getByRole('tab', { name: 'Alerts' });
|
|
137
|
-
|
|
145
|
+
// Only the selected tab's panel is exposed (the design-system tabs unmount
|
|
146
|
+
// the others, Carbon marks them hidden), and it is named after its tab.
|
|
147
|
+
// Page-wide, getByRole('row') read the Overview "Cluster Details" rows
|
|
148
|
+
// whenever Overview was showing -- the nightly's `Received string:
|
|
149
|
+
// "NameAlert Flow Cluster"` where the Email alert row was expected.
|
|
150
|
+
this.alertsTabPanel = page.getByRole('tabpanel', { name: 'Alerts' });
|
|
151
|
+
this.alertsList = this.alertsTabPanel
|
|
138
152
|
.getByRole('row')
|
|
139
153
|
.filter({ hasNotText: 'Delivery method' }); //Filter out header row
|
|
140
154
|
// Hub's Alerts tab is a design-system data table: one summary row per
|
|
@@ -191,6 +205,12 @@ class ClusterDetailsPage {
|
|
|
191
205
|
});
|
|
192
206
|
this.expandButton = page.locator('button[class="cds--table-expand__button"]');
|
|
193
207
|
this.rbaEnabledMessage = page.getByText('Authorizations enabled');
|
|
208
|
+
// Hub's design-system Settings tab: <button role="switch"
|
|
209
|
+
// aria-label="Switch Enable authorizations" aria-checked="true|false">
|
|
210
|
+
// (c4 C4Toggle). It has no Carbon `.cds--toggle__*` nodes. Same as 8.8.
|
|
211
|
+
this.authorizationsSwitch = this.page.getByRole('switch', {
|
|
212
|
+
name: /Enable authorizations/i,
|
|
213
|
+
});
|
|
194
214
|
this.authorizationsHeading = this.page.getByRole('heading', {
|
|
195
215
|
name: 'Authorizations',
|
|
196
216
|
});
|
|
@@ -381,9 +401,40 @@ class ClusterDetailsPage {
|
|
|
381
401
|
},
|
|
382
402
|
});
|
|
383
403
|
}
|
|
404
|
+
// Drives the design-system Authorizations switch. Returns false when this
|
|
405
|
+
// shell does not render one, so the caller falls through to the legacy
|
|
406
|
+
// Carbon toggle.
|
|
407
|
+
async setAuthorizationsViaSwitch(enabled) {
|
|
408
|
+
const hasSwitch = await this.authorizationsSwitch
|
|
409
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
410
|
+
.then(() => true)
|
|
411
|
+
.catch(() => false);
|
|
412
|
+
if (!hasSwitch) {
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
const isOn = (await this.authorizationsSwitch.getAttribute('aria-checked')) === 'true';
|
|
416
|
+
if (isOn === enabled) {
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
await this.authorizationsSwitch.click({ timeout: 30000 });
|
|
420
|
+
const confirmOpened = await this.dialog
|
|
421
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
422
|
+
.then(() => true)
|
|
423
|
+
.catch(() => false);
|
|
424
|
+
if (confirmOpened) {
|
|
425
|
+
const confirm = enabled ? this.rbaEnableButton : this.rbaDisableButton;
|
|
426
|
+
await confirm.click({ timeout: 30000 });
|
|
427
|
+
await (0, test_1.expect)(this.page.getByText(enabled ? 'Enabling...' : 'Disabling...')).not.toBeVisible({ timeout: 60000 });
|
|
428
|
+
}
|
|
429
|
+
await (0, test_1.expect)(this.authorizationsSwitch).toHaveAttribute('aria-checked', String(enabled), { timeout: 120000 });
|
|
430
|
+
return true;
|
|
431
|
+
}
|
|
384
432
|
async enableAuthorizations() {
|
|
385
433
|
await (0, sleep_1.sleep)(5000);
|
|
386
434
|
await this.waitForAuthorizationsSection();
|
|
435
|
+
if (await this.setAuthorizationsViaSwitch(true)) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
387
438
|
// Locate all elements with class .cds--toggle__text
|
|
388
439
|
const toggleTextElements = await this.page.$$('.cds--toggle__text');
|
|
389
440
|
// Check if toggleTextElements has at least 2 elements
|
|
@@ -417,11 +468,18 @@ class ClusterDetailsPage {
|
|
|
417
468
|
}
|
|
418
469
|
}
|
|
419
470
|
else {
|
|
420
|
-
console.log
|
|
471
|
+
// Was a console.log that returned as though authorizations had been
|
|
472
|
+
// enabled; under Hub this branch ran every time (8.8 fix 7d771aa).
|
|
473
|
+
throw new Error('Could not enable authorizations: the Settings tab rendered neither ' +
|
|
474
|
+
'the design-system "Enable authorizations" switch nor a Carbon ' +
|
|
475
|
+
'toggle pair.');
|
|
421
476
|
}
|
|
422
477
|
}
|
|
423
478
|
async disableAuthorizations() {
|
|
424
479
|
await this.waitForAuthorizationsSection();
|
|
480
|
+
if (await this.setAuthorizationsViaSwitch(false)) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
425
483
|
// Locate all elements with class .cds--toggle__text
|
|
426
484
|
const toggleTextElements = await this.page.$$('.cds--toggle__text');
|
|
427
485
|
// Check if toggleTextElements has at least 2 elements
|
|
@@ -456,7 +514,9 @@ class ClusterDetailsPage {
|
|
|
456
514
|
}
|
|
457
515
|
}
|
|
458
516
|
else {
|
|
459
|
-
|
|
517
|
+
throw new Error('Could not disable authorizations: the Settings tab rendered neither ' +
|
|
518
|
+
'the design-system "Enable authorizations" switch nor a Carbon ' +
|
|
519
|
+
'toggle pair.');
|
|
460
520
|
}
|
|
461
521
|
}
|
|
462
522
|
async searchAndClickClientCredentialsLink(name) {
|
|
@@ -494,10 +554,9 @@ class ClusterDetailsPage {
|
|
|
494
554
|
await this.alertsTab.click();
|
|
495
555
|
// The tab bar remounts while the cluster payload is still arriving, which
|
|
496
556
|
// discards the click and leaves Overview selected -- and the tab is a
|
|
497
|
-
// button, not a route, so nothing later re-selects it. Callers then
|
|
498
|
-
//
|
|
499
|
-
//
|
|
500
|
-
// mounted, re-selecting the tab after a reload between attempts.
|
|
557
|
+
// button, not a route, so nothing later re-selects it. Callers would then
|
|
558
|
+
// find no Alerts panel for alertsList to read. Confirm the panel mounted,
|
|
559
|
+
// re-selecting the tab after a reload between attempts.
|
|
501
560
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.alertsPanel, {
|
|
502
561
|
visibilityTimeout: 60000,
|
|
503
562
|
totalTimeout: 240000,
|
|
@@ -509,6 +568,31 @@ class ClusterDetailsPage {
|
|
|
509
568
|
},
|
|
510
569
|
});
|
|
511
570
|
}
|
|
571
|
+
// Re-enters the cluster page and re-opens the Alerts tab, for callers that
|
|
572
|
+
// poll the alerts table for a server-side change. Hub keeps the selected
|
|
573
|
+
// tab out of the URL (ClusterDetails.saas.tsx drops c4's tabChange), so any
|
|
574
|
+
// full load comes back on Overview and the tab has to be clicked again.
|
|
575
|
+
// A goto to the remembered cluster page rather than a reload: a full load
|
|
576
|
+
// re-runs Hub's Auth0 hand-off, and a reload issued while the page is still
|
|
577
|
+
// parked on /login-callback?code=... replays that single-use code.
|
|
578
|
+
async reopenAlertsTab() {
|
|
579
|
+
if ((0, sessionRecovery_1.isAtIdentityProvider)(this.page)) {
|
|
580
|
+
await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page);
|
|
581
|
+
}
|
|
582
|
+
const current = new URL(this.page.url());
|
|
583
|
+
if (!(0, sessionRecovery_1.isAtIdentityProvider)(this.page) &&
|
|
584
|
+
!HUB_LOGIN_ROUTE.test(current.pathname)) {
|
|
585
|
+
this.clusterDetailsUrl = current.href;
|
|
586
|
+
}
|
|
587
|
+
const target = this.clusterDetailsUrl ?? current.href;
|
|
588
|
+
const host = new URL(target).host;
|
|
589
|
+
await this.page.goto(target, { waitUntil: 'domcontentloaded' });
|
|
590
|
+
// goto() resolves on the first document; the hand-off then bounces
|
|
591
|
+
// through /login and /login-callback (~45s on CI) before it lands back.
|
|
592
|
+
await this.page.waitForURL((url) => url.host === host && !HUB_LOGIN_ROUTE.test(url.pathname), { timeout: 120000 });
|
|
593
|
+
await this.alertsTab.click({ timeout: 60000 });
|
|
594
|
+
await (0, test_1.expect)(this.alertsPanel.first()).toBeVisible({ timeout: 60000 });
|
|
595
|
+
}
|
|
512
596
|
async clickExpandButton() {
|
|
513
597
|
await this.expandButton.click({ timeout: 60000 });
|
|
514
598
|
}
|
|
@@ -829,8 +913,7 @@ class ClusterDetailsPage {
|
|
|
829
913
|
catch (error) {
|
|
830
914
|
if (attempt < maxRetries - 1) {
|
|
831
915
|
console.warn(`Attempt ${attempt + 1} failed for asserting ${text}. Retrying...`);
|
|
832
|
-
await this.
|
|
833
|
-
await (0, sleep_1.sleep)(20000);
|
|
916
|
+
await this.reopenAlertsTab();
|
|
834
917
|
}
|
|
835
918
|
else {
|
|
836
919
|
throw new Error(`Assertion failed after ${maxRetries} attempts`);
|