@camunda/e2e-test-suite 0.0.1249 → 0.0.1250
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.7/AppsPage.d.ts +7 -3
- package/dist/pages/8.7/AppsPage.js +187 -3
- package/dist/pages/8.7/ClusterPage.js +22 -2
- package/dist/pages/8.7/ClusterSecretsPage.js +22 -1
- package/dist/pages/8.7/HomePage.d.ts +2 -0
- package/dist/pages/8.7/HomePage.js +70 -6
- package/dist/pages/8.7/LoginPage.js +15 -0
- package/dist/pages/8.7/ModelerCreatePage.d.ts +4 -0
- package/dist/pages/8.7/ModelerCreatePage.js +59 -5
- package/dist/pages/8.7/ModelerHomePage.d.ts +11 -0
- package/dist/pages/8.7/ModelerHomePage.js +184 -10
- package/dist/pages/8.7/ProcessApplicationPage.js +27 -7
- package/dist/pages/8.7/UtilitiesPage.js +54 -1
- package/dist/tests/8.7/test-setup.spec.js +18 -5
- package/dist/tests/8.7/web-modeler-user-flows.spec.js +14 -14
- package/package.json +1 -1
|
@@ -15,8 +15,12 @@ declare class AppsPage {
|
|
|
15
15
|
readonly consoleLink: Locator;
|
|
16
16
|
readonly camundaComponentsButton: Locator;
|
|
17
17
|
readonly camundaAppsButton: Locator;
|
|
18
|
+
readonly hubSidebar: Locator;
|
|
18
19
|
constructor(page: Page);
|
|
19
20
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
21
|
+
private expandHubClusterGroup;
|
|
22
|
+
private clickHubClusterApp;
|
|
23
|
+
isHubClusterAppPresent(clusterName: string, appName: string): Promise<boolean>;
|
|
20
24
|
clickModeler(): Promise<void>;
|
|
21
25
|
clickTasklist(clusterName: string): Promise<void>;
|
|
22
26
|
private doClickClusterInOperate;
|
|
@@ -26,8 +30,8 @@ declare class AppsPage {
|
|
|
26
30
|
clickOptimize(clusterName: string): Promise<void>;
|
|
27
31
|
clickCamundaApps(maxRetries?: number): Promise<void>;
|
|
28
32
|
clickConsoleLink(): Promise<void>;
|
|
29
|
-
assertOperatePresent(shouldBeVisible?: boolean): Promise<void>;
|
|
30
|
-
assertTasklistPresent(shouldBeVisible?: boolean): Promise<void>;
|
|
31
|
-
assertOptimizePresent(shouldBeVisible?: boolean): Promise<void>;
|
|
33
|
+
assertOperatePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
|
|
34
|
+
assertTasklistPresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
|
|
35
|
+
assertOptimizePresent(clusterName: string, shouldBeVisible?: boolean): Promise<void>;
|
|
32
36
|
}
|
|
33
37
|
export { AppsPage };
|
|
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.AppsPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
|
+
function escapeRegExp(value) {
|
|
7
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
8
|
+
}
|
|
9
|
+
function isHubUrl(url) {
|
|
10
|
+
return new URL(url).hostname.startsWith('hub.');
|
|
11
|
+
}
|
|
6
12
|
class AppsPage {
|
|
7
13
|
page;
|
|
8
14
|
modelerLink;
|
|
@@ -19,12 +25,17 @@ class AppsPage {
|
|
|
19
25
|
consoleLink;
|
|
20
26
|
camundaComponentsButton;
|
|
21
27
|
camundaAppsButton;
|
|
28
|
+
hubSidebar;
|
|
22
29
|
constructor(page) {
|
|
23
30
|
this.page = page;
|
|
24
31
|
this.modelerLink = page
|
|
25
32
|
.getByRole('link', { name: 'Modeler', exact: true })
|
|
26
33
|
.or(page.getByLabel('App Panel').getByLabel('Camunda Modeler'))
|
|
27
34
|
.first();
|
|
35
|
+
// Camunda Hub's persistent "Main navigation" sidebar lists each cluster
|
|
36
|
+
// with its own apps (Operate/Tasklist/Optimize/...) as child links --
|
|
37
|
+
// there is no per-app switcher panel to open on Hub (see clickCamundaApps).
|
|
38
|
+
this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
|
|
28
39
|
this.appSwitcherButton = page.getByLabel('App Switcher');
|
|
29
40
|
this.tasklistLink = page.getByRole('link', { name: 'Tasklist', exact: true });
|
|
30
41
|
this.operateLink = page.getByRole('link', { name: 'Operate', exact: true });
|
|
@@ -59,7 +70,130 @@ class AppsPage {
|
|
|
59
70
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
|
|
60
71
|
await cluster.click();
|
|
61
72
|
}
|
|
73
|
+
// Camunda Hub's sidebar groups by cluster, not by app: each cluster has its
|
|
74
|
+
// own row with per-app child links (Operate/Tasklist/Optimize/...), the
|
|
75
|
+
// opposite axis from the old Console app-switcher (which grouped by app,
|
|
76
|
+
// with a cluster picked underneath it -- see doClickClusterInOperate and
|
|
77
|
+
// friends below). Expand the cluster's row if its children aren't already
|
|
78
|
+
// showing, then click the named app link inside it.
|
|
79
|
+
// Expands the requested cluster's Hub sidebar group and collapses every
|
|
80
|
+
// other expanded group, so a caller resolving an app link sidebar-wide
|
|
81
|
+
// (see clickHubClusterApp and isHubClusterAppPresent below) gets at most
|
|
82
|
+
// one match, scoped to this cluster.
|
|
83
|
+
async expandHubClusterGroup(clusterName) {
|
|
84
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the actual
|
|
85
|
+
// shape: "the design system splits the group row into a label control
|
|
86
|
+
// and a chevron control, both labelled by the cluster name; either one
|
|
87
|
+
// opens it" -- there is no separate "Toggle <name>" control, and the
|
|
88
|
+
// child links do not exist in the DOM at all until expanded (not merely
|
|
89
|
+
// hidden), nor are they nested under any container matchable by cluster
|
|
90
|
+
// name once expanded. So: find the toggle by the cluster's own name
|
|
91
|
+
// (`.first()` because both controls share that name), click it, then
|
|
92
|
+
// look up the app link sidebar-wide rather than scoped to a "group"
|
|
93
|
+
// container that doesn't reliably exist.
|
|
94
|
+
// Check THIS cluster's own toggle state, not "is any app link with this
|
|
95
|
+
// name visible anywhere in the sidebar" -- with more than one cluster in
|
|
96
|
+
// the org, a DIFFERENT cluster's group can already be expanded (left over
|
|
97
|
+
// from an earlier action on the same page), and that cluster's own
|
|
98
|
+
// Operate/Tasklist/Optimize link would satisfy an unscoped visibility
|
|
99
|
+
// check, skipping expansion of the requested cluster entirely and then
|
|
100
|
+
// clicking the wrong cluster's link. The child links are not nested under
|
|
101
|
+
// any container matchable by cluster name once expanded (confirmed
|
|
102
|
+
// against camunda-hub's own nav-shell-ds.spec.saas.tsx), so the toggle's
|
|
103
|
+
// own aria-expanded is the only reliable per-cluster signal available.
|
|
104
|
+
const toggle = this.hubSidebar
|
|
105
|
+
.getByRole('button', { name: clusterName, exact: true })
|
|
106
|
+
.first();
|
|
107
|
+
const alreadyExpanded = (await toggle.getAttribute('aria-expanded').catch(() => null)) === 'true';
|
|
108
|
+
if (!alreadyExpanded) {
|
|
109
|
+
// This is the only expansion action in the Hub path -- swallowing its
|
|
110
|
+
// failure made a broken expansion look successful and left the caller
|
|
111
|
+
// to burn the full 30s below waiting on an app link that could never
|
|
112
|
+
// appear, hiding the actionable error (e.g. a toggle that doesn't
|
|
113
|
+
// exist or isn't clickable) behind an unrelated timeout.
|
|
114
|
+
await toggle.click({ timeout: 10000 });
|
|
115
|
+
}
|
|
116
|
+
// An app link resolved after this is sidebar-wide (see callers below), so
|
|
117
|
+
// if an EARLIER call in the same test left a different cluster's group
|
|
118
|
+
// expanded too, two "Operate" (say) links now exist and that becomes a
|
|
119
|
+
// strict-mode violation. Collapse every other expanded group before
|
|
120
|
+
// resolving it -- there is no container to scope the link to instead,
|
|
121
|
+
// since the child links aren't nested under anything matchable by
|
|
122
|
+
// cluster name once expanded.
|
|
123
|
+
// hasNotText filters DOM text content, not the accessible name -- the
|
|
124
|
+
// expanded chevron can be labelled via aria-label with no text content
|
|
125
|
+
// of its own, in which case hasNotText would fail to exclude the toggle
|
|
126
|
+
// we just expanded and this loop would immediately collapse it again.
|
|
127
|
+
// Match by accessible name (same algorithm getByRole's own name option
|
|
128
|
+
// uses) instead, excluding an exact match on clusterName.
|
|
129
|
+
const otherExpandedToggles = this.hubSidebar.getByRole('button', {
|
|
130
|
+
expanded: true,
|
|
131
|
+
name: new RegExp(`^(?!${escapeRegExp(clusterName)}$).+$`),
|
|
132
|
+
});
|
|
133
|
+
for (let i = 0; i < 5 && (await otherExpandedToggles.count()) > 0; i++) {
|
|
134
|
+
// Let a failed collapse throw rather than swallowing it: the app link
|
|
135
|
+
// is intentionally sidebar-wide, so a group left expanded here produces
|
|
136
|
+
// a strict-mode violation (or the wrong cluster's link) below anyway --
|
|
137
|
+
// failing at the actual cause is more useful than at that symptom.
|
|
138
|
+
await otherExpandedToggles.first().click({ timeout: 10000 });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
async clickHubClusterApp(clusterName, appName) {
|
|
142
|
+
await this.expandHubClusterGroup(clusterName);
|
|
143
|
+
// Not exact: camunda-hub's own nav-shell-ds.spec.saas.tsx asserts this same
|
|
144
|
+
// link with a substring/regex match (`{name: /Operate/}`), not an exact
|
|
145
|
+
// string -- and CI traces confirm why: even after the toggle click
|
|
146
|
+
// succeeds (no error thrown), an exact-match lookup here still times out
|
|
147
|
+
// with "element(s) not found", meaning the rendered link's accessible
|
|
148
|
+
// name is not byte-for-byte "Operate"/"Tasklist"/"Optimize" (likely
|
|
149
|
+
// additional content from the design-system sidebar item, e.g. an icon
|
|
150
|
+
// or badge). The five app names are distinct enough that a substring
|
|
151
|
+
// match carries no ambiguity risk.
|
|
152
|
+
const appLink = this.hubSidebar.getByRole('link', {
|
|
153
|
+
name: appName,
|
|
154
|
+
});
|
|
155
|
+
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
156
|
+
await appLink.click({ timeout: 10000 });
|
|
157
|
+
}
|
|
158
|
+
// Checks whether an app link is present inside the requested cluster's Hub
|
|
159
|
+
// sidebar group, without clicking it. Unlike clickHubClusterApp, absence is
|
|
160
|
+
// an expected, valid outcome here -- this is how callers assert per-cluster
|
|
161
|
+
// app authorization (e.g. an Analyst-role user should not see Operate),
|
|
162
|
+
// which on Hub renders as the link never appearing in the expanded group
|
|
163
|
+
// rather than a disabled control -- so this never throws on a miss.
|
|
164
|
+
async isHubClusterAppPresent(clusterName, appName) {
|
|
165
|
+
await this.expandHubClusterGroup(clusterName);
|
|
166
|
+
const appLink = this.hubSidebar.getByRole('link', { name: appName });
|
|
167
|
+
return appLink.isVisible({ timeout: 10000 }).catch(() => false);
|
|
168
|
+
}
|
|
62
169
|
async clickModeler() {
|
|
170
|
+
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
171
|
+
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
172
|
+
// check alone can't tell whether we're already there -- Hub's cluster
|
|
173
|
+
// pages are on the same hub.* hostname, and a no-op here would leave a
|
|
174
|
+
// caller like console-user-flows.spec.ts (which reaches clickModeler()
|
|
175
|
+
// from a cluster page) stranded with the next locator running against
|
|
176
|
+
// the wrong page. Navigate home through the header logo link instead,
|
|
177
|
+
// same as ModelerHomePage.clickHomeBreadcrumb -- clicking it when
|
|
178
|
+
// already on the Modeler home is a harmless in-SPA no-op.
|
|
179
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
180
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
181
|
+
// The href stays literally "/login" until the client's auth state
|
|
182
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
183
|
+
// clicking it in that window would log the test out instead of
|
|
184
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
185
|
+
// one rather than racing it.
|
|
186
|
+
const homeLink = header
|
|
187
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
188
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
189
|
+
.or(header
|
|
190
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
191
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
192
|
+
.first();
|
|
193
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
194
|
+
await homeLink.click({ timeout: 30000 });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
63
197
|
const maxRetries = 5;
|
|
64
198
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
65
199
|
try {
|
|
@@ -82,6 +216,10 @@ class AppsPage {
|
|
|
82
216
|
throw new Error(`Failed to click the modeler link after ${maxRetries} attempts.`);
|
|
83
217
|
}
|
|
84
218
|
async clickTasklist(clusterName) {
|
|
219
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
220
|
+
await this.clickHubClusterApp(clusterName, 'Tasklist');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
85
223
|
const maxRetries = 5;
|
|
86
224
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
87
225
|
try {
|
|
@@ -148,6 +286,10 @@ class AppsPage {
|
|
|
148
286
|
await this.clickCluster(this.optimizeListItem, clusterName);
|
|
149
287
|
}
|
|
150
288
|
async clickOperate(clusterName) {
|
|
289
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
290
|
+
await this.clickHubClusterApp(clusterName, 'Operate');
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
151
293
|
const maxRetries = 5;
|
|
152
294
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
153
295
|
try {
|
|
@@ -184,6 +326,10 @@ class AppsPage {
|
|
|
184
326
|
throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
|
|
185
327
|
}
|
|
186
328
|
async clickOptimize(clusterName) {
|
|
329
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
330
|
+
await this.clickHubClusterApp(clusterName, 'Optimize');
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
187
333
|
const maxRetries = 3;
|
|
188
334
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
189
335
|
try {
|
|
@@ -216,6 +362,14 @@ class AppsPage {
|
|
|
216
362
|
throw new Error(`Failed to click the optimize link after ${maxRetries} attempts.`);
|
|
217
363
|
}
|
|
218
364
|
async clickCamundaApps(maxRetries = 3) {
|
|
365
|
+
// Camunda Hub's rewritten shells (NavShell / NavShellDs) have no
|
|
366
|
+
// app-switcher button to open -- Modeler is already Hub's home area and
|
|
367
|
+
// the other apps are reached per-cluster via the persistent sidebar, not
|
|
368
|
+
// through a global switcher panel. Mirrors the same hostname guard in
|
|
369
|
+
// clickModeler() below.
|
|
370
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
219
373
|
await (0, sleep_1.sleep)(2000);
|
|
220
374
|
const appButtons = [
|
|
221
375
|
this.camundaComponentsButton,
|
|
@@ -248,6 +402,15 @@ class AppsPage {
|
|
|
248
402
|
}
|
|
249
403
|
}
|
|
250
404
|
async clickConsoleLink() {
|
|
405
|
+
// Camunda Hub folded Console into the same persistent shell as
|
|
406
|
+
// Modeler/home: Console's Dashboard/Clusters/Organization links are just
|
|
407
|
+
// another section of the same "Main navigation" sidebar, not a separate
|
|
408
|
+
// app to switch into. Its header banner (homePage.consoleBanner) is
|
|
409
|
+
// already visible from any Hub page, so there is no navigation action
|
|
410
|
+
// needed here -- mirrors the same hostname guard in clickModeler().
|
|
411
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
251
414
|
const maxRetries = 3;
|
|
252
415
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
253
416
|
try {
|
|
@@ -274,7 +437,16 @@ class AppsPage {
|
|
|
274
437
|
}
|
|
275
438
|
throw new Error(`Failed to click the console link after ${maxRetries} attempts.`);
|
|
276
439
|
}
|
|
277
|
-
|
|
440
|
+
// Asserts whether the current user's role grants them Operate access.
|
|
441
|
+
// clusterName is required on Hub (see isHubClusterAppPresent) but unused on
|
|
442
|
+
// the legacy Console app-switcher, which has no per-cluster concept here --
|
|
443
|
+
// the caller's single active cluster is implicit in operateLink/operateButton.
|
|
444
|
+
async assertOperatePresent(clusterName, shouldBeVisible = true) {
|
|
445
|
+
if (isHubUrl(this.page.url())) {
|
|
446
|
+
const present = await this.isHubClusterAppPresent(clusterName, 'Operate');
|
|
447
|
+
(0, test_1.expect)(present).toBe(shouldBeVisible);
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
278
450
|
const maxRetries = 3;
|
|
279
451
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
280
452
|
try {
|
|
@@ -303,7 +475,13 @@ class AppsPage {
|
|
|
303
475
|
}
|
|
304
476
|
throw new Error(`Failed to assert the operate link visibility after ${maxRetries} attempts.`);
|
|
305
477
|
}
|
|
306
|
-
|
|
478
|
+
// See assertOperatePresent for the clusterName/Hub note.
|
|
479
|
+
async assertTasklistPresent(clusterName, shouldBeVisible = true) {
|
|
480
|
+
if (isHubUrl(this.page.url())) {
|
|
481
|
+
const present = await this.isHubClusterAppPresent(clusterName, 'Tasklist');
|
|
482
|
+
(0, test_1.expect)(present).toBe(shouldBeVisible);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
307
485
|
const maxRetries = 3;
|
|
308
486
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
309
487
|
try {
|
|
@@ -332,7 +510,13 @@ class AppsPage {
|
|
|
332
510
|
}
|
|
333
511
|
throw new Error(`Failed to assert the tasklist link visibility after ${maxRetries} attempts.`);
|
|
334
512
|
}
|
|
335
|
-
|
|
513
|
+
// See assertOperatePresent for the clusterName/Hub note.
|
|
514
|
+
async assertOptimizePresent(clusterName, shouldBeVisible = true) {
|
|
515
|
+
if (isHubUrl(this.page.url())) {
|
|
516
|
+
const present = await this.isHubClusterAppPresent(clusterName, 'Optimize');
|
|
517
|
+
(0, test_1.expect)(present).toBe(shouldBeVisible);
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
336
520
|
const maxRetries = 3;
|
|
337
521
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
338
522
|
try {
|
|
@@ -118,10 +118,30 @@ class ClusterPage {
|
|
|
118
118
|
});
|
|
119
119
|
}
|
|
120
120
|
async clickClusterBanner() {
|
|
121
|
-
|
|
121
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
122
|
+
// persistent "Main navigation" sidebar, and once the org has environments
|
|
123
|
+
// enabled that link is further renamed "Environments" (same rollout
|
|
124
|
+
// HomePage.clusterTab handles).
|
|
125
|
+
const clusterLink = this.page
|
|
122
126
|
.getByRole('banner')
|
|
123
127
|
.getByRole('link', { name: 'Clusters' })
|
|
124
|
-
.
|
|
128
|
+
.or(this.page
|
|
129
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
130
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
131
|
+
.or(this.page
|
|
132
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
133
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
134
|
+
await clusterLink.click({ timeout: 60000 });
|
|
135
|
+
// Landing on Hub's Environments overview (see above) renders clusters as
|
|
136
|
+
// cards, not the getByRole('row') table this page's own locators expect
|
|
137
|
+
// -- route through the page's own "Clusters" tab to reach the classic
|
|
138
|
+
// route. Same fix as HomePage.clickClusters().
|
|
139
|
+
const clustersPageTab = this.page
|
|
140
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
141
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
142
|
+
if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
143
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
144
|
+
}
|
|
125
145
|
}
|
|
126
146
|
async deleteCluster(name) {
|
|
127
147
|
const clusterInstance = this.cluster(name).first();
|
|
@@ -49,9 +49,19 @@ class ClusterSecretsPage {
|
|
|
49
49
|
.getByRole('dialog')
|
|
50
50
|
.getByRole('button', { name: 'Delete', exact: true });
|
|
51
51
|
this.dialog = page.getByRole('dialog');
|
|
52
|
+
// Camunda Hub has no `banner` landmark; "Clusters" instead lives in its
|
|
53
|
+
// persistent "Main navigation" sidebar (same fix as HomePage.clusterTab).
|
|
54
|
+
// Once the org has environments enabled, that sidebar link is further
|
|
55
|
+
// renamed "Environments" -- see HomePage.clusterTab for the full context.
|
|
52
56
|
this.clusterBanner = this.page
|
|
53
57
|
.getByRole('banner')
|
|
54
|
-
.getByRole('link', { name: 'Clusters' })
|
|
58
|
+
.getByRole('link', { name: 'Clusters' })
|
|
59
|
+
.or(this.page
|
|
60
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
61
|
+
.getByRole('link', { name: 'Clusters', exact: true }))
|
|
62
|
+
.or(this.page
|
|
63
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
64
|
+
.getByRole('link', { name: 'Environments', exact: true }));
|
|
55
65
|
// exact: true because console-user-flows.spec.ts creates a second cluster
|
|
56
66
|
// called "UI Test Cluster" in the same org -- a substring match on
|
|
57
67
|
// "Test Cluster" then resolves to both links and fails on strict mode.
|
|
@@ -158,6 +168,17 @@ class ClusterSecretsPage {
|
|
|
158
168
|
await this.dismissOpenModal();
|
|
159
169
|
await (0, test_1.expect)(this.clusterBanner).toBeVisible({ timeout: 30000 });
|
|
160
170
|
await this.clusterBanner.click({ timeout: 60000 });
|
|
171
|
+
// Landing on Hub's Environments overview (see clusterBanner above) renders
|
|
172
|
+
// clusters as cards, not the getByRole('link') the clusterLink() locator
|
|
173
|
+
// below expects to find in a row -- route through the page's own
|
|
174
|
+
// "Clusters" tab to reach the classic route. Same fix as
|
|
175
|
+
// HomePage.clickClusters().
|
|
176
|
+
const clustersPageTab = this.page
|
|
177
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
178
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
179
|
+
if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
180
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
181
|
+
}
|
|
161
182
|
}
|
|
162
183
|
async clickCluster(name) {
|
|
163
184
|
await (0, test_1.expect)(this.clusterLink(name)).toBeVisible({ timeout: 30000 });
|
|
@@ -15,6 +15,8 @@ declare class HomePage {
|
|
|
15
15
|
readonly modelerBanner: Locator;
|
|
16
16
|
readonly gettingStartedHeading: Locator;
|
|
17
17
|
readonly buttonSkipCustomization: Locator;
|
|
18
|
+
readonly hubSidebar: Locator;
|
|
19
|
+
readonly hubHeader: Locator;
|
|
18
20
|
constructor(page: Page);
|
|
19
21
|
clickClusters(): Promise<void>;
|
|
20
22
|
clickDashboard(): Promise<void>;
|
|
@@ -18,12 +18,39 @@ class HomePage {
|
|
|
18
18
|
modelerBanner;
|
|
19
19
|
gettingStartedHeading;
|
|
20
20
|
buttonSkipCustomization;
|
|
21
|
+
hubSidebar;
|
|
22
|
+
hubHeader;
|
|
21
23
|
constructor(page) {
|
|
22
24
|
this.page = page;
|
|
23
25
|
this.dashboardTab = page.getByRole('link', { name: 'Dashboard' });
|
|
26
|
+
// Camunda Hub (which replaced Console on SaaS) moved Clusters from the
|
|
27
|
+
// header into its "Main navigation" sidebar.
|
|
28
|
+
this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
|
|
29
|
+
// Structural signal that Hub's authenticated shell has mounted, used as a
|
|
30
|
+
// fallback below: the header logo link's accessible name AND href both
|
|
31
|
+
// depend on client state (aria-label toggles on the workspacesEnabled
|
|
32
|
+
// flag; href is literally `/login` until `isAuthenticated` flips true --
|
|
33
|
+
// camunda-hub NavShellDs.common.tsx) that can lag just after login,
|
|
34
|
+
// making a name+href-exclusion match miss the timing window. The header
|
|
35
|
+
// only renders the user menu once authenticated, so this never races.
|
|
36
|
+
this.hubHeader = page
|
|
37
|
+
.locator('header[data-slot="app-header"]')
|
|
38
|
+
.filter({ has: page.locator('[data-slot="user-menu-trigger"]') });
|
|
39
|
+
// camunda-hub's use-ds-sidebar-items.tsx: "Environments supersede the
|
|
40
|
+
// older Clusters surface: once the environments feature is on, the
|
|
41
|
+
// workspace nav drops Clusters entirely for every role." The sidebar
|
|
42
|
+
// link is renamed "Environments" (not merely relabelled -- "Clusters"
|
|
43
|
+
// stops rendering at all), but it still lists the same clusters by name
|
|
44
|
+
// underneath, so callers of clickClusters() need no other change.
|
|
24
45
|
this.clusterTab = page
|
|
25
46
|
.getByRole('banner')
|
|
26
|
-
.getByRole('link', { name: 'Clusters' })
|
|
47
|
+
.getByRole('link', { name: 'Clusters' })
|
|
48
|
+
.or(this.hubSidebar.getByRole('link', { name: 'Clusters', exact: true }))
|
|
49
|
+
.or(this.hubSidebar.getByRole('link', {
|
|
50
|
+
name: 'Environments',
|
|
51
|
+
exact: true,
|
|
52
|
+
}))
|
|
53
|
+
.first();
|
|
27
54
|
this.organizationTab = page.getByRole('link', { name: 'Organization' });
|
|
28
55
|
this.organizationViewTitle = page.getByText('Organization Management');
|
|
29
56
|
this.welcomeMessage = page.getByText('Welcome to Camunda!').first();
|
|
@@ -32,17 +59,43 @@ class HomePage {
|
|
|
32
59
|
});
|
|
33
60
|
this.camundaAppsButton = page.getByLabel('Camunda apps');
|
|
34
61
|
this.appSwitcher = page.getByLabel('App Switcher');
|
|
62
|
+
// Camunda Hub's rewritten shells (NavShell / NavShellDs) dropped the
|
|
63
|
+
// app-switcher dropdown entirely in favor of a persistent
|
|
64
|
+
// "Main navigation" sidebar (see hubSidebar above) -- there is no
|
|
65
|
+
// "Camunda components"/"Camunda apps"/"App Switcher" element to find on
|
|
66
|
+
// those shells. Accept the sidebar too so this locator resolves whichever
|
|
67
|
+
// shell the org's live rollout has reached.
|
|
35
68
|
this.camundaComponentsButton = page
|
|
36
69
|
.getByLabel('Camunda components')
|
|
37
70
|
.or(this.camundaAppsButton)
|
|
38
|
-
.or(this.appSwitcher)
|
|
71
|
+
.or(this.appSwitcher)
|
|
72
|
+
.or(this.hubSidebar);
|
|
39
73
|
this.openOrganizationButton = page.getByLabel('Open Organizations');
|
|
40
|
-
|
|
74
|
+
// Camunda Hub replaced the Console shell on SaaS; its header logo link is
|
|
75
|
+
// "Camunda Modeler home" on every page, Console pages included -- and, as
|
|
76
|
+
// of camunda-hub PR#29349, "Camunda Hub home" once workspaces are enabled
|
|
77
|
+
// for the org. Accept either name so login doesn't depend on which one
|
|
78
|
+
// this org's live rollout has reached.
|
|
79
|
+
this.consoleBanner = page
|
|
80
|
+
.getByRole('link', {
|
|
41
81
|
name: 'Camunda logo Console',
|
|
42
|
-
})
|
|
43
|
-
|
|
82
|
+
})
|
|
83
|
+
.or(page
|
|
84
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
85
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
86
|
+
.or(page
|
|
87
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
88
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
89
|
+
.or(this.hubHeader)
|
|
90
|
+
.first();
|
|
91
|
+
this.modelerBanner = page
|
|
92
|
+
.getByRole('banner', {
|
|
44
93
|
name: 'Camunda Modeler',
|
|
45
|
-
})
|
|
94
|
+
})
|
|
95
|
+
.or(page
|
|
96
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
97
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
98
|
+
.or(this.hubHeader);
|
|
46
99
|
this.gettingStartedHeading = page.getByText('start by creating a project');
|
|
47
100
|
this.buttonSkipCustomization = page.getByRole('button', {
|
|
48
101
|
name: 'Skip customization',
|
|
@@ -51,6 +104,17 @@ class HomePage {
|
|
|
51
104
|
async clickClusters() {
|
|
52
105
|
await (0, test_1.expect)(this.clusterTab).toBeVisible({ timeout: 15000 });
|
|
53
106
|
await this.clusterTab.click({ timeout: 15000 });
|
|
107
|
+
// Landing on Hub's Environments overview (see clusterTab above) doesn't
|
|
108
|
+
// put us on the classic row-based cluster list ClusterPage.ts expects --
|
|
109
|
+
// that page renders clusters as cards. Its own header renders a separate
|
|
110
|
+
// "Clusters" tab (camunda-hub EnvironmentsPageHeader.tsx, href /clusters)
|
|
111
|
+
// that does.
|
|
112
|
+
const clustersPageTab = this.page
|
|
113
|
+
.getByRole('navigation', { name: 'Environments views' })
|
|
114
|
+
.getByRole('link', { name: 'Clusters', exact: true });
|
|
115
|
+
if (await clustersPageTab.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
116
|
+
await clustersPageTab.click({ timeout: 15000 });
|
|
117
|
+
}
|
|
54
118
|
}
|
|
55
119
|
async clickDashboard() {
|
|
56
120
|
await this.dashboardTab.click({ timeout: 90000 });
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.LoginPage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const UtilitiesPage_1 = require("./UtilitiesPage");
|
|
6
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
6
7
|
class LoginPage {
|
|
7
8
|
page;
|
|
8
9
|
usernameInput;
|
|
@@ -52,6 +53,10 @@ class LoginPage {
|
|
|
52
53
|
}
|
|
53
54
|
async login(credentials = {}) {
|
|
54
55
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
56
|
+
// See loginWithTestUser below: this re-authenticates the same page and
|
|
57
|
+
// must clear the Hub bearer-token cache too, or getUserToken() can
|
|
58
|
+
// return whatever user was previously logged in on it.
|
|
59
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
55
60
|
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 60000 });
|
|
56
61
|
await this.fillUsername(username);
|
|
57
62
|
await this.clickContinueButton();
|
|
@@ -63,6 +68,12 @@ class LoginPage {
|
|
|
63
68
|
}
|
|
64
69
|
async loginWithTestUser(credentials = {}) {
|
|
65
70
|
const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
|
|
71
|
+
// See loginWithRetry in UtilitiesPage.ts: the Hub bearer-token cache in
|
|
72
|
+
// consoleApiHelpers.ts is keyed by Page for the tab's whole lifetime, so
|
|
73
|
+
// this re-authentication (a different user than whatever was logged in
|
|
74
|
+
// before) must clear it too, or getUserToken() can return the previous
|
|
75
|
+
// user's cached token.
|
|
76
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
66
77
|
// Navigate to app root to start the auth flow.
|
|
67
78
|
await this.page.goto('/');
|
|
68
79
|
await this.page
|
|
@@ -112,6 +123,10 @@ class LoginPage {
|
|
|
112
123
|
}
|
|
113
124
|
async loginWithoutOrgAssertion(credentials = {}) {
|
|
114
125
|
const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
|
|
126
|
+
// See loginWithTestUser above: this re-authenticates the same page and
|
|
127
|
+
// must clear the Hub bearer-token cache too, or getUserToken() can
|
|
128
|
+
// return whatever user was previously logged in on it.
|
|
129
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(this.page);
|
|
115
130
|
await (0, test_1.expect)(this.usernameInput).toBeVisible({ timeout: 180000 });
|
|
116
131
|
await this.fillUsername(username);
|
|
117
132
|
await this.clickContinueButton();
|
|
@@ -14,8 +14,12 @@ declare class ModelerCreatePage {
|
|
|
14
14
|
readonly autosaveIndicator: Locator;
|
|
15
15
|
readonly nameInput: Locator;
|
|
16
16
|
readonly diagramBreadcrumb: Locator;
|
|
17
|
+
readonly diagramActionsButton: Locator;
|
|
17
18
|
readonly renameDiagramNameButton: Locator;
|
|
18
19
|
readonly diagramNameInput: Locator;
|
|
20
|
+
readonly renameDialog: Locator;
|
|
21
|
+
readonly renameDialogInput: Locator;
|
|
22
|
+
readonly renameDialogSubmitButton: Locator;
|
|
19
23
|
readonly variableInput: Locator;
|
|
20
24
|
readonly embedFormButton: Locator;
|
|
21
25
|
readonly embedButton: Locator;
|
|
@@ -20,8 +20,12 @@ class ModelerCreatePage {
|
|
|
20
20
|
autosaveIndicator;
|
|
21
21
|
nameInput;
|
|
22
22
|
diagramBreadcrumb;
|
|
23
|
+
diagramActionsButton;
|
|
23
24
|
renameDiagramNameButton;
|
|
24
25
|
diagramNameInput;
|
|
26
|
+
renameDialog;
|
|
27
|
+
renameDialogInput;
|
|
28
|
+
renameDialogSubmitButton;
|
|
25
29
|
variableInput;
|
|
26
30
|
embedFormButton;
|
|
27
31
|
embedButton;
|
|
@@ -154,8 +158,26 @@ class ModelerCreatePage {
|
|
|
154
158
|
this.autosaveIndicator = page.locator('[data-test="autosave"]');
|
|
155
159
|
this.nameInput = page.getByLabel('Name', { exact: true });
|
|
156
160
|
this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
|
|
161
|
+
// Hub's newer nav (isNavV2: the design-system shell or C3NavigationV2)
|
|
162
|
+
// makes the legacy TopBar breadcrumb dropdown above a no-op and moves
|
|
163
|
+
// Rename/Duplicate/Delete behind a separate kebab button next to it,
|
|
164
|
+
// labeled "<entity name> actions" (use-file-page-actions.ts). Its accessible
|
|
165
|
+
// name always ends in " actions", regardless of the current diagram name.
|
|
166
|
+
this.diagramActionsButton = page
|
|
167
|
+
.getByRole('button', { name: / actions$/ })
|
|
168
|
+
.first();
|
|
157
169
|
this.renameDiagramNameButton = page.getByText('Rename');
|
|
158
170
|
this.diagramNameInput = page.locator('[data-test="editable-input"]');
|
|
171
|
+
// The design-system nav's Rename action opens a modal dialog
|
|
172
|
+
// (RenameDialogDs.tsx) instead of the legacy inline breadcrumb editor.
|
|
173
|
+
this.renameDialog = page.getByRole('dialog', { name: 'Rename', exact: true });
|
|
174
|
+
this.renameDialogInput = this.renameDialog.getByLabel('Name', {
|
|
175
|
+
exact: true,
|
|
176
|
+
});
|
|
177
|
+
this.renameDialogSubmitButton = this.renameDialog.getByRole('button', {
|
|
178
|
+
name: 'Rename',
|
|
179
|
+
exact: true,
|
|
180
|
+
});
|
|
159
181
|
this.variableInput = page.locator('textarea#variables-json');
|
|
160
182
|
this.embedFormButton = page.getByRole('button', { name: 'Link form' });
|
|
161
183
|
this.embedButton = page.locator('[data-test="confirm-move"]');
|
|
@@ -673,7 +695,19 @@ class ModelerCreatePage {
|
|
|
673
695
|
return;
|
|
674
696
|
}
|
|
675
697
|
try {
|
|
676
|
-
|
|
698
|
+
// Hub's newer nav (isNavV2) makes this legacy breadcrumb click a
|
|
699
|
+
// no-op and moves the dropdown behind a separate "<name> actions"
|
|
700
|
+
// kebab instead -- prefer it when present, and only fall back to the
|
|
701
|
+
// legacy breadcrumb when it is not (see diagramActionsButton).
|
|
702
|
+
const kebabVisible = await this.diagramActionsButton
|
|
703
|
+
.isVisible({ timeout: 5000 })
|
|
704
|
+
.catch(() => false);
|
|
705
|
+
if (kebabVisible) {
|
|
706
|
+
await this.diagramActionsButton.click({ timeout: 20000 });
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
await this.diagramBreadcrumb.click({ timeout: 20000 });
|
|
710
|
+
}
|
|
677
711
|
await this.renameDiagramNameButton.waitFor({
|
|
678
712
|
state: 'visible',
|
|
679
713
|
timeout: 20000,
|
|
@@ -726,7 +760,9 @@ class ModelerCreatePage {
|
|
|
726
760
|
// failure: the assertion below is what decides whether this worked.
|
|
727
761
|
await this.renameDiagramNameButton.dispatchEvent('click');
|
|
728
762
|
}
|
|
729
|
-
|
|
763
|
+
// The design-system nav's "Rename" opens a modal dialog
|
|
764
|
+
// (RenameDialogDs.tsx) instead of the legacy inline breadcrumb editor.
|
|
765
|
+
await (0, test_1.expect)(this.diagramNameInput.or(this.renameDialogInput)).toBeVisible({ timeout: 10000 });
|
|
730
766
|
}).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
|
|
731
767
|
}
|
|
732
768
|
async enterDiagramName(name) {
|
|
@@ -740,9 +776,27 @@ class ModelerCreatePage {
|
|
|
740
776
|
const maxAttempts = 3;
|
|
741
777
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
742
778
|
try {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
779
|
+
// The design-system nav's Rename opens a modal dialog
|
|
780
|
+
// (RenameDialogDs.tsx) instead of the legacy inline breadcrumb
|
|
781
|
+
// editor -- fill and submit whichever is actually open.
|
|
782
|
+
const modalOpen = await this.renameDialog
|
|
783
|
+
.isVisible()
|
|
784
|
+
.catch(() => false);
|
|
785
|
+
if (modalOpen) {
|
|
786
|
+
await this.renameDialogInput.fill(name, { timeout: 45000 });
|
|
787
|
+
await this.renameDialogSubmitButton.click({ timeout: 10000 });
|
|
788
|
+
await this.renameDialog.waitFor({ state: 'hidden', timeout: 30000 });
|
|
789
|
+
}
|
|
790
|
+
else {
|
|
791
|
+
await this.diagramNameInput.fill(name, { timeout: 45000 });
|
|
792
|
+
await this.diagramNameInput.press('Enter', { timeout: 45000 });
|
|
793
|
+
}
|
|
794
|
+
// The legacy breadcrumb shows the new name directly; the
|
|
795
|
+
// design-system kebab instead embeds it in its own accessible name
|
|
796
|
+
// ("<name> actions"). Accept either as confirmation the rename took.
|
|
797
|
+
await this.diagramNameBreadcrumb(name)
|
|
798
|
+
.or(this.page.getByRole('button', { name: `${name} actions` }))
|
|
799
|
+
.waitFor({
|
|
746
800
|
state: 'visible',
|
|
747
801
|
timeout: 30000,
|
|
748
802
|
});
|
|
@@ -2,12 +2,18 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
import { LoginPage } from './LoginPage';
|
|
3
3
|
declare class ModelerHomePage {
|
|
4
4
|
private defaultFolderName;
|
|
5
|
+
private defaultClusterName;
|
|
5
6
|
private page;
|
|
6
7
|
private processApplicationPage;
|
|
7
8
|
private processApplicationUrl?;
|
|
8
9
|
private processApplicationName?;
|
|
9
10
|
readonly modelerPageBanner: Locator;
|
|
10
11
|
readonly createNewProjectButton: Locator;
|
|
12
|
+
readonly createWorkspaceButton: Locator;
|
|
13
|
+
readonly workspaceWizardNameInput: Locator;
|
|
14
|
+
readonly workspaceWizardSubmitButton: Locator;
|
|
15
|
+
readonly workspaceWizardMembersStep: Locator;
|
|
16
|
+
readonly workspaceWizardEnvironmentsHeading: Locator;
|
|
11
17
|
readonly projectNameInput: Locator;
|
|
12
18
|
readonly formNameInput: Locator;
|
|
13
19
|
readonly chooseBpmnTemplateButton: Locator;
|
|
@@ -31,7 +37,11 @@ declare class ModelerHomePage {
|
|
|
31
37
|
readonly closeButton: Locator;
|
|
32
38
|
readonly formNameBreadcrumb: (formName: string) => Locator;
|
|
33
39
|
readonly formBreadcrumbButton: Locator;
|
|
40
|
+
readonly formActionsButton: Locator;
|
|
34
41
|
readonly renameFormMenuItem: Locator;
|
|
42
|
+
readonly renameDialog: Locator;
|
|
43
|
+
readonly renameDialogInput: Locator;
|
|
44
|
+
readonly renameDialogSubmitButton: Locator;
|
|
35
45
|
constructor(page: Page);
|
|
36
46
|
dismissOverlays(): Promise<void>;
|
|
37
47
|
clickCreateNewProjectButton(): Promise<void>;
|
|
@@ -63,6 +73,7 @@ declare class ModelerHomePage {
|
|
|
63
73
|
clickHomeBreadcrumb(): Promise<void>;
|
|
64
74
|
clickOpenOrganizationsButton(): Promise<void>;
|
|
65
75
|
clickCrossComponentProjectFolder(): Promise<void>;
|
|
76
|
+
private createWorkspaceViaWizard;
|
|
66
77
|
createCrossComponentProjectFolder(): Promise<void>;
|
|
67
78
|
/**
|
|
68
79
|
* Asserts the Cross Component project folder is listed on the Modeler home
|
|
@@ -9,6 +9,11 @@ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
|
9
9
|
const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
|
|
10
10
|
class ModelerHomePage {
|
|
11
11
|
defaultFolderName = 'Cross Component Test Project';
|
|
12
|
+
// The default cluster test-setup.spec.ts's "Create Default Cluster" test
|
|
13
|
+
// provisions for the whole suite -- assigned to the shared workspace's
|
|
14
|
+
// Environments step so downstream specs can deploy to it without a manual
|
|
15
|
+
// assignment step first.
|
|
16
|
+
defaultClusterName = 'Test Cluster';
|
|
12
17
|
page;
|
|
13
18
|
// Since camunda/camunda-hub#25831 files and folders cannot sit at a
|
|
14
19
|
// workspace ("project" in today's UI) root -- only process applications and
|
|
@@ -25,6 +30,11 @@ class ModelerHomePage {
|
|
|
25
30
|
processApplicationName;
|
|
26
31
|
modelerPageBanner;
|
|
27
32
|
createNewProjectButton;
|
|
33
|
+
createWorkspaceButton;
|
|
34
|
+
workspaceWizardNameInput;
|
|
35
|
+
workspaceWizardSubmitButton;
|
|
36
|
+
workspaceWizardMembersStep;
|
|
37
|
+
workspaceWizardEnvironmentsHeading;
|
|
28
38
|
projectNameInput;
|
|
29
39
|
formNameInput;
|
|
30
40
|
chooseBpmnTemplateButton;
|
|
@@ -48,13 +58,31 @@ class ModelerHomePage {
|
|
|
48
58
|
closeButton;
|
|
49
59
|
formNameBreadcrumb;
|
|
50
60
|
formBreadcrumbButton;
|
|
61
|
+
formActionsButton;
|
|
51
62
|
renameFormMenuItem;
|
|
63
|
+
renameDialog;
|
|
64
|
+
renameDialogInput;
|
|
65
|
+
renameDialogSubmitButton;
|
|
52
66
|
constructor(page) {
|
|
53
67
|
this.page = page;
|
|
54
68
|
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
55
|
-
|
|
69
|
+
// Hub's design-system shell renders the app header as a role=banner with no
|
|
70
|
+
// accessible name after the Camunda Hub rebrand, so the classic
|
|
71
|
+
// name-matched banner no longer resolves; the Modeler logo link inside it
|
|
72
|
+
// carries "Camunda Modeler home" or, once camunda-hub PR#29349's rename
|
|
73
|
+
// has rolled out to an org, "Camunda Hub home". Keep the legacy
|
|
74
|
+
// named-banner branch for shells where the rename has not shipped yet.
|
|
75
|
+
this.modelerPageBanner = page
|
|
76
|
+
.getByRole('banner', {
|
|
56
77
|
name: 'Camunda Modeler',
|
|
57
|
-
})
|
|
78
|
+
})
|
|
79
|
+
.or(page
|
|
80
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
81
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
82
|
+
.or(page
|
|
83
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
84
|
+
.and(page.locator(':not([href$="/login"])')))
|
|
85
|
+
.first();
|
|
58
86
|
// Hub renamed this button to "Create project" on both the classic and the
|
|
59
87
|
// design-system shell. The legacy label is kept so the flow still works
|
|
60
88
|
// against Modeler builds where the rename has not shipped yet. exact:true
|
|
@@ -67,6 +95,24 @@ class ModelerHomePage {
|
|
|
67
95
|
name: 'Create project',
|
|
68
96
|
exact: true,
|
|
69
97
|
}));
|
|
98
|
+
// Camunda Hub, which replaced Console + Web Modeler on SaaS, tops the
|
|
99
|
+
// hierarchy with a Workspaces overview whose "Create workspace" CTA opens a
|
|
100
|
+
// wizard (General -> Members) that names the workspace up front. Same
|
|
101
|
+
// hooks as pages/8.10/ModelerHomePage.ts. exact:true keeps the split
|
|
102
|
+
// button's options chevron out of the match.
|
|
103
|
+
this.createWorkspaceButton = page.getByRole('button', {
|
|
104
|
+
name: 'Create workspace',
|
|
105
|
+
exact: true,
|
|
106
|
+
});
|
|
107
|
+
this.workspaceWizardNameInput = page.locator('#workspace-name');
|
|
108
|
+
this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
|
|
109
|
+
this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
|
|
110
|
+
// When the org has environments enabled, an "Assign environments" step is
|
|
111
|
+
// inserted after Members -- camunda-hub's own
|
|
112
|
+
// e2e-playwright/pages/CreateWorkspacePage.ts detects it the same way.
|
|
113
|
+
this.workspaceWizardEnvironmentsHeading = page.getByRole('heading', {
|
|
114
|
+
name: 'Assign environments',
|
|
115
|
+
});
|
|
70
116
|
this.projectNameInput = page.locator('[data-test="editable-input"]');
|
|
71
117
|
this.formNameInput = page.locator('[data-test="editable-input"]');
|
|
72
118
|
this.idpApplicationNameInput = page.getByPlaceholder('New IDP Application');
|
|
@@ -98,10 +144,13 @@ class ModelerHomePage {
|
|
|
98
144
|
this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
|
|
99
145
|
this.openOrganizationsButton = page.getByLabel('Open Organizations');
|
|
100
146
|
this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
|
|
147
|
+
// On the Camunda Hub Workspaces overview the folder is a workspace tile
|
|
148
|
+
// whose "Open workspace <name>" button navigates into it.
|
|
101
149
|
this.crossComponentProjectFolder = page
|
|
102
|
-
.
|
|
150
|
+
.getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
|
|
151
|
+
.or(page.getByTitle(this.defaultFolderName, {
|
|
103
152
|
exact: true,
|
|
104
|
-
})
|
|
153
|
+
}))
|
|
105
154
|
.first();
|
|
106
155
|
this.rows = page.getByRole('row');
|
|
107
156
|
this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
|
|
@@ -116,7 +165,26 @@ class ModelerHomePage {
|
|
|
116
165
|
hasText: formName,
|
|
117
166
|
});
|
|
118
167
|
this.formBreadcrumbButton = page.locator('button[data-test="breadcrumb-form"]');
|
|
119
|
-
|
|
168
|
+
// Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
|
|
169
|
+
// Rename/Duplicate/Delete behind a separate "<name> actions" kebab
|
|
170
|
+
// instead (use-file-page-actions.ts); its own menu item carries no
|
|
171
|
+
// data-test, just the text "Rename".
|
|
172
|
+
this.formActionsButton = page
|
|
173
|
+
.getByRole('button', { name: / actions$/ })
|
|
174
|
+
.first();
|
|
175
|
+
this.renameFormMenuItem = page
|
|
176
|
+
.locator('[data-test="rename-form"]')
|
|
177
|
+
.or(page.getByRole('menuitem', { name: 'Rename', exact: true }));
|
|
178
|
+
// The design-system nav's Rename opens a modal dialog (RenameDialogDs.tsx)
|
|
179
|
+
// instead of the legacy inline breadcrumb editor.
|
|
180
|
+
this.renameDialog = page.getByRole('dialog', { name: 'Rename', exact: true });
|
|
181
|
+
this.renameDialogInput = this.renameDialog.getByLabel('Name', {
|
|
182
|
+
exact: true,
|
|
183
|
+
});
|
|
184
|
+
this.renameDialogSubmitButton = this.renameDialog.getByRole('button', {
|
|
185
|
+
name: 'Rename',
|
|
186
|
+
exact: true,
|
|
187
|
+
});
|
|
120
188
|
}
|
|
121
189
|
// The release-announcement dialog ("Check out our new release") re-mounts
|
|
122
190
|
// after the projects list re-renders, so one dismissal pass is not enough.
|
|
@@ -300,10 +368,28 @@ class ModelerHomePage {
|
|
|
300
368
|
if (!editorOpen) {
|
|
301
369
|
await this.openFormRenameEditor();
|
|
302
370
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
await this.
|
|
371
|
+
// The design-system nav's Rename opens a modal dialog
|
|
372
|
+
// (RenameDialogDs.tsx) instead of the legacy inline breadcrumb
|
|
373
|
+
// editor -- fill and submit whichever is actually open.
|
|
374
|
+
const modalOpen = await this.renameDialog
|
|
375
|
+
.isVisible()
|
|
376
|
+
.catch(() => false);
|
|
377
|
+
if (modalOpen) {
|
|
378
|
+
await this.renameDialogInput.fill(name, { timeout: 45000 });
|
|
379
|
+
await this.renameDialogSubmitButton.click({ timeout: 10000 });
|
|
380
|
+
await this.renameDialog.waitFor({ state: 'hidden', timeout: 30000 });
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
await this.formNameInput.click({ timeout: 30000 });
|
|
384
|
+
await this.formNameInput.fill(name);
|
|
385
|
+
await this.formNameInput.press('Enter');
|
|
386
|
+
}
|
|
387
|
+
// The legacy breadcrumb shows the new name directly; the
|
|
388
|
+
// design-system kebab instead embeds it in its own accessible name
|
|
389
|
+
// ("<name> actions"). Accept either as confirmation the rename took.
|
|
390
|
+
await this.formNameBreadcrumb(name)
|
|
391
|
+
.or(this.page.getByRole('button', { name: `${name} actions` }))
|
|
392
|
+
.waitFor({
|
|
307
393
|
state: 'visible',
|
|
308
394
|
timeout: 30000,
|
|
309
395
|
});
|
|
@@ -319,7 +405,18 @@ class ModelerHomePage {
|
|
|
319
405
|
}
|
|
320
406
|
}
|
|
321
407
|
async openFormRenameEditor() {
|
|
322
|
-
|
|
408
|
+
// Hub's newer nav (isNavV2) makes formBreadcrumbButton a no-op and moves
|
|
409
|
+
// the dropdown behind a separate "<name> actions" kebab instead -- prefer
|
|
410
|
+
// it when present, and only fall back to the legacy breadcrumb otherwise.
|
|
411
|
+
const kebabVisible = await this.formActionsButton
|
|
412
|
+
.isVisible({ timeout: 5000 })
|
|
413
|
+
.catch(() => false);
|
|
414
|
+
if (kebabVisible) {
|
|
415
|
+
await this.formActionsButton.click({ timeout: 60000 });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
await this.formBreadcrumbButton.click({ timeout: 60000 });
|
|
419
|
+
}
|
|
323
420
|
await this.renameFormMenuItem.click({ timeout: 30000 });
|
|
324
421
|
}
|
|
325
422
|
// Kept under its original name because it is what the existing call sites
|
|
@@ -432,6 +529,28 @@ class ModelerHomePage {
|
|
|
432
529
|
return this.becameVisible(this.modelerPageBanner, timeout);
|
|
433
530
|
}
|
|
434
531
|
async clickHomeBreadcrumb() {
|
|
532
|
+
// Camunda Hub has no Modeler home breadcrumb: its home is the Workspaces
|
|
533
|
+
// overview, reached through the header logo link so the SPA keeps its
|
|
534
|
+
// in-memory session (a full navigation re-runs the whole Auth0 hand-off).
|
|
535
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
536
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
537
|
+
// The href stays literally "/login" until the client's auth state
|
|
538
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
539
|
+
// clicking it in that window would log the test out instead of
|
|
540
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
541
|
+
// one rather than racing it (same hazard AppsPage.clickModeler()
|
|
542
|
+
// guards against).
|
|
543
|
+
const homeLink = header
|
|
544
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
545
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
546
|
+
.or(header
|
|
547
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
548
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
549
|
+
.first();
|
|
550
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
551
|
+
await homeLink.click({ timeout: 30000 });
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
435
554
|
try {
|
|
436
555
|
await this.homeBreadcrumb.click();
|
|
437
556
|
}
|
|
@@ -468,6 +587,57 @@ class ModelerHomePage {
|
|
|
468
587
|
},
|
|
469
588
|
});
|
|
470
589
|
}
|
|
590
|
+
// Drives Camunda Hub's workspace-creation wizard. The name is entered on
|
|
591
|
+
// the General step, which enables "Next"; the same submit button then
|
|
592
|
+
// relabels itself through the remaining steps and finally reads "Create
|
|
593
|
+
// workspace" once it creates the workspace and routes the creator into its
|
|
594
|
+
// projects listing. Gate the Members-step click on that step becoming
|
|
595
|
+
// active so the shared submit button is not clicked twice while still on
|
|
596
|
+
// General.
|
|
597
|
+
async createWorkspaceViaWizard(name) {
|
|
598
|
+
await this.dismissOverlays();
|
|
599
|
+
await this.createWorkspaceButton.click({ timeout: 60000 });
|
|
600
|
+
await this.workspaceWizardNameInput.fill(name, { timeout: 60000 });
|
|
601
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
602
|
+
await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
|
|
603
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
604
|
+
// When the org has environments enabled, this only advances the wizard
|
|
605
|
+
// into an "Assign environments" step instead of submitting. Assign the
|
|
606
|
+
// suite's default cluster here rather than submitting with an empty
|
|
607
|
+
// selection -- otherwise this shared workspace starts with no
|
|
608
|
+
// environments and every downstream spec that deploys to it needs its
|
|
609
|
+
// own manual assignment first. Each card's accessible name is
|
|
610
|
+
// "Select <cluster name>" before it is picked (camunda-hub's own
|
|
611
|
+
// create-workspace-environments.spec.tsx asserts the same pattern).
|
|
612
|
+
// Sampled immediately (isVisible() doesn't wait) since the step change
|
|
613
|
+
// is a synchronous client-side re-render.
|
|
614
|
+
if (await this.workspaceWizardEnvironmentsHeading
|
|
615
|
+
.isVisible()
|
|
616
|
+
.catch(() => false)) {
|
|
617
|
+
// "Create Default Cluster" runs in parallel with this flow
|
|
618
|
+
// (test-setup.spec.ts's mode: 'parallel'), so the cluster this picker
|
|
619
|
+
// lists may still be provisioning -- wait for it a generous 90s rather
|
|
620
|
+
// than sampling once. If it genuinely never appears (the cluster
|
|
621
|
+
// failed rather than just being slow), don't fail the whole shared
|
|
622
|
+
// workspace's setup over it: Hub's own recovery copy for this step
|
|
623
|
+
// ("Could not load environments. You can finish creating the
|
|
624
|
+
// workspace and assign them later.") confirms submitting unassigned
|
|
625
|
+
// is a supported, recoverable path, not a broken one.
|
|
626
|
+
const clusterOption = this.page.getByRole('button', {
|
|
627
|
+
name: new RegExp(`^Select ${this.defaultClusterName}`),
|
|
628
|
+
});
|
|
629
|
+
if (await this.becameVisible(clusterOption, 90000)) {
|
|
630
|
+
await clusterOption.click({ timeout: 10000 });
|
|
631
|
+
}
|
|
632
|
+
else {
|
|
633
|
+
console.warn(`"${this.defaultClusterName}" was not selectable on the Environments step; creating the workspace without it.`);
|
|
634
|
+
}
|
|
635
|
+
await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
|
|
636
|
+
}
|
|
637
|
+
await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
|
|
638
|
+
timeout: 120000,
|
|
639
|
+
});
|
|
640
|
+
}
|
|
471
641
|
async createCrossComponentProjectFolder() {
|
|
472
642
|
await this.dismissOverlays();
|
|
473
643
|
// The project list can take well over a minute to render after a session
|
|
@@ -483,6 +653,10 @@ class ModelerHomePage {
|
|
|
483
653
|
await this.clickCrossComponentProjectFolder();
|
|
484
654
|
return;
|
|
485
655
|
}
|
|
656
|
+
if (await this.becameVisible(this.createWorkspaceButton, 5000)) {
|
|
657
|
+
await this.createWorkspaceViaWizard(this.defaultFolderName);
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
486
660
|
await this.clickCreateNewProjectButton();
|
|
487
661
|
await this.enterNewProjectName(this.defaultFolderName);
|
|
488
662
|
}
|
|
@@ -48,21 +48,36 @@ class ProcessApplicationPage {
|
|
|
48
48
|
// .first() defensively only: hub renders exactly one of these today, in the
|
|
49
49
|
// workspace-root empty state. A future second placement should not turn
|
|
50
50
|
// every PA-creating test into a strict-mode violation.
|
|
51
|
+
//
|
|
52
|
+
// camunda-hub#25831 renamed the concept from "process application" to
|
|
53
|
+
// "project" (workspace root's CreateSplitButton renders literal text
|
|
54
|
+
// "Create project", opening a dialog titled "Create project"). Accept
|
|
55
|
+
// both names so this resolves whichever the org's live rollout has
|
|
56
|
+
// reached.
|
|
51
57
|
this.createProcessApplicationButton = page
|
|
52
58
|
.getByRole('button', { name: 'Create process application' })
|
|
59
|
+
.or(page.getByRole('button', { name: 'Create project' }))
|
|
53
60
|
.first();
|
|
54
61
|
this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
|
|
55
|
-
this.creationDialog = page
|
|
56
|
-
name: 'Create a process application'
|
|
57
|
-
|
|
62
|
+
this.creationDialog = page
|
|
63
|
+
.getByRole('dialog', { name: 'Create a process application' })
|
|
64
|
+
.or(page.getByRole('dialog', { name: 'Create project' }));
|
|
58
65
|
// Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
|
|
59
66
|
// getByLabel('Name', {exact: true}) for the properties panel, which would
|
|
60
67
|
// resolve to two elements while the creation modal is open.
|
|
61
|
-
|
|
62
|
-
this
|
|
63
|
-
|
|
68
|
+
// CreateProjectModal.tsx labels its field "Project name", not "Name" --
|
|
69
|
+
// accept both so this resolves under either dialog variant.
|
|
70
|
+
this.nameInput = this.creationDialog
|
|
71
|
+
.getByLabel('Name', { exact: true })
|
|
72
|
+
.or(this.creationDialog.getByLabel('Project name', { exact: true }));
|
|
73
|
+
// CreateProjectModal.tsx's submit button reads "Create project", not the
|
|
74
|
+
// exact "Create" the legacy dialog used.
|
|
75
|
+
this.submitButton = this.creationDialog
|
|
76
|
+
.getByRole('button', { name: 'Create', exact: true })
|
|
77
|
+
.or(this.creationDialog.getByRole('button', {
|
|
78
|
+
name: 'Create project',
|
|
64
79
|
exact: true,
|
|
65
|
-
});
|
|
80
|
+
}));
|
|
66
81
|
this.cancelButton = this.creationDialog.getByRole('button', {
|
|
67
82
|
name: 'Cancel',
|
|
68
83
|
exact: true,
|
|
@@ -72,8 +87,13 @@ class ProcessApplicationPage {
|
|
|
72
87
|
// The process application page renders more than one diagram-dropdown
|
|
73
88
|
// (hub's own unit tests reach for it with getAllByTestId), so the dropdown
|
|
74
89
|
// has to be resolved inside the folder explorer rather than page-wide.
|
|
90
|
+
// camunda-hub#25831's workspace-projects flow (WorkspaceProjectDetail.tsx)
|
|
91
|
+
// renders this control as CreateActionDs with test-id
|
|
92
|
+
// "create-action-dropdown" instead of the legacy "diagram-dropdown" --
|
|
93
|
+
// accept both.
|
|
75
94
|
this.createNewDropdown = this.folderExplorer
|
|
76
95
|
.locator('[data-test="diagram-dropdown"]')
|
|
96
|
+
.or(this.folderExplorer.locator('[data-test="create-action-dropdown"]'))
|
|
77
97
|
.first();
|
|
78
98
|
this.inlineNameInput = page.locator('[data-test="editable-input"]');
|
|
79
99
|
this.defineStagesDialog = page
|
|
@@ -7,11 +7,34 @@ const HomePage_1 = require("./HomePage");
|
|
|
7
7
|
const sleep_1 = require("../../utils/sleep");
|
|
8
8
|
const fileUpload_1 = require("../../utils/fileUpload");
|
|
9
9
|
const mailSlurpClient_1 = require("../../utils/mailSlurpClient");
|
|
10
|
+
const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
|
|
10
11
|
// Tracks which worker-derived `timeout` values have already paid the
|
|
11
12
|
// pre-flight stagger below, so a worker only pays it once per process
|
|
12
13
|
// lifetime rather than on every call -- see the comment at the call sites.
|
|
13
14
|
const staggeredWorkerTimeouts = new Set();
|
|
15
|
+
const onboardingTourSuppressed = new WeakSet();
|
|
16
|
+
// Mirrors camunda-hub@452978f: pre-seed the onboarding tour's "seen" localStorage flag
|
|
17
|
+
// (SEEN_KEY_PREFIX in .../tour-dialog/use-onboarding-seen.ts) so it never auto-opens. The
|
|
18
|
+
// "Welcome to Camunda Hub" tour mounts app-wide -- Console included, not just Modeler -- and
|
|
19
|
+
// hides the post-login banner link from the accessibility tree until dismissed, which times
|
|
20
|
+
// out assertTestUsesCorrectOrganization's banner check. addInitScript runs before any page
|
|
21
|
+
// script on every navigation/reload, so there's no login-response race to lose; the flag is
|
|
22
|
+
// matched by prefix since the user id isn't known ahead of login.
|
|
23
|
+
async function suppressOnboardingTour(page) {
|
|
24
|
+
if (onboardingTourSuppressed.has(page)) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
onboardingTourSuppressed.add(page);
|
|
28
|
+
await page.addInitScript((prefixPattern) => {
|
|
29
|
+
const pattern = new RegExp(prefixPattern);
|
|
30
|
+
const originalGetItem = Storage.prototype.getItem;
|
|
31
|
+
Storage.prototype.getItem = function (key) {
|
|
32
|
+
return pattern.test(key) ? 'true' : originalGetItem.call(this, key);
|
|
33
|
+
};
|
|
34
|
+
}, '^hub\\.onboarding\\..*\\.seen\\.');
|
|
35
|
+
}
|
|
14
36
|
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5, skipOrgAssertion = false) {
|
|
37
|
+
await suppressOnboardingTour(page);
|
|
15
38
|
// Callers derive `timeout` as (workerIndex + 1) * 1000, so it's a stable
|
|
16
39
|
// per-worker identifier: a Playwright worker is a long-lived process that
|
|
17
40
|
// calls loginWithRetry once per test/repeat it runs, and this stagger only
|
|
@@ -32,6 +55,11 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
|
|
|
32
55
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
33
56
|
try {
|
|
34
57
|
await page.context().clearCookies();
|
|
58
|
+
// The Hub bearer-token cache in consoleApiHelpers.ts is keyed by Page
|
|
59
|
+
// for the tab's whole lifetime; without clearing it here alongside
|
|
60
|
+
// cookies/storage, a flow that logs a different user into the same
|
|
61
|
+
// page could have getUserToken() return the PREVIOUS user's token.
|
|
62
|
+
(0, consoleApiHelpers_1.resetHubBearerToken)(page);
|
|
35
63
|
await page.goto('about:blank');
|
|
36
64
|
await page
|
|
37
65
|
.evaluate(() => {
|
|
@@ -429,8 +457,15 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
429
457
|
const homePage = new HomePage_1.HomePage(page);
|
|
430
458
|
if (page.url().includes('?code=') || page.url().includes('&state=')) {
|
|
431
459
|
console.log('OAuth callback detected, waiting for redirect...');
|
|
460
|
+
// Camunda Hub (which replaced Console on SaaS) only exchanges the code
|
|
461
|
+
// once its SPA has booted, and on CI runners that boot alone was measured
|
|
462
|
+
// at ~45s past /login-callback -- a 30s wait aborted the exchange on every
|
|
463
|
+
// attempt. The code is single-use, so falling back to page.goto('/') on
|
|
464
|
+
// timeout (as this used to) doesn't recover a slow-but-valid exchange, it
|
|
465
|
+
// just turns it into a guaranteed failure. Wait longer instead, matching
|
|
466
|
+
// the sibling 8.10 fix.
|
|
432
467
|
await page.waitForURL((url) => !url.toString().includes('?code='), {
|
|
433
|
-
timeout:
|
|
468
|
+
timeout: 120000,
|
|
434
469
|
});
|
|
435
470
|
await (0, sleep_1.sleep)(5000);
|
|
436
471
|
}
|
|
@@ -442,6 +477,24 @@ async function assertTestUsesCorrectOrganization(page) {
|
|
|
442
477
|
else {
|
|
443
478
|
await (0, test_1.expect)(homePage.consoleBanner).toBeVisible({ timeout: 30000 });
|
|
444
479
|
}
|
|
480
|
+
// Camunda Hub replaced Console + Web Modeler on SaaS: it lands on
|
|
481
|
+
// /workspaces (no org id in the URL) and has no "Open Organizations"
|
|
482
|
+
// button. Its sidebar links are org-scoped, so assert on those instead and
|
|
483
|
+
// switch via the same /org/<id> route Hub's org breadcrumb dropdown uses.
|
|
484
|
+
const isShown = (locator, timeout) => locator
|
|
485
|
+
.waitFor({ state: 'visible', timeout })
|
|
486
|
+
.then(() => true)
|
|
487
|
+
.catch(() => false);
|
|
488
|
+
if (await isShown(homePage.hubSidebar, 10000)) {
|
|
489
|
+
const orgScopedLink = homePage.hubSidebar
|
|
490
|
+
.locator(`a[href*="/org/${process.env.ORG_ID}/"]`)
|
|
491
|
+
.first();
|
|
492
|
+
if (!(await isShown(orgScopedLink, 30000))) {
|
|
493
|
+
await page.goto(`/org/${process.env.ORG_ID}`);
|
|
494
|
+
}
|
|
495
|
+
await (0, test_1.expect)(orgScopedLink).toBeVisible({ timeout: 60000 });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
445
498
|
await (0, test_1.expect)(homePage.openOrganizationButton).toBeVisible({ timeout: 180000 });
|
|
446
499
|
try {
|
|
447
500
|
await (0, test_1.expect)(page.url()).toContain(process.env.ORG_ID);
|
|
@@ -12,7 +12,8 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
|
|
|
12
12
|
await (0, _setup_1.captureScreenshot)(page, testInfo);
|
|
13
13
|
await (0, _setup_1.captureFailureVideo)(page, testInfo);
|
|
14
14
|
});
|
|
15
|
-
|
|
15
|
+
// Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
|
|
16
|
+
_8_7_1.test.skip('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
|
|
16
17
|
_8_7_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
|
|
17
18
|
_8_7_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
|
|
18
19
|
await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
|
|
@@ -60,10 +61,14 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
|
|
|
60
61
|
await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
|
|
61
62
|
});
|
|
62
63
|
await _8_7_1.test.step('Navigate to Web Modeler', async () => {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
|
|
65
|
+
// Modeler home directly and has no app switcher to open.
|
|
66
|
+
if (!new URL(page.url()).hostname.startsWith('hub.')) {
|
|
67
|
+
await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
|
|
68
|
+
timeout: 120000,
|
|
69
|
+
});
|
|
70
|
+
await appsPage.clickCamundaApps();
|
|
71
|
+
}
|
|
67
72
|
await appsPage.clickModeler();
|
|
68
73
|
await (0, test_1.expect)(modelerHomePage.modelerPageBanner).toBeVisible({
|
|
69
74
|
timeout: 180000,
|
|
@@ -71,6 +76,14 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
|
|
|
71
76
|
});
|
|
72
77
|
await _8_7_1.test.step('Create Cross Component Project Folder', async () => {
|
|
73
78
|
await homePage.clickSkipCustomization();
|
|
79
|
+
// Do not attempt a "only User 1 creates, everyone else waits" fix for
|
|
80
|
+
// the parallel race here: the 8.10 sibling already tried exactly this
|
|
81
|
+
// and reverted it (see tests/8.10/test-setup.spec.ts) after finding
|
|
82
|
+
// the created workspace never became visible to the other users'
|
|
83
|
+
// sessions at all -- not a wait-budget problem, so no amount of
|
|
84
|
+
// retrying fixes it. Every user independently checking-and-creating
|
|
85
|
+
// still races occasionally, but that is the smaller, known failure
|
|
86
|
+
// mode; see camunda/c8-cross-component-e2e-tests#3649.
|
|
74
87
|
await modelerHomePage.createCrossComponentProjectFolder();
|
|
75
88
|
await modelerHomePage.clickHomeBreadcrumb();
|
|
76
89
|
await modelerHomePage.assertCrossComponentProjectFolderVisible();
|
|
@@ -134,13 +134,15 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
134
134
|
});
|
|
135
135
|
await (0, sleep_1.sleep)(30000);
|
|
136
136
|
await appsPage.clickCamundaApps();
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
137
|
+
// clickCamundaApps() is a no-op on Hub (no global app-switcher panel to
|
|
138
|
+
// open -- see its own comment), so modelerLink has nothing to assert
|
|
139
|
+
// there: Hub's authenticated shell already proves Modeler access by
|
|
140
|
+
// having been reached at all. consoleLink's accessible name ("Console")
|
|
141
|
+
// matches Hub's persistent sidebar link unchanged, so that assertion
|
|
142
|
+
// still applies on both shells.
|
|
140
143
|
await (0, test_1.expect)(appsPage.consoleLink).toBeVisible();
|
|
141
|
-
await (
|
|
142
|
-
await (
|
|
143
|
-
await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
|
|
144
|
+
await appsPage.assertOperatePresent(clusterName, false);
|
|
145
|
+
await appsPage.assertTasklistPresent(clusterName, false);
|
|
144
146
|
await appsPage.clickModeler();
|
|
145
147
|
await homePage.clickSkipCustomization();
|
|
146
148
|
await modelerHomePage.createCrossComponentProjectFolder();
|
|
@@ -206,10 +208,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
206
208
|
await (0, test_1.expect)(appsPage.consoleLink).toBeVisible({
|
|
207
209
|
timeout: 30000,
|
|
208
210
|
});
|
|
209
|
-
await appsPage.assertOperatePresent();
|
|
210
|
-
await appsPage.assertTasklistPresent();
|
|
211
|
-
await (
|
|
212
|
-
await appsPage.assertOptimizePresent(false);
|
|
211
|
+
await appsPage.assertOperatePresent(clusterName);
|
|
212
|
+
await appsPage.assertTasklistPresent(clusterName);
|
|
213
|
+
await appsPage.assertOptimizePresent(clusterName, false);
|
|
213
214
|
await appsPage.clickModeler();
|
|
214
215
|
await modelerHomePage.clickCrossComponentProjectFolder();
|
|
215
216
|
await modelerHomePage.clickProcessDiagram(processName);
|
|
@@ -266,10 +267,9 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
266
267
|
await (0, test_1.expect)(appsPage.consoleLink).toBeVisible({
|
|
267
268
|
timeout: 20000,
|
|
268
269
|
});
|
|
269
|
-
await appsPage.assertOptimizePresent();
|
|
270
|
-
await appsPage.assertOperatePresent();
|
|
271
|
-
await appsPage.assertTasklistPresent();
|
|
272
|
-
await (0, test_1.expect)(appsPage.modelerLink).toBeVisible();
|
|
270
|
+
await appsPage.assertOptimizePresent(clusterName);
|
|
271
|
+
await appsPage.assertOperatePresent(clusterName);
|
|
272
|
+
await appsPage.assertTasklistPresent(clusterName);
|
|
273
273
|
await appsPage.clickModeler();
|
|
274
274
|
await modelerHomePage.clickCrossComponentProjectFolder();
|
|
275
275
|
await modelerHomePage.clickProcessDiagram(processName);
|