@camunda/e2e-test-suite 0.0.1272 → 0.0.1274
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 +2 -0
- package/dist/pages/8.8/AppsPage.js +122 -0
- package/dist/pages/8.8/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterDetailsPage.js +29 -10
- package/dist/pages/8.8/ClusterPage.js +35 -2
- package/dist/pages/8.8/ClusterSecretsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterSecretsPage.js +62 -2
- package/dist/pages/8.8/FormJsPage.js +55 -24
- package/dist/pages/8.8/HomePage.d.ts +3 -0
- package/dist/pages/8.8/HomePage.js +62 -7
- package/dist/pages/8.8/LoginPage.js +32 -1
- package/dist/pages/8.8/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/8.8/ModelerCreatePage.js +78 -16
- package/dist/pages/8.8/ModelerHomePage.d.ts +10 -0
- package/dist/pages/8.8/ModelerHomePage.js +339 -14
- package/dist/pages/8.8/UtilitiesPage.js +47 -2
- package/dist/pages/8.9/AppsPage.js +10 -1
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +66 -1
- package/dist/pages/8.9/ConsoleOrganizationPage.js +50 -17
- package/dist/pages/8.9/FormJsPage.js +23 -0
- package/dist/pages/8.9/ModelerCreatePage.d.ts +3 -0
- package/dist/pages/8.9/ModelerCreatePage.js +108 -7
- package/dist/pages/8.9/ModelerHomePage.d.ts +8 -0
- package/dist/pages/8.9/ModelerHomePage.js +116 -7
- package/dist/pages/8.9/ProcessApplicationPage.d.ts +1 -0
- package/dist/pages/8.9/ProcessApplicationPage.js +96 -0
- package/dist/pages/8.9/SettingsPage.js +12 -2
- package/dist/tests/8.8/test-setup.spec.js +29 -4
- package/dist/tests/8.9/console-user-flows.spec.js +3 -0
- package/dist/utils/consoleApiHelpers.d.ts +5 -0
- package/dist/utils/consoleApiHelpers.js +177 -2
- package/dist/utils/hubDeployDialog.d.ts +3 -0
- package/dist/utils/hubDeployDialog.js +74 -0
- package/dist/utils/hubResourceRename.d.ts +4 -0
- package/dist/utils/hubResourceRename.js +74 -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
|
/**
|
|
25
27
|
* Waits for the Modeler app to actually render, and recovers the one landing
|
|
@@ -23,6 +23,7 @@ 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 });
|
|
@@ -34,6 +35,11 @@ class AppsPage {
|
|
|
34
35
|
this.consoleLink = page.getByRole('link', { name: 'Console', exact: true });
|
|
35
36
|
this.camundaComponentsButton = page.getByLabel('Camunda components');
|
|
36
37
|
this.camundaAppsButton = page.getByLabel('Camunda apps');
|
|
38
|
+
// Camunda Hub's persistent "Main navigation" sidebar lists each cluster
|
|
39
|
+
// with its own apps (Operate/Tasklist/Optimize/Identity) as child links
|
|
40
|
+
// -- there is no per-app switcher panel to open on Hub (see
|
|
41
|
+
// clickCamundaApps below). Ported from pages/8.9/AppsPage.ts.
|
|
42
|
+
this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
|
|
37
43
|
this.tasklistListItem = this.page
|
|
38
44
|
.getByRole('listitem')
|
|
39
45
|
.filter({ hasText: 'Tasklist' });
|
|
@@ -67,7 +73,83 @@ class AppsPage {
|
|
|
67
73
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 60000 });
|
|
68
74
|
await cluster.click();
|
|
69
75
|
}
|
|
76
|
+
// Camunda Hub's sidebar groups by cluster, not by app: each cluster row
|
|
77
|
+
// expands to reveal its own per-app child links (Operate/Tasklist/...).
|
|
78
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: the
|
|
79
|
+
// design system splits the group row into a label control and a chevron
|
|
80
|
+
// control, both labelled by the cluster name; either one opens it -- and
|
|
81
|
+
// the child links do not exist in the DOM at all until expanded (not just
|
|
82
|
+
// hidden), and are not nested under any container matchable by cluster
|
|
83
|
+
// name once expanded. So: find the toggle by the cluster's own name
|
|
84
|
+
// (`.first()` because both controls share that name), click it, then look
|
|
85
|
+
// up the app link sidebar-wide rather than scoped to a "group" container
|
|
86
|
+
// that doesn't reliably exist. Ported from pages/8.9/AppsPage.ts, where
|
|
87
|
+
// this shape is already nightly-proven.
|
|
88
|
+
async clickHubClusterApp(clusterName, appName) {
|
|
89
|
+
// When the sidebar is collapsed (the default on a fresh session), the
|
|
90
|
+
// group toggle opens a Radix popover -- a role="dialog" labelled with the
|
|
91
|
+
// cluster name and portalled OUTSIDE the "Main navigation" <nav> -- so a
|
|
92
|
+
// sidebar-scoped lookup finds nothing there. Look in that popover first;
|
|
93
|
+
// keep the sidebar-scoped lookup for the expanded-sidebar layout, where
|
|
94
|
+
// the links render inline. Not exact: the rendered accessible name
|
|
95
|
+
// carries more than the bare app name (e.g. "Operate in <cluster>
|
|
96
|
+
// (opens in a new tab)").
|
|
97
|
+
const appLink = this.page
|
|
98
|
+
.getByRole('dialog', { name: clusterName, exact: true })
|
|
99
|
+
.getByRole('link', { name: appName })
|
|
100
|
+
.or(this.hubSidebar.getByRole('link', { name: appName }))
|
|
101
|
+
.first();
|
|
102
|
+
// Decide whether to expand from clusterName's OWN toggle state, not from
|
|
103
|
+
// whether "some" appLink is already visible sidebar-wide: in a
|
|
104
|
+
// multi-cluster org another cluster's group can already be expanded with
|
|
105
|
+
// the same app name, and the click would then fire on the wrong
|
|
106
|
+
// cluster's link.
|
|
107
|
+
const collapsedToggle = this.hubSidebar
|
|
108
|
+
.getByRole('button', { name: clusterName, expanded: false })
|
|
109
|
+
.first();
|
|
110
|
+
if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
111
|
+
await collapsedToggle.click({ timeout: 10000 });
|
|
112
|
+
}
|
|
113
|
+
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
114
|
+
// Hub's cluster-app links carry target="_blank", so clicking them opens
|
|
115
|
+
// the app in a new tab while every caller keeps asserting on this.page.
|
|
116
|
+
// Follow the href in the current tab instead, matching the same-tab
|
|
117
|
+
// navigation the Console app switcher gave these callers.
|
|
118
|
+
const href = await appLink.getAttribute('href');
|
|
119
|
+
if (href) {
|
|
120
|
+
await this.page.goto(new URL(href, this.page.url()).toString());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await appLink.click({ timeout: 10000 });
|
|
124
|
+
}
|
|
70
125
|
async clickModeler() {
|
|
126
|
+
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
127
|
+
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
128
|
+
// check alone can't tell whether we're already there -- Hub's cluster
|
|
129
|
+
// pages are on the same hub.* hostname, and a no-op here would leave a
|
|
130
|
+
// caller that reaches clickModeler() from a cluster/API-client page
|
|
131
|
+
// stranded with the next locator running against the wrong page.
|
|
132
|
+
// Navigate home through the header logo link instead, same as
|
|
133
|
+
// ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
|
|
134
|
+
// Modeler home is a harmless in-SPA no-op.
|
|
135
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
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 });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
71
153
|
const maxRetries = 5;
|
|
72
154
|
// If the click is already done and we are sitting on Modeler, return
|
|
73
155
|
// immediately — observed in failing runs where the banner role lookup
|
|
@@ -148,6 +230,10 @@ class AppsPage {
|
|
|
148
230
|
}
|
|
149
231
|
}
|
|
150
232
|
async clickTasklist(clusterName) {
|
|
233
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
234
|
+
await this.clickHubClusterApp(clusterName, 'Tasklist');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
151
237
|
const maxRetries = 5;
|
|
152
238
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
153
239
|
try {
|
|
@@ -185,6 +271,14 @@ class AppsPage {
|
|
|
185
271
|
throw new Error(`Failed to click the tasklist link after ${maxRetries} attempts.`);
|
|
186
272
|
}
|
|
187
273
|
async clickIdentity(clusterName) {
|
|
274
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
275
|
+
// Non-anchored, matching the substring lookup clickHubClusterApp uses
|
|
276
|
+
// for Operate/Tasklist/Optimize -- an anchored pattern fails the same
|
|
277
|
+
// way an exact-string match did for those apps if the rendered link's
|
|
278
|
+
// accessible name carries any extra content (icon/badge).
|
|
279
|
+
await this.clickHubClusterApp(clusterName, /Admin|Identity/);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
188
282
|
const maxRetries = 5;
|
|
189
283
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
190
284
|
try {
|
|
@@ -251,6 +345,10 @@ class AppsPage {
|
|
|
251
345
|
await this.clickCluster(this.optimizeListItem, clusterName);
|
|
252
346
|
}
|
|
253
347
|
async clickOperate(clusterName) {
|
|
348
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
349
|
+
await this.clickHubClusterApp(clusterName, 'Operate');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
254
352
|
const maxRetries = 3;
|
|
255
353
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
256
354
|
try {
|
|
@@ -287,6 +385,10 @@ class AppsPage {
|
|
|
287
385
|
throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
|
|
288
386
|
}
|
|
289
387
|
async clickOptimize(clusterName) {
|
|
388
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
389
|
+
await this.clickHubClusterApp(clusterName, 'Optimize');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
290
392
|
const maxRetries = 3;
|
|
291
393
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
292
394
|
try {
|
|
@@ -373,6 +475,13 @@ class AppsPage {
|
|
|
373
475
|
}
|
|
374
476
|
}
|
|
375
477
|
async clickCamundaApps(maxRetries = 3) {
|
|
478
|
+
// Camunda Hub's rewritten shells have no app-switcher button to open --
|
|
479
|
+
// Modeler is already Hub's home area and the other apps are reached
|
|
480
|
+
// per-cluster via the persistent sidebar, not through a global switcher
|
|
481
|
+
// panel. Mirrors the same hostname guard in clickModeler() above.
|
|
482
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
376
485
|
await (0, sleep_1.sleep)(2000);
|
|
377
486
|
const appButtons = [
|
|
378
487
|
this.camundaComponentsButton,
|
|
@@ -420,6 +529,19 @@ class AppsPage {
|
|
|
420
529
|
.catch(() => { });
|
|
421
530
|
}
|
|
422
531
|
async clickConsoleLink() {
|
|
532
|
+
// Camunda Hub replaced Console on SaaS: there is no "Console" entry in a
|
|
533
|
+
// switcher, and the org-scoped Console area is the sidebar's
|
|
534
|
+
// Clusters/Environments section. Going there is what callers assert on
|
|
535
|
+
// (HomePage.consoleBanner resolves to Hub's authenticated header).
|
|
536
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
537
|
+
const consoleArea = this.hubSidebar
|
|
538
|
+
.locator('a[href$="/clusters"]')
|
|
539
|
+
.or(this.hubSidebar.locator('a[href$="/environments"]'))
|
|
540
|
+
.first();
|
|
541
|
+
await (0, test_1.expect)(consoleArea).toBeVisible({ timeout: 30000 });
|
|
542
|
+
await consoleArea.click({ timeout: 30000 });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
423
545
|
const maxRetries = 3;
|
|
424
546
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
425
547
|
try {
|
|
@@ -40,6 +40,7 @@ declare class ClusterDetailsPage {
|
|
|
40
40
|
readonly orchestrationClusterCheckbox: Locator;
|
|
41
41
|
readonly clientCredentialsLink: (clientCredentials: string) => Locator;
|
|
42
42
|
readonly clientRow: (name: string) => Locator;
|
|
43
|
+
readonly clientRowByDom: (name: string) => Locator;
|
|
43
44
|
readonly clientRowDeleteButton: (name: string) => Locator;
|
|
44
45
|
readonly reviewUpdateButton: Locator;
|
|
45
46
|
readonly selectTargetVersionDialog: Locator;
|
|
@@ -46,6 +46,7 @@ class ClusterDetailsPage {
|
|
|
46
46
|
orchestrationClusterCheckbox;
|
|
47
47
|
clientCredentialsLink;
|
|
48
48
|
clientRow;
|
|
49
|
+
clientRowByDom;
|
|
49
50
|
clientRowDeleteButton;
|
|
50
51
|
reviewUpdateButton;
|
|
51
52
|
selectTargetVersionDialog;
|
|
@@ -78,9 +79,6 @@ class ClusterDetailsPage {
|
|
|
78
79
|
.locator('label')
|
|
79
80
|
.filter({ hasText: /^Administration API - Resource: Secrets$/ });
|
|
80
81
|
this.createButton = page.getByRole('button', { name: 'Create', exact: true });
|
|
81
|
-
this.closeModalButton = page
|
|
82
|
-
.getByRole('dialog', { name: 'Client credentials', exact: true })
|
|
83
|
-
.getByLabel('Close');
|
|
84
82
|
this.createClientButton = page.getByRole('button', {
|
|
85
83
|
name: 'Create new Client',
|
|
86
84
|
});
|
|
@@ -98,9 +96,18 @@ class ClusterDetailsPage {
|
|
|
98
96
|
this.createClientCredentialsDialog = page.getByRole('dialog', {
|
|
99
97
|
name: 'Create new client credentials',
|
|
100
98
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
// Use a content-based locator so the test stays robust if the dialog
|
|
100
|
+
// title is ever renamed in the Console UI.
|
|
101
|
+
this.clientCredentialsDialog = page.getByRole('dialog').filter({
|
|
102
|
+
has: page.getByText('The Client Secret will not be shown again.'),
|
|
103
|
+
});
|
|
104
|
+
// Hub's client-credentials dialog (design-system Dialog, not the old
|
|
105
|
+
// Carbon one) exposes its close affix as an accessible button named
|
|
106
|
+
// "Close" rather than via an aria-label picked up by getByLabel -- see
|
|
107
|
+
// camunda-hub's own e2e suite, ConsoleClusterDetailsPage.ts
|
|
108
|
+
// (clientCredentialsDialogCloseButton), which already uses this pattern.
|
|
109
|
+
this.closeModalButton = this.clientCredentialsDialog.getByRole('button', {
|
|
110
|
+
name: 'Close',
|
|
104
111
|
});
|
|
105
112
|
this.clustersLink = page
|
|
106
113
|
.getByRole('banner')
|
|
@@ -154,10 +161,22 @@ class ClusterDetailsPage {
|
|
|
154
161
|
this.orchestrationClusterCheckbox = page
|
|
155
162
|
.locator('label')
|
|
156
163
|
.filter({ hasText: /^Orchestration Cluster API$/ });
|
|
164
|
+
// Camunda Hub's data table renders the client name as a link-styled
|
|
165
|
+
// <button data-slot="button"> rather than an <a>. The cell also holds a
|
|
166
|
+
// pointer-events-none row-overlay button with the same accessible name,
|
|
167
|
+
// so match the visible one by its data-slot.
|
|
157
168
|
this.clientCredentialsLink = (clientCredentials) => page
|
|
158
169
|
.getByRole('cell', { name: clientCredentials })
|
|
159
|
-
.
|
|
170
|
+
.locator('a, button[data-slot="button"]')
|
|
171
|
+
.first();
|
|
160
172
|
this.clientRow = (name) => this.clientsList.filter({ hasText: name });
|
|
173
|
+
// Same row, matched through the DOM instead of the accessibility tree.
|
|
174
|
+
// Hub's client-credentials dialog is a Radix dialog: while it is open the
|
|
175
|
+
// whole app shell behind it carries aria-hidden/inert, so `getByRole`
|
|
176
|
+
// resolves nothing there and the post-create scope check below can never
|
|
177
|
+
// pass. `locator('tr')` is unaffected. The client name is unique per run,
|
|
178
|
+
// so matching page-wide cannot pick up another table's row.
|
|
179
|
+
this.clientRowByDom = (name) => page.locator('tr').filter({ hasText: name });
|
|
161
180
|
this.clientRowDeleteButton = (name) => this.clientRow(name).getByRole('button', { name: 'Delete' });
|
|
162
181
|
this.reviewUpdateButton = page.getByRole('button', { name: 'Review Update' });
|
|
163
182
|
this.selectTargetVersionDialog = page
|
|
@@ -609,9 +628,9 @@ class ClusterDetailsPage {
|
|
|
609
628
|
}
|
|
610
629
|
await (0, test_1.expect)(this.clientCredentialsDialog.getByText('The Client Secret will not be shown again.')).toBeVisible({ timeout: 30000 });
|
|
611
630
|
// The clients table behind the dialog re-fetches after the create, and
|
|
612
|
-
// renders a skeleton (no
|
|
613
|
-
//
|
|
614
|
-
await (0, test_1.expect)(this.
|
|
631
|
+
// renders a skeleton (no rows at all) until that GET resolves — so the
|
|
632
|
+
// default 5s expect timeout can expire before the new row exists.
|
|
633
|
+
await (0, test_1.expect)(this.clientRowByDom(name)).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/, { timeout: 60000 });
|
|
615
634
|
}
|
|
616
635
|
async clickEnvVarsButton() {
|
|
617
636
|
await (0, test_1.expect)(this.envVarsTab).toBeVisible({ timeout: 40000 });
|
|
@@ -124,10 +124,43 @@ class ClusterPage {
|
|
|
124
124
|
this.clusterVersion = (clusterName, versionText) => this.cluster(clusterName).getByText(versionText);
|
|
125
125
|
}
|
|
126
126
|
async clickClusterBanner() {
|
|
127
|
-
|
|
127
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
128
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
129
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
130
|
+
// HomePage.clusterTab handles).
|
|
131
|
+
const clusterLink = this.page
|
|
128
132
|
.getByRole('banner')
|
|
129
133
|
.getByRole('link', { name: 'Clusters' })
|
|
130
|
-
.
|
|
134
|
+
.or(this.page
|
|
135
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
136
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
137
|
+
.or(this.page
|
|
138
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
139
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
140
|
+
await clusterLink.click({ timeout: 60000 });
|
|
141
|
+
// Landing on Hub's Environments overview (see above) renders clusters as
|
|
142
|
+
// cards, not the getByRole('row') table this page's own locators expect
|
|
143
|
+
// -- route through the page's own "Clusters" tab to reach the classic
|
|
144
|
+
// route. Same fix as HomePage.clickClusters().
|
|
145
|
+
const clustersPageTab = this.page
|
|
146
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
147
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
148
|
+
// Required on Hub, not optional: this page's row-based cluster locators
|
|
149
|
+
// cannot be satisfied by the card-based Environments overview we just
|
|
150
|
+
// landed on. isVisible() does not wait, so a slow tab render used to be
|
|
151
|
+
// read as "no tab here" and the flow continued on the wrong route.
|
|
152
|
+
const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
|
|
153
|
+
const tabShown = await clustersPageTab
|
|
154
|
+
.waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
|
|
155
|
+
.then(() => true)
|
|
156
|
+
.catch(() => false);
|
|
157
|
+
if (tabShown) {
|
|
158
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
159
|
+
}
|
|
160
|
+
else if (onHub) {
|
|
161
|
+
throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
|
|
162
|
+
'row-based cluster list this page needs is unreachable without it');
|
|
163
|
+
}
|
|
131
164
|
}
|
|
132
165
|
async deleteCluster(name) {
|
|
133
166
|
await this.page.waitForTimeout(1000);
|
|
@@ -36,6 +36,7 @@ declare class ClusterSecretsPage {
|
|
|
36
36
|
value: string;
|
|
37
37
|
}[]): Promise<void>;
|
|
38
38
|
reopenConnectorSecretsTab(cluster: string): Promise<void>;
|
|
39
|
+
private reselectConnectorSecretsTabIfPresent;
|
|
39
40
|
assertSecretsToolbarVisible(cluster: string): Promise<void>;
|
|
40
41
|
assertSecretsOrCreate(cluster: string, secrets: {
|
|
41
42
|
name: string;
|
|
@@ -50,10 +50,27 @@ class ClusterSecretsPage {
|
|
|
50
50
|
this.dialogImportButton = this.page
|
|
51
51
|
.getByRole('dialog')
|
|
52
52
|
.getByRole('button', { name: 'Import', exact: true });
|
|
53
|
-
|
|
53
|
+
// "Filter table" was Carbon's default TableToolbarSearch label. Camunda
|
|
54
|
+
// Hub's rebranded panel renders a plain "Search" input instead, so the
|
|
55
|
+
// old exact-name match never resolves and every lookup falls through to
|
|
56
|
+
// creating a secret that may already exist. Match either.
|
|
57
|
+
this.secretsSearchBox = page
|
|
58
|
+
.getByRole('searchbox', { name: 'Filter table' })
|
|
59
|
+
.or(page.getByRole('searchbox', { name: 'Search' }))
|
|
60
|
+
.or(page.getByPlaceholder('Search'));
|
|
61
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
62
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
63
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
64
|
+
// HomePage.clusterTab handles).
|
|
54
65
|
this.clusterBanner = this.page
|
|
55
66
|
.getByRole('banner')
|
|
56
|
-
.getByRole('link', { name: 'Clusters' })
|
|
67
|
+
.getByRole('link', { name: 'Clusters' })
|
|
68
|
+
.or(this.page
|
|
69
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
70
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
71
|
+
.or(this.page
|
|
72
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
73
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
57
74
|
this.clusterLink = (name) => page.getByRole('link', { name: name });
|
|
58
75
|
this.clusterSecretTab = this.page.getByRole('tab', {
|
|
59
76
|
name: 'Connector Secrets',
|
|
@@ -144,6 +161,30 @@ class ClusterSecretsPage {
|
|
|
144
161
|
await this.dismissOpenDialogIfPresent();
|
|
145
162
|
await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
|
|
146
163
|
await this.clusterBanner.click({ timeout: 60000 });
|
|
164
|
+
// Landing on Hub's Environments overview (see clusterBanner above) renders
|
|
165
|
+
// clusters as cards, not the getByRole('link') the clusterLink() locator
|
|
166
|
+
// below expects to find directly -- route through the page's own
|
|
167
|
+
// "Clusters" tab to reach the classic route. Same fix as
|
|
168
|
+
// HomePage.clickClusters().
|
|
169
|
+
const clustersPageTab = this.page
|
|
170
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
171
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
172
|
+
// Required on Hub, not optional: this page's row-based cluster locators
|
|
173
|
+
// cannot be satisfied by the card-based Environments overview we just
|
|
174
|
+
// landed on. isVisible() does not wait, so a slow tab render used to be
|
|
175
|
+
// read as "no tab here" and the flow continued on the wrong route.
|
|
176
|
+
const onHub = new URL(this.page.url()).hostname.startsWith('hub.');
|
|
177
|
+
const tabShown = await clustersPageTab
|
|
178
|
+
.waitFor({ state: 'visible', timeout: onHub ? 30000 : 5000 })
|
|
179
|
+
.then(() => true)
|
|
180
|
+
.catch(() => false);
|
|
181
|
+
if (tabShown) {
|
|
182
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
183
|
+
}
|
|
184
|
+
else if (onHub) {
|
|
185
|
+
throw new Error('Hub Environments view did not render its "Clusters" tab; the ' +
|
|
186
|
+
'row-based cluster list this page needs is unreachable without it');
|
|
187
|
+
}
|
|
147
188
|
}
|
|
148
189
|
async clickCluster(name) {
|
|
149
190
|
await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
|
|
@@ -241,7 +282,25 @@ class ClusterSecretsPage {
|
|
|
241
282
|
await (0, test_1.expect)(this.clusterSecretTab).toBeVisible({ timeout: 30000 });
|
|
242
283
|
await this.clusterSecretTab.click({ timeout: 30000 });
|
|
243
284
|
}
|
|
285
|
+
// Camunda Hub keeps the selected cluster tab in client-side state rather
|
|
286
|
+
// than in the URL (/org/<id>/cluster/<id> carries no tab segment), so every
|
|
287
|
+
// page.reload() in this file lands back on the cluster Overview tab, where
|
|
288
|
+
// the secrets toolbar does not exist at all. Re-selecting the tab is a
|
|
289
|
+
// client-side click that costs nothing, whereas re-entering from the
|
|
290
|
+
// Clusters list navigates to `/` and pays a full Hub OAuth hand-off -- so
|
|
291
|
+
// try the cheap route before the toolbar assertions fall through to their
|
|
292
|
+
// broken-panel recovery.
|
|
293
|
+
async reselectConnectorSecretsTabIfPresent() {
|
|
294
|
+
const onTab = await this.clusterSecretTab
|
|
295
|
+
.waitFor({ state: 'visible', timeout: 30000 })
|
|
296
|
+
.then(() => true)
|
|
297
|
+
.catch(() => false);
|
|
298
|
+
if (onTab) {
|
|
299
|
+
await this.clusterSecretTab.click({ timeout: 15000 }).catch(() => { });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
244
302
|
async assertSecretsToolbarVisible(cluster) {
|
|
303
|
+
await this.reselectConnectorSecretsTabIfPresent();
|
|
245
304
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewSecretButton, {
|
|
246
305
|
visibilityTimeout: 60000,
|
|
247
306
|
totalTimeout: 300000,
|
|
@@ -272,6 +331,7 @@ class ClusterSecretsPage {
|
|
|
272
331
|
// Same broken-panel state as assertSecretsToolbarVisible guards against:
|
|
273
332
|
// when the secrets panel failed to load, Import is simply absent, and only
|
|
274
333
|
// re-entering the tab brings it back.
|
|
334
|
+
await this.reselectConnectorSecretsTabIfPresent();
|
|
275
335
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.importButton, {
|
|
276
336
|
visibilityTimeout: 30000,
|
|
277
337
|
totalTimeout: 300000,
|
|
@@ -5,6 +5,8 @@ const test_1 = require("@playwright/test");
|
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
7
7
|
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
8
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
9
|
+
const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
|
|
8
10
|
class FormJsPage {
|
|
9
11
|
page;
|
|
10
12
|
processApplicationPage;
|
|
@@ -54,16 +56,36 @@ class FormJsPage {
|
|
|
54
56
|
// Inside a process application hub titles this modal for the application
|
|
55
57
|
// rather than for the form, so 'Deploy form' never matches and the dialog
|
|
56
58
|
// reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
|
|
59
|
+
// Camunda Hub's design-system shell renders no Carbon markup at all: a DOM
|
|
60
|
+
// audit of a live run found zero `cds--*` classes on these pages, so the
|
|
61
|
+
// Carbon container selector matched nothing and the dialog read as absent
|
|
62
|
+
// even once it had opened. Match the dialog by role and keep the Carbon
|
|
63
|
+
// container for shells where the Ds migration has not shipped.
|
|
57
64
|
this.dialog = page
|
|
65
|
+
.getByRole('dialog')
|
|
66
|
+
.filter({ hasText: 'Deploy' })
|
|
67
|
+
.or(page
|
|
58
68
|
.locator('.cds--modal-container[role="dialog"]')
|
|
59
|
-
.filter({ hasText: 'Deploy' })
|
|
69
|
+
.filter({ hasText: 'Deploy' }))
|
|
70
|
+
.first();
|
|
60
71
|
this.deploySubButton = this.dialog.getByRole('button', {
|
|
61
72
|
name: 'Deploy',
|
|
62
73
|
exact: true,
|
|
63
74
|
});
|
|
75
|
+
// Same Carbon-to-design-system move as the dialog above: the success toast
|
|
76
|
+
// is a role=status/alert region on Hub rather than a `cds--toast-*` node.
|
|
77
|
+
// Hub's dialog deploys the whole PROJECT ("Deploy project"), so its
|
|
78
|
+
// confirmation is no longer the form-specific "Form deployed!" string.
|
|
79
|
+
// Match the deployment confirmation however it is worded, while still
|
|
80
|
+
// requiring a success (not an error) wording so a failed deploy cannot
|
|
81
|
+
// satisfy it.
|
|
64
82
|
this.deploySuccessNotification = page
|
|
83
|
+
.getByText(/(form|project|resources?)\s+(was\s+)?deployed/i)
|
|
84
|
+
.or(page.getByText('Form deployed!'))
|
|
85
|
+
.or(page
|
|
65
86
|
.locator('.cds--toast-notification--success')
|
|
66
|
-
.filter({ hasText: 'Form deployed!' })
|
|
87
|
+
.filter({ hasText: 'Form deployed!' }))
|
|
88
|
+
.first();
|
|
67
89
|
this.deployErrorMessage = this.dialog.getByText('The form failed to deploy');
|
|
68
90
|
}
|
|
69
91
|
async generateAIForm(request = 'Create a form with the following fields: 1. A text field with the label "Full Name" 2. A number with the label "Count" 3. A date input with the label "Date of birth"4. A Checkbox with the label "Agree"') {
|
|
@@ -159,14 +181,30 @@ class FormJsPage {
|
|
|
159
181
|
}
|
|
160
182
|
return;
|
|
161
183
|
}
|
|
162
|
-
|
|
163
|
-
|
|
184
|
+
// "HealthydevManage" was the Carbon row's chrome concatenated together.
|
|
185
|
+
// The design-system dialog renders the cluster row as
|
|
186
|
+
// "<name> ... <generation> dev <checkmark> Healthy" with no Manage link,
|
|
187
|
+
// so the old pattern could never match. Keep what the assertion is for --
|
|
188
|
+
// the chosen cluster reports Healthy -- without the stale chrome.
|
|
189
|
+
const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?Healthy`);
|
|
190
|
+
// `[title="<cluster>"]` was the Carbon toggle's own tooltip attribute; the
|
|
191
|
+
// design-system dialog renders no `title` here, so the click target is the
|
|
192
|
+
// labelled toggle itself (the same node `legacyClusterToggle` already
|
|
193
|
+
// matched to decide this branch).
|
|
194
|
+
const cluster = this.dialog
|
|
195
|
+
.locator(`[title="${clusterName}"]`)
|
|
196
|
+
.or(legacyClusterToggle.first())
|
|
197
|
+
.first();
|
|
164
198
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
|
|
165
199
|
await cluster.click();
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
200
|
+
// Carbon rendered the cluster choice as a toggle `switch`; the
|
|
201
|
+
// design-system dialog uses a different control (and auto-selects the
|
|
202
|
+
// single offered cluster), so a switch-only assertion timed out on a
|
|
203
|
+
// dialog that had in fact selected the cluster. Assert whichever control
|
|
204
|
+
// shape is present, and skip the check when none is -- the deploy itself,
|
|
205
|
+
// and the success notification the caller waits on, are what actually
|
|
206
|
+
// prove the right cluster was used.
|
|
207
|
+
await (0, hubDeployDialog_1.assertClusterSelectedInDialog)(this.dialog, clusterName);
|
|
170
208
|
await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
|
|
171
209
|
timeout: healthCheckTimeout,
|
|
172
210
|
});
|
|
@@ -189,22 +227,7 @@ class FormJsPage {
|
|
|
189
227
|
// silently turning it into a process-application deploy. The combobox only
|
|
190
228
|
// exists inside a container, hence the boolean probe.
|
|
191
229
|
async selectOnlyThisResource() {
|
|
192
|
-
|
|
193
|
-
name: 'Resources to deploy',
|
|
194
|
-
});
|
|
195
|
-
const hasScopeSelect = await scopeSelect
|
|
196
|
-
.waitFor({ state: 'visible', timeout: 10000 })
|
|
197
|
-
.then(() => true)
|
|
198
|
-
.catch(() => false);
|
|
199
|
-
if (!hasScopeSelect) {
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
await scopeSelect.click();
|
|
203
|
-
await this.dialog
|
|
204
|
-
.getByRole('option', { name: 'Only this resource' })
|
|
205
|
-
.or(this.dialog.getByText('Only this resource', { exact: true }))
|
|
206
|
-
.first()
|
|
207
|
-
.click({ timeout: 30000 });
|
|
230
|
+
await (0, hubDeployDialog_1.selectOnlyThisResourceInDialog)(this.page, this.dialog);
|
|
208
231
|
}
|
|
209
232
|
async deployForm(clusterName) {
|
|
210
233
|
await this.processApplicationPage.ensureStageCluster(clusterName);
|
|
@@ -214,6 +237,14 @@ class FormJsPage {
|
|
|
214
237
|
// retry through that propagation window instead of failing on the transient
|
|
215
238
|
// rejection. Non-migration runs keep the original single-attempt behavior.
|
|
216
239
|
const maxAttempts = process.env.IS_MIGRATION === 'true' ? 15 : 1;
|
|
240
|
+
// Camunda Hub's deploy modal lists the ENVIRONMENTS assigned to this
|
|
241
|
+
// workspace, not the org's clusters, so a cluster created after the
|
|
242
|
+
// workspace existed never appears and the picker comes up empty. The
|
|
243
|
+
// wizard's "Assign environments" step only covers a workspace created
|
|
244
|
+
// after its cluster, which test-setup cannot guarantee -- it creates
|
|
245
|
+
// clusters and workspaces in parallel. Ensure the link here, at the point
|
|
246
|
+
// of use, so the ordering does not matter.
|
|
247
|
+
await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(this.page, clusterName);
|
|
217
248
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
218
249
|
await this.clickDeployButton();
|
|
219
250
|
await (0, test_1.expect)(this.dialog).toBeVisible({
|
|
@@ -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
|
clickDashboard(): Promise<void>;
|
|
21
24
|
clickOrganization(): Promise<void>;
|
|
22
25
|
closeInformationDialog(): Promise<void>;
|