@camunda/e2e-test-suite 0.0.1249 → 0.0.1251
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/pages/BrowseAllConnectorMarketplacePage.d.ts +17 -0
- package/dist/pages/BrowseAllConnectorMarketplacePage.js +145 -0
- package/dist/pages/SM-8.10/ClusterDetailsPage.d.ts +1 -1
- package/dist/pages/SM-8.10/ClusterDetailsPage.js +13 -6
- package/dist/pages/SM-8.10/ClusterPage.js +3 -1
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.d.ts +2 -0
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.js +16 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +4 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.js +78 -51
- package/dist/pages/SM-8.10/ModelerHomePage.d.ts +2 -1
- package/dist/pages/SM-8.10/ModelerHomePage.js +24 -0
- package/dist/pages/SM-8.10/OperateHomePage.js +8 -1
- package/dist/pages/SM-8.10/OptimizeHomePage.js +13 -4
- package/dist/pages/SM-8.10/ProcessApplicationPage.d.ts +10 -5
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +163 -68
- package/dist/pages/SM-8.10/UtilitiesPage.js +6 -6
- package/dist/pages/SM-8.7/NavigationPage.js +2 -1
- package/dist/tests/8.10/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.10/smoke-tests.spec.js +2 -1
- package/dist/tests/8.7/connectors-user-flows.spec.js +8 -4
- package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.7/smoke-tests.spec.js +2 -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/dist/tests/8.8/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.8/smoke-tests.spec.js +2 -1
- package/dist/tests/8.9/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.9/smoke-tests.spec.js +2 -1
- package/dist/tests/SM-8.10/cluster-management-user-flows.spec.js +13 -6
- package/dist/tests/SM-8.10/connectors-user-flows.spec.js +2 -1
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +3 -1
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.js +62 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +6 -3
- package/dist/utils/modelerCluster.d.ts +4 -6
- package/dist/utils/modelerCluster.js +55 -32
- package/package.json +1 -1
- package/playwright.config.ts +18 -5
|
@@ -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
|
});
|