@camunda/e2e-test-suite 0.0.1248 → 0.0.1249
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 +2 -0
- package/dist/pages/8.9/AppsPage.js +114 -0
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +3 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +92 -25
- package/dist/pages/8.9/ClusterPage.js +49 -8
- package/dist/pages/8.9/ClusterSecretsPage.js +47 -2
- package/dist/pages/8.9/HomePage.d.ts +3 -0
- package/dist/pages/8.9/HomePage.js +44 -5
- package/dist/pages/8.9/LoginPage.d.ts +1 -0
- package/dist/pages/8.9/LoginPage.js +19 -0
- package/dist/pages/8.9/ModelerHomePage.d.ts +7 -0
- package/dist/pages/8.9/ModelerHomePage.js +122 -3
- package/dist/pages/8.9/UtilitiesPage.js +60 -2
- package/dist/tests/8.9/test-setup.spec.js +11 -4
- package/dist/utils/authErrorClassification.d.ts +5 -0
- package/dist/utils/authErrorClassification.js +49 -1
- package/dist/utils/consoleApiHelpers.d.ts +1 -0
- package/dist/utils/consoleApiHelpers.js +183 -30
- package/dist/utils/resetSession.js +7 -0
- package/package.json +1 -1
|
@@ -18,8 +18,10 @@ declare class AppsPage {
|
|
|
18
18
|
readonly consoleLink: Locator;
|
|
19
19
|
readonly camundaComponentsButton: Locator;
|
|
20
20
|
readonly camundaAppsButton: Locator;
|
|
21
|
+
readonly hubSidebar: Locator;
|
|
21
22
|
constructor(page: Page);
|
|
22
23
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
24
|
+
private clickHubClusterApp;
|
|
23
25
|
clickModeler(): Promise<void>;
|
|
24
26
|
clickTasklist(clusterName: string): Promise<void>;
|
|
25
27
|
clickAdmin(clusterName: string): Promise<void>;
|
|
@@ -23,9 +23,15 @@ class AppsPage {
|
|
|
23
23
|
consoleLink;
|
|
24
24
|
camundaComponentsButton;
|
|
25
25
|
camundaAppsButton;
|
|
26
|
+
hubSidebar;
|
|
26
27
|
constructor(page) {
|
|
27
28
|
this.page = page;
|
|
28
29
|
this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
|
|
30
|
+
// Camunda Hub's persistent "Main navigation" sidebar lists each cluster
|
|
31
|
+
// with its own apps (Operate/Tasklist/Optimize/Identity) as child links
|
|
32
|
+
// -- there is no per-app switcher panel to open on Hub (see
|
|
33
|
+
// clickCamundaApps below). Mirrors pages/8.7/AppsPage.ts.
|
|
34
|
+
this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
|
|
29
35
|
this.appSwitcherButton = page.getByLabel('App Switcher');
|
|
30
36
|
this.tasklistLink = page.getByRole('link', { name: 'Tasklist', exact: true });
|
|
31
37
|
this.adminLink = page.getByRole('link', { name: /^(Admin|Identity)$/ });
|
|
@@ -75,7 +81,78 @@ class AppsPage {
|
|
|
75
81
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
|
|
76
82
|
await cluster.click();
|
|
77
83
|
}
|
|
84
|
+
// Camunda Hub's sidebar groups by cluster, not by app: each cluster row
|
|
85
|
+
// 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: "the
|
|
87
|
+
// 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" -- and
|
|
89
|
+
// the child links do not exist in the DOM at all until expanded (not just
|
|
90
|
+
// hidden), and are not nested under any container matchable by cluster
|
|
91
|
+
// 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
|
+
// look up the app link sidebar-wide rather than scoped to a "group"
|
|
94
|
+
// container that doesn't reliably exist.
|
|
95
|
+
async clickHubClusterApp(clusterName, appName) {
|
|
96
|
+
// Not exact: camunda-hub's own nav-shell-ds.spec.saas.tsx asserts this same
|
|
97
|
+
// link with a substring/regex match (`{name: /Operate/}`), not an exact
|
|
98
|
+
// string -- and CI traces confirm why: even after the toggle click
|
|
99
|
+
// succeeds (no error thrown), an exact-match lookup here still times out
|
|
100
|
+
// with "element(s) not found", meaning the rendered link's accessible
|
|
101
|
+
// name is not byte-for-byte "Operate"/"Tasklist"/"Optimize" (likely
|
|
102
|
+
// additional content from the design-system sidebar item, e.g. an icon
|
|
103
|
+
// or badge). The app names are distinct enough that a substring match
|
|
104
|
+
// carries no ambiguity risk.
|
|
105
|
+
const appLink = this.hubSidebar.getByRole('link', {
|
|
106
|
+
name: appName,
|
|
107
|
+
});
|
|
108
|
+
// Decide whether to expand from clusterName's OWN toggle state, not from
|
|
109
|
+
// whether "some" appLink is already visible sidebar-wide: in a
|
|
110
|
+
// multi-cluster org another cluster's group can already be expanded with
|
|
111
|
+
// the same app name, which made this check pass while clusterName's own
|
|
112
|
+
// group stayed collapsed -- appLink.click() below then fired on the
|
|
113
|
+
// wrong cluster's link (see PR #3627 review).
|
|
114
|
+
const collapsedToggle = this.hubSidebar
|
|
115
|
+
.getByRole('button', { name: clusterName, expanded: false })
|
|
116
|
+
.first();
|
|
117
|
+
if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
118
|
+
// This is the only expansion action in the Hub path -- swallowing its
|
|
119
|
+
// failure made a broken expansion look successful and left the caller
|
|
120
|
+
// to burn the full 30s below waiting on an app link that could never
|
|
121
|
+
// appear, hiding the actionable error (e.g. a toggle that doesn't
|
|
122
|
+
// exist or isn't clickable) behind an unrelated timeout.
|
|
123
|
+
await collapsedToggle.click({ timeout: 10000 });
|
|
124
|
+
}
|
|
125
|
+
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
126
|
+
await appLink.click({ timeout: 10000 });
|
|
127
|
+
}
|
|
78
128
|
async clickModeler() {
|
|
129
|
+
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
130
|
+
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
131
|
+
// check alone can't tell whether we're already there -- Hub's cluster
|
|
132
|
+
// pages are on the same hub.* hostname, and a no-op here would leave a
|
|
133
|
+
// caller that reaches clickModeler() from a cluster/API-client page
|
|
134
|
+
// stranded with the next locator running against the wrong page.
|
|
135
|
+
// Navigate home through the header logo link instead, same as
|
|
136
|
+
// ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
|
|
137
|
+
// Modeler home is a harmless in-SPA no-op.
|
|
138
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
139
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
140
|
+
// The href stays literally "/login" until the client's auth state
|
|
141
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
142
|
+
// clicking it in that window would log the test out instead of
|
|
143
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
144
|
+
// one rather than racing it.
|
|
145
|
+
const homeLink = header
|
|
146
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
147
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
148
|
+
.or(header
|
|
149
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
150
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
151
|
+
.first();
|
|
152
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
153
|
+
await homeLink.click({ timeout: 30000 });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
79
156
|
const maxRetries = 3;
|
|
80
157
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
81
158
|
try {
|
|
@@ -106,6 +183,10 @@ class AppsPage {
|
|
|
106
183
|
throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
|
|
107
184
|
}
|
|
108
185
|
async clickTasklist(clusterName) {
|
|
186
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
187
|
+
await this.clickHubClusterApp(clusterName, 'Tasklist');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
109
190
|
const maxRetries = 3;
|
|
110
191
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
111
192
|
try {
|
|
@@ -144,6 +225,14 @@ class AppsPage {
|
|
|
144
225
|
throw new Error(`Failed to click the tasklist link after ${maxRetries} attempts.`);
|
|
145
226
|
}
|
|
146
227
|
async clickAdmin(clusterName) {
|
|
228
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
229
|
+
// Non-anchored, matching the substring lookup clickHubClusterApp uses
|
|
230
|
+
// for Operate/Tasklist/Optimize -- an anchored pattern fails the same
|
|
231
|
+
// way an exact-string match did for those apps if the rendered link's
|
|
232
|
+
// accessible name carries any extra content (icon/badge).
|
|
233
|
+
await this.clickHubClusterApp(clusterName, /Admin|Identity/);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
147
236
|
const maxRetries = 5;
|
|
148
237
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
149
238
|
try {
|
|
@@ -218,6 +307,10 @@ class AppsPage {
|
|
|
218
307
|
await this.clickCluster(this.optimizeListItem, clusterName);
|
|
219
308
|
}
|
|
220
309
|
async clickOperate(clusterName) {
|
|
310
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
311
|
+
await this.clickHubClusterApp(clusterName, 'Operate');
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
221
314
|
const maxRetries = 3;
|
|
222
315
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
223
316
|
try {
|
|
@@ -252,6 +345,10 @@ class AppsPage {
|
|
|
252
345
|
throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
|
|
253
346
|
}
|
|
254
347
|
async clickOptimize(clusterName) {
|
|
348
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
349
|
+
await this.clickHubClusterApp(clusterName, 'Optimize');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
255
352
|
const maxRetries = 3;
|
|
256
353
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
257
354
|
try {
|
|
@@ -329,6 +426,13 @@ class AppsPage {
|
|
|
329
426
|
}
|
|
330
427
|
}
|
|
331
428
|
async clickCamundaApps(maxRetries = 3) {
|
|
429
|
+
// Camunda Hub's rewritten shells have no app-switcher button to open --
|
|
430
|
+
// Modeler is already Hub's home area and the other apps are reached
|
|
431
|
+
// per-cluster via the persistent sidebar, not through a global switcher
|
|
432
|
+
// panel. Mirrors the same hostname guard in clickModeler() above.
|
|
433
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
332
436
|
await (0, sleep_1.sleep)(2000);
|
|
333
437
|
await this.dismissBlockingOverlays();
|
|
334
438
|
const appButtons = [
|
|
@@ -357,6 +461,16 @@ class AppsPage {
|
|
|
357
461
|
}
|
|
358
462
|
}
|
|
359
463
|
async clickConsoleLink() {
|
|
464
|
+
// Camunda Hub folded Console into the same persistent shell as
|
|
465
|
+
// Modeler/home: Console's Dashboard/Clusters/Organization links are
|
|
466
|
+
// just another section of the same "Main navigation" sidebar, not a
|
|
467
|
+
// separate app to switch into. Its header banner (homePage.consoleBanner,
|
|
468
|
+
// via hubHeader) is already visible from any Hub page, so there is no
|
|
469
|
+
// navigation action needed here -- mirrors the same hostname guard in
|
|
470
|
+
// clickModeler().
|
|
471
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
360
474
|
const maxRetries = 3;
|
|
361
475
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
362
476
|
try {
|
|
@@ -3,6 +3,7 @@ import { ClientCredentialsDetailsPage } from './ClientCredentialsDetailsPage';
|
|
|
3
3
|
import { McpCredentials } from '../../utils/mcpSecrets';
|
|
4
4
|
declare class ClusterDetailsPage {
|
|
5
5
|
private page;
|
|
6
|
+
private pendingClientScopeCheck;
|
|
6
7
|
readonly apiTab: Locator;
|
|
7
8
|
readonly createFirstClientButton: Locator;
|
|
8
9
|
readonly clientNameTextbox: Locator;
|
|
@@ -54,7 +55,9 @@ declare class ClusterDetailsPage {
|
|
|
54
55
|
readonly updateButton: Locator;
|
|
55
56
|
constructor(page: Page);
|
|
56
57
|
clickAPITab(): Promise<void>;
|
|
58
|
+
dismissOopsIfPresent(): Promise<void>;
|
|
57
59
|
clickCreateClientButton(): Promise<void>;
|
|
60
|
+
assertAPIClientScopes(name: string): Promise<void>;
|
|
58
61
|
clickClientNameTextbox(): Promise<void>;
|
|
59
62
|
fillClientNameTextbox(name: string): Promise<void>;
|
|
60
63
|
clickTasklistCheckbox(): Promise<void>;
|
|
@@ -8,6 +8,9 @@ const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLoc
|
|
|
8
8
|
const ClientCredentialsDetailsPage_1 = require("./ClientCredentialsDetailsPage");
|
|
9
9
|
class ClusterDetailsPage {
|
|
10
10
|
page;
|
|
11
|
+
// Name of a client whose scopes still have to be checked against the
|
|
12
|
+
// clients list — see createAPIClient() / clickCloseModalButton().
|
|
13
|
+
pendingClientScopeCheck = null;
|
|
11
14
|
apiTab;
|
|
12
15
|
createFirstClientButton;
|
|
13
16
|
clientNameTextbox;
|
|
@@ -80,9 +83,6 @@ class ClusterDetailsPage {
|
|
|
80
83
|
.locator('label')
|
|
81
84
|
.filter({ hasText: /^Administration API - Resource: Secrets$/ });
|
|
82
85
|
this.createButton = page.getByRole('button', { name: 'Create', exact: true });
|
|
83
|
-
this.closeModalButton = page
|
|
84
|
-
.getByRole('dialog', { name: 'Client credentials', exact: true })
|
|
85
|
-
.getByLabel('Close');
|
|
86
86
|
this.createClientButton = page.getByRole('button', {
|
|
87
87
|
name: 'Create new Client',
|
|
88
88
|
});
|
|
@@ -113,9 +113,18 @@ class ClusterDetailsPage {
|
|
|
113
113
|
this.createClientCredentialsDialog = page.getByRole('dialog', {
|
|
114
114
|
name: 'Create new client credentials',
|
|
115
115
|
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
// Use a content-based locator so the test stays robust if the dialog
|
|
117
|
+
// title is ever renamed in the Console UI.
|
|
118
|
+
this.clientCredentialsDialog = page.getByRole('dialog').filter({
|
|
119
|
+
has: page.getByText('The Client Secret will not be shown again.'),
|
|
120
|
+
});
|
|
121
|
+
// Hub's client-credentials dialog (design-system Dialog, not the old
|
|
122
|
+
// Carbon one) exposes its close affix as an accessible button named
|
|
123
|
+
// "Close" rather than via an aria-label picked up by getByLabel -- see
|
|
124
|
+
// camunda-hub's own e2e suite, ConsoleClusterDetailsPage.ts
|
|
125
|
+
// (clientCredentialsDialogCloseButton), which already uses this pattern.
|
|
126
|
+
this.closeModalButton = this.clientCredentialsDialog.getByRole('button', {
|
|
127
|
+
name: 'Close',
|
|
119
128
|
});
|
|
120
129
|
this.clustersLink = page
|
|
121
130
|
.getByRole('banner')
|
|
@@ -202,8 +211,68 @@ class ClusterDetailsPage {
|
|
|
202
211
|
await (0, test_1.expect)(this.apiTab).toBeVisible({ timeout: 60000 });
|
|
203
212
|
await this.apiTab.click({ timeout: 60000 });
|
|
204
213
|
}
|
|
214
|
+
async dismissOopsIfPresent() {
|
|
215
|
+
// The clients list panel sometimes lands in a Carbon "Oops ... something
|
|
216
|
+
// went wrong." state when a background ClustersService request fails on
|
|
217
|
+
// SaaS INT. The panel replaces the create-client button, so any click
|
|
218
|
+
// attempt will time out until the user clicks the inline Reload button.
|
|
219
|
+
const oopsHeading = this.page.getByRole('heading', { name: /Oops/i });
|
|
220
|
+
if (!(await oopsHeading.isVisible({ timeout: 1000 }).catch(() => false))) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
const reloadButton = this.page.getByRole('button', {
|
|
224
|
+
name: 'Reload',
|
|
225
|
+
exact: true,
|
|
226
|
+
});
|
|
227
|
+
if (await reloadButton.isVisible({ timeout: 1000 }).catch(() => false)) {
|
|
228
|
+
await reloadButton.click({ timeout: 15000 });
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
await this.page.reload();
|
|
232
|
+
}
|
|
233
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
234
|
+
// The swallowed click error above let a failed Reload look like recovery,
|
|
235
|
+
// leaving the caller to burn its own timeout against the same Oops state
|
|
236
|
+
// and report an unrelated downstream failure. Verify the panel actually
|
|
237
|
+
// recovered instead of assuming either recovery path worked.
|
|
238
|
+
await (0, test_1.expect)(oopsHeading).not.toBeVisible({ timeout: 15000 });
|
|
239
|
+
}
|
|
205
240
|
async clickCreateClientButton() {
|
|
206
|
-
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.createFirstClientButton.or(this.createNewClientButton)
|
|
241
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.createFirstClientButton.or(this.createNewClientButton), {
|
|
242
|
+
preAction: async () => {
|
|
243
|
+
await this.dismissOopsIfPresent();
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// Asserts the scopes an API client was created with, read from its row in
|
|
248
|
+
// the clients list. Only meaningful once the credentials modal is closed:
|
|
249
|
+
// while it is open the panel behind it can still be showing its pre-create
|
|
250
|
+
// state and lists no row for the new client at all.
|
|
251
|
+
async assertAPIClientScopes(name) {
|
|
252
|
+
const row = this.clientsList.filter({ hasText: name });
|
|
253
|
+
const scopes = /(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/;
|
|
254
|
+
const maxAttempts = 3;
|
|
255
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
256
|
+
try {
|
|
257
|
+
await (0, test_1.expect)(row).toContainText(scopes, { timeout: 60000 });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (attempt === maxAttempts) {
|
|
262
|
+
throw error;
|
|
263
|
+
}
|
|
264
|
+
// A panel that rendered before the client existed — or that failed
|
|
265
|
+
// into the "Oops ... something went wrong." state — only picks the
|
|
266
|
+
// new client up on a refetch, which is what reloading the API tab
|
|
267
|
+
// forces. A single reload isn't always enough on a nightly cluster
|
|
268
|
+
// this suite reuses across parallel test workers, so retry more
|
|
269
|
+
// than once before failing the test.
|
|
270
|
+
console.warn(`assertAPIClientScopes: "${name}" is not listed yet (attempt ${attempt}/${maxAttempts}); reloading the API tab and retrying. ${error}`);
|
|
271
|
+
await this.dismissOopsIfPresent();
|
|
272
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
273
|
+
await this.clickAPITab();
|
|
274
|
+
}
|
|
275
|
+
}
|
|
207
276
|
}
|
|
208
277
|
async clickClientNameTextbox() {
|
|
209
278
|
await this.clientNameTextbox.click({ timeout: 60000 });
|
|
@@ -253,6 +322,12 @@ class ClusterDetailsPage {
|
|
|
253
322
|
}
|
|
254
323
|
async clickCloseModalButton() {
|
|
255
324
|
await this.closeModalButton.click({ timeout: 60000 });
|
|
325
|
+
const pendingClient = this.pendingClientScopeCheck;
|
|
326
|
+
if (pendingClient === null) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
this.pendingClientScopeCheck = null;
|
|
330
|
+
await this.assertAPIClientScopes(pendingClient);
|
|
256
331
|
}
|
|
257
332
|
async clickSettingsTab() {
|
|
258
333
|
await this.settingsTab.click({ timeout: 60000 });
|
|
@@ -582,24 +657,16 @@ class ClusterDetailsPage {
|
|
|
582
657
|
await (0, test_1.expect)(this.clientCredentialsDialog
|
|
583
658
|
.getByText('The Client Secret will not be shown again.')
|
|
584
659
|
.first()).toBeVisible();
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
const oopsVisible = await this.page
|
|
596
|
-
.getByRole('heading', { name: /Oops/i })
|
|
597
|
-
.isVisible({ timeout: 1000 })
|
|
598
|
-
.catch(() => false);
|
|
599
|
-
if (!oopsVisible)
|
|
600
|
-
throw err;
|
|
601
|
-
console.warn(`createAPIClient: clients list panel is in Oops state; skipping row scope assertion for "${attemptName}".`);
|
|
602
|
-
}
|
|
660
|
+
// The client now exists — the modal above is proof — so the scopes it
|
|
661
|
+
// was created with are checked in clickCloseModalButton() rather than
|
|
662
|
+
// here. The clients list behind the open modal is not a reliable
|
|
663
|
+
// source for that check: it can still be rendering its pre-create
|
|
664
|
+
// state (no row for this client) or have failed into "Oops ...
|
|
665
|
+
// something went wrong.", and the modal blocks the pointer events a
|
|
666
|
+
// reload would need. Asserting it here instead sent a created client
|
|
667
|
+
// round the retry loop, which both left orphan clients behind and
|
|
668
|
+
// discarded the credentials the caller has yet to read.
|
|
669
|
+
this.pendingClientScopeCheck = attemptName;
|
|
603
670
|
return;
|
|
604
671
|
}
|
|
605
672
|
catch (error) {
|
|
@@ -123,10 +123,30 @@ class ClusterPage {
|
|
|
123
123
|
this.clusterVersion = (clusterName, versionText) => this.cluster(clusterName).getByText(versionText);
|
|
124
124
|
}
|
|
125
125
|
async clickClusterBanner() {
|
|
126
|
-
|
|
126
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
127
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
128
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
129
|
+
// HomePage.clusterTab handles).
|
|
130
|
+
const clusterLink = this.page
|
|
127
131
|
.getByRole('banner')
|
|
128
132
|
.getByRole('link', { name: 'Clusters' })
|
|
129
|
-
.
|
|
133
|
+
.or(this.page
|
|
134
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
135
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
136
|
+
.or(this.page
|
|
137
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
138
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
139
|
+
await clusterLink.click({ timeout: 60000 });
|
|
140
|
+
// Landing on Hub's Environments overview (see above) renders clusters as
|
|
141
|
+
// cards, not the getByRole('row') table this page's own locators expect
|
|
142
|
+
// -- route through the page's own "Clusters" tab to reach the classic
|
|
143
|
+
// route. Same fix as HomePage.clickClusters().
|
|
144
|
+
const clustersPageTab = this.page
|
|
145
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
146
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
147
|
+
if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
148
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
149
|
+
}
|
|
130
150
|
}
|
|
131
151
|
async deleteCluster(name) {
|
|
132
152
|
await this.page.waitForTimeout(1000);
|
|
@@ -302,12 +322,33 @@ class ClusterPage {
|
|
|
302
322
|
});
|
|
303
323
|
}
|
|
304
324
|
async clickConnectorSecretsTab(clusterName) {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
325
|
+
// clickLocatorWithRetry only reports whether the click itself threw --
|
|
326
|
+
// Radix's tab trigger always accepts the click, so a click that lands
|
|
327
|
+
// right before the panel re-renders (e.g. Hub's cluster-page background
|
|
328
|
+
// reauth reload) can report success while the "Connector Secrets" panel
|
|
329
|
+
// never actually activates. Verify aria-selected rather than trusting
|
|
330
|
+
// the click outcome, and redo the full navigate-and-click on a miss.
|
|
331
|
+
const maxAttempts = 3;
|
|
332
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
333
|
+
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.connectorSecretsTab, {
|
|
334
|
+
postAction: async () => {
|
|
335
|
+
await this.clickClusterBanner();
|
|
336
|
+
await this.clickClusterLink(clusterName);
|
|
337
|
+
},
|
|
338
|
+
});
|
|
339
|
+
const isSelected = (await this.connectorSecretsTab
|
|
340
|
+
.getAttribute('aria-selected')
|
|
341
|
+
.catch(() => null)) === 'true';
|
|
342
|
+
if (isSelected) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (attempt === maxAttempts) {
|
|
346
|
+
throw new Error('clickConnectorSecretsTab: "Connector Secrets" tab click did not activate the tab after retries');
|
|
347
|
+
}
|
|
348
|
+
console.warn(`clickConnectorSecretsTab: click succeeded but the tab did not become active (attempt ${attempt}/${maxAttempts}); retrying`);
|
|
349
|
+
await this.clickClusterBanner();
|
|
350
|
+
await this.clickClusterLink(clusterName);
|
|
351
|
+
}
|
|
311
352
|
}
|
|
312
353
|
async clickLatestStableForMinorVersion() {
|
|
313
354
|
const minorVersion = process.env.MINOR_VERSION;
|
|
@@ -55,10 +55,28 @@ class ClusterSecretsPage {
|
|
|
55
55
|
this.dialogCancelButton = this.page
|
|
56
56
|
.getByRole('dialog')
|
|
57
57
|
.getByRole('button', { name: 'Cancel', exact: true });
|
|
58
|
-
|
|
58
|
+
// "Filter table" was Carbon's default TableToolbarSearch label. A live
|
|
59
|
+
// trace off this branch shows the panel now rendering a plain "Search"
|
|
60
|
+
// input instead (Camunda Hub's rebranded UI, not Carbon), so the old
|
|
61
|
+
// exact-name match never resolves and every lookup falls through to
|
|
62
|
+
// creating a secret that may already exist. Match either.
|
|
63
|
+
this.secretsSearchBox = page
|
|
64
|
+
.getByRole('searchbox', { name: 'Filter table' })
|
|
65
|
+
.or(page.getByRole('searchbox', { name: 'Search' }))
|
|
66
|
+
.or(page.getByPlaceholder('Search'));
|
|
67
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
68
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
69
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
70
|
+
// HomePage.clusterTab handles).
|
|
59
71
|
this.clusterBanner = this.page
|
|
60
72
|
.getByRole('banner')
|
|
61
|
-
.getByRole('link', { name: 'Clusters' })
|
|
73
|
+
.getByRole('link', { name: 'Clusters' })
|
|
74
|
+
.or(this.page
|
|
75
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
76
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
77
|
+
.or(this.page
|
|
78
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
79
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
62
80
|
this.clusterLink = (name) => page.getByRole('link', { name: name });
|
|
63
81
|
this.clusterSecretTab = this.page.getByRole('tab', {
|
|
64
82
|
name: 'Connector Secrets',
|
|
@@ -115,6 +133,17 @@ class ClusterSecretsPage {
|
|
|
115
133
|
async clickClusterBanner() {
|
|
116
134
|
await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
|
|
117
135
|
await this.clusterBanner.click({ timeout: 60000 });
|
|
136
|
+
// Landing on Hub's Environments overview (see clusterBanner above) renders
|
|
137
|
+
// clusters as cards, not the getByRole('link') the clusterLink() locator
|
|
138
|
+
// below expects to find directly -- route through the page's own
|
|
139
|
+
// "Clusters" tab to reach the classic route. Same fix as
|
|
140
|
+
// HomePage.clickClusters().
|
|
141
|
+
const clustersPageTab = this.page
|
|
142
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
143
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
144
|
+
if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
145
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
146
|
+
}
|
|
118
147
|
}
|
|
119
148
|
async clickCluster(name) {
|
|
120
149
|
await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
|
|
@@ -169,6 +198,22 @@ class ClusterSecretsPage {
|
|
|
169
198
|
await (0, test_1.expect)(this.page.getByText('Creating...')).not.toBeVisible({
|
|
170
199
|
timeout: 60000,
|
|
171
200
|
});
|
|
201
|
+
// A live trace off this exact branch (test-setup.spec.ts, PR#3627) shows
|
|
202
|
+
// the panel taking the reload path for every one of 7 secrets created in
|
|
203
|
+
// a row, not "near-universally" -- so the 15s pre-reload probe this used
|
|
204
|
+
// to run first never once paid off and only burned 15s x N secrets.
|
|
205
|
+
// Reload unconditionally, matching the pattern createSetOfSecrets's own
|
|
206
|
+
// bulkImportSecrets() already uses after an import on this identical
|
|
207
|
+
// panel.
|
|
208
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
209
|
+
// The reload lands back on the cluster's default Overview tab, not
|
|
210
|
+
// Connector Secrets -- a CI failure screenshot confirmed the page stuck
|
|
211
|
+
// there with the secrets panel never reached, so createNewSecretButton
|
|
212
|
+
// was never going to reappear regardless of whether the reload fixed the
|
|
213
|
+
// underlying render issue. Re-navigate to the tab before waiting for it.
|
|
214
|
+
await this.clickClusterBanner();
|
|
215
|
+
await this.clickCluster(cluster);
|
|
216
|
+
await this.clusterSecretTab.click({ timeout: 30000 });
|
|
172
217
|
await (0, test_1.expect)(this.createNewSecretButton).toBeVisible({
|
|
173
218
|
timeout: 60000,
|
|
174
219
|
});
|
|
@@ -12,11 +12,14 @@ declare class HomePage {
|
|
|
12
12
|
readonly appSwitcher: Locator;
|
|
13
13
|
readonly openOrganizationButton: Locator;
|
|
14
14
|
readonly consoleBanner: Locator;
|
|
15
|
+
readonly hubHeader: Locator;
|
|
16
|
+
readonly hubSidebar: Locator;
|
|
15
17
|
readonly modelerBanner: Locator;
|
|
16
18
|
readonly gettingStartedHeading: Locator;
|
|
17
19
|
readonly buttonSkipCustomization: Locator;
|
|
18
20
|
constructor(page: Page);
|
|
19
21
|
clickClusters(): Promise<void>;
|
|
22
|
+
private clickClustersPageTabIfPresent;
|
|
20
23
|
clickClustersWithReloadRecovery(): Promise<void>;
|
|
21
24
|
clickDashboard(): Promise<void>;
|
|
22
25
|
clickOrganization(): Promise<void>;
|
|
@@ -16,16 +16,35 @@ class HomePage {
|
|
|
16
16
|
appSwitcher;
|
|
17
17
|
openOrganizationButton;
|
|
18
18
|
consoleBanner;
|
|
19
|
+
hubHeader;
|
|
20
|
+
hubSidebar;
|
|
19
21
|
modelerBanner;
|
|
20
22
|
gettingStartedHeading;
|
|
21
23
|
buttonSkipCustomization;
|
|
22
24
|
constructor(page) {
|
|
23
25
|
this.page = page;
|
|
26
|
+
// Camunda Hub replaces Console on SaaS: an app header (only carrying the
|
|
27
|
+
// user menu once authenticated) plus a sidebar whose Console section
|
|
28
|
+
// links to /org/<id>/clusters and /org/<id>/management.
|
|
29
|
+
this.hubHeader = page
|
|
30
|
+
.locator('header[data-slot="app-header"]')
|
|
31
|
+
.filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
|
|
32
|
+
this.hubSidebar = page.locator('nav[data-slot="app-sidebar"]');
|
|
24
33
|
this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
|
|
34
|
+
// Once an org has environmentsEnabled, camunda-hub's
|
|
35
|
+
// use-ds-sidebar-items.tsx replaces the sidebar's Clusters link with one
|
|
36
|
+
// labelled "Environments" whose href is '/environments', not
|
|
37
|
+
// '/clusters' -- accept both hrefs so this resolves either way.
|
|
25
38
|
this.clusterTab = page
|
|
26
39
|
.getByRole('banner')
|
|
27
|
-
.getByRole('link', { name: 'Clusters' })
|
|
28
|
-
|
|
40
|
+
.getByRole('link', { name: 'Clusters' })
|
|
41
|
+
.or(this.hubSidebar.locator('a[href$="/clusters"]'))
|
|
42
|
+
.or(this.hubSidebar.locator('a[href$="/environments"]'))
|
|
43
|
+
.first();
|
|
44
|
+
this.organizationTab = page
|
|
45
|
+
.getByRole('link', { name: 'Organization' })
|
|
46
|
+
.or(this.hubSidebar.locator('a[href$="/management"]'))
|
|
47
|
+
.first();
|
|
29
48
|
this.organizationViewTitle = page.getByText('Organization Management');
|
|
30
49
|
this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
|
|
31
50
|
this.informationDialog = page.getByRole('button', {
|
|
@@ -38,9 +57,14 @@ class HomePage {
|
|
|
38
57
|
.or(this.camundaAppsButton)
|
|
39
58
|
.or(this.appSwitcher);
|
|
40
59
|
this.openOrganizationButton = page.getByLabel('Open Organizations');
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
60
|
+
// "Camunda logo Console" was the old Carbon header's logo link. Camunda
|
|
61
|
+
// Hub has no equivalent link -- its post-login landing is `hubHeader`
|
|
62
|
+
// (see above) -- so callers that assert this banner as a login/Console
|
|
63
|
+
// arrival signal need the Hub fallback too (UtilitiesPage.ts already
|
|
64
|
+
// does this ad hoc at one call site; fold it in here instead).
|
|
65
|
+
this.consoleBanner = page
|
|
66
|
+
.getByRole('link', { name: 'Camunda logo Console' })
|
|
67
|
+
.or(this.hubHeader);
|
|
44
68
|
this.gettingStartedHeading = page.getByText('start by creating a project');
|
|
45
69
|
this.buttonSkipCustomization = page.getByRole('button', {
|
|
46
70
|
name: 'Skip customization',
|
|
@@ -52,6 +76,20 @@ class HomePage {
|
|
|
52
76
|
async clickClusters() {
|
|
53
77
|
await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 30000 });
|
|
54
78
|
await this.clusterTab.click({ timeout: 15000 });
|
|
79
|
+
await this.clickClustersPageTabIfPresent();
|
|
80
|
+
}
|
|
81
|
+
// Landing on Hub's Environments overview (see clusterTab above) doesn't put
|
|
82
|
+
// us on the classic row-based cluster list ClusterPage.ts expects -- that
|
|
83
|
+
// page renders clusters as cards. Its own header renders a separate
|
|
84
|
+
// "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
|
|
85
|
+
// that does.
|
|
86
|
+
async clickClustersPageTabIfPresent() {
|
|
87
|
+
const clustersPageTab = this.page
|
|
88
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
89
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
90
|
+
if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
91
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
92
|
+
}
|
|
55
93
|
}
|
|
56
94
|
// Same navigation as clickClusters(), but recovers from a Console SPA crash
|
|
57
95
|
// by remounting the app. Toggling authorizations restarts the cluster, and
|
|
@@ -70,6 +108,7 @@ class HomePage {
|
|
|
70
108
|
await this.page.reload();
|
|
71
109
|
},
|
|
72
110
|
});
|
|
111
|
+
await this.clickClustersPageTabIfPresent();
|
|
73
112
|
}
|
|
74
113
|
async clickDashboard() {
|
|
75
114
|
await this.dashboardTab.click({ timeout: 90000 });
|
|
@@ -26,6 +26,7 @@ declare class LoginPage {
|
|
|
26
26
|
clickContinueButton(): Promise<void>;
|
|
27
27
|
fillPassword(password: string): Promise<void>;
|
|
28
28
|
clickLoginButton(): Promise<void>;
|
|
29
|
+
private throwIfCredentialsRejected;
|
|
29
30
|
login(credentials?: {
|
|
30
31
|
username?: string;
|
|
31
32
|
password?: string;
|