@camunda/e2e-test-suite 0.0.1273 → 0.0.1275
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.js +11 -3
- package/dist/pages/8.7/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.7/ClusterDetailsPage.js +22 -64
- package/dist/pages/8.7/ModelerCreatePage.js +56 -14
- package/dist/pages/8.7/PlayPage.js +12 -1
- package/dist/pages/8.7/SettingsPage.js +15 -2
- package/dist/pages/8.7/UtilitiesPage.js +22 -0
- package/dist/pages/8.8/AppsPage.d.ts +2 -0
- package/dist/pages/8.8/AppsPage.js +122 -0
- package/dist/pages/8.8/ClusterDetailsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterDetailsPage.js +29 -10
- package/dist/pages/8.8/ClusterPage.js +35 -2
- package/dist/pages/8.8/ClusterSecretsPage.d.ts +1 -0
- package/dist/pages/8.8/ClusterSecretsPage.js +62 -2
- package/dist/pages/8.8/FormJsPage.js +55 -24
- package/dist/pages/8.8/HomePage.d.ts +3 -0
- package/dist/pages/8.8/HomePage.js +62 -7
- package/dist/pages/8.8/LoginPage.js +32 -1
- package/dist/pages/8.8/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/8.8/ModelerCreatePage.js +78 -16
- package/dist/pages/8.8/ModelerHomePage.d.ts +10 -0
- package/dist/pages/8.8/ModelerHomePage.js +339 -14
- package/dist/pages/8.8/UtilitiesPage.js +47 -2
- package/dist/tests/8.7/rba-enabled-user-flows.spec.js +5 -1
- package/dist/tests/8.7/web-modeler-user-flows.spec.js +7 -4
- package/dist/tests/8.8/test-setup.spec.js +29 -4
- package/dist/utils/consoleApiHelpers.d.ts +5 -0
- package/dist/utils/consoleApiHelpers.js +177 -2
- package/dist/utils/hubDeployDialog.d.ts +3 -0
- package/dist/utils/hubDeployDialog.js +74 -0
- package/dist/utils/hubResourceRename.d.ts +4 -0
- package/dist/utils/hubResourceRename.js +74 -0
- package/package.json +1 -1
|
@@ -501,10 +501,18 @@ class AppsPage {
|
|
|
501
501
|
// Camunda Hub folded Console into the same persistent shell as
|
|
502
502
|
// Modeler/home: Console's Dashboard/Clusters/Organization links are just
|
|
503
503
|
// another section of the same "Main navigation" sidebar, not a separate
|
|
504
|
-
// app to switch into
|
|
505
|
-
//
|
|
506
|
-
//
|
|
504
|
+
// app to switch into -- but ONLY on routes the Hub sidebar registry marks
|
|
505
|
+
// sidebarMode 'default' ('/', '/org/:id', '/workspaces', '/environments',
|
|
506
|
+
// '/console/*'). Any '/workspaces/:wsId/...' route is sidebarMode
|
|
507
|
+
// 'workspace', whose sidebar drops the Environments/Clusters item
|
|
508
|
+
// entirely (camunda-hub features/navigation/registry.ts,
|
|
509
|
+
// use-ds-sidebar-items.tsx) -- so a caller landing there and finding
|
|
510
|
+
// "already on hub." would previously no-op onto a page that can never
|
|
511
|
+
// show homePage.clusterTab. '/workspaces' is unconditionally 'default',
|
|
512
|
+
// so navigate there directly rather than assuming the current route
|
|
513
|
+
// already has it.
|
|
507
514
|
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
515
|
+
await this.page.goto('/workspaces', { waitUntil: 'domcontentloaded' });
|
|
508
516
|
return;
|
|
509
517
|
}
|
|
510
518
|
const maxRetries = 3;
|
|
@@ -22,6 +22,7 @@ declare class ClusterDetailsPage {
|
|
|
22
22
|
readonly clientCredentialsDialog: Locator;
|
|
23
23
|
readonly clustersLink: Locator;
|
|
24
24
|
readonly userTaskRestriction: Locator;
|
|
25
|
+
readonly rbaToggle: Locator;
|
|
25
26
|
readonly enableRBADialog: Locator;
|
|
26
27
|
readonly disableRBADialog: Locator;
|
|
27
28
|
readonly rbaEnabledMessage: Locator;
|
|
@@ -28,6 +28,7 @@ class ClusterDetailsPage {
|
|
|
28
28
|
clientCredentialsDialog;
|
|
29
29
|
clustersLink;
|
|
30
30
|
userTaskRestriction;
|
|
31
|
+
rbaToggle;
|
|
31
32
|
enableRBADialog;
|
|
32
33
|
disableRBADialog;
|
|
33
34
|
rbaEnabledMessage;
|
|
@@ -160,6 +161,13 @@ class ClusterDetailsPage {
|
|
|
160
161
|
this.rbaHeading = this.page.getByRole('heading', {
|
|
161
162
|
name: 'Resource-based authorizations',
|
|
162
163
|
});
|
|
164
|
+
// A nightly trace screenshot confirmed this toggle rendered as a small
|
|
165
|
+
// pill switch, not a Carbon .cds--toggle -- the class scraping this used
|
|
166
|
+
// to rely on matched 0 elements. No DOM snapshot was available to read
|
|
167
|
+
// the exact markup (page.$$ calls aren't snapshotted), but
|
|
168
|
+
// userTaskRestriction above -- on this same Settings page, still passing
|
|
169
|
+
// -- proves this page's toggle buttons carry aria-label="Switch <Label>".
|
|
170
|
+
this.rbaToggle = page.locator('button[aria-label="Switch Resource-based authorizations"]');
|
|
163
171
|
this.clientCredentialsLink = (clientCredentials) => page
|
|
164
172
|
.getByRole('cell', { name: clientCredentials })
|
|
165
173
|
.getByText(clientCredentials);
|
|
@@ -228,73 +236,23 @@ class ClusterDetailsPage {
|
|
|
228
236
|
}
|
|
229
237
|
async enableRBA() {
|
|
230
238
|
await (0, test_1.expect)(this.rbaHeading).toBeVisible({ timeout: 60000 });
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
if (toggleText === 'Disabled') {
|
|
239
|
-
// Locate all elements with class .cds--toggle__switch
|
|
240
|
-
const toggleSwitchElements = await this.page.$$('.cds--toggle__switch');
|
|
241
|
-
// Check if toggleSwitchElements has at least 2 elements
|
|
242
|
-
if (toggleSwitchElements.length >= 2) {
|
|
243
|
-
// Click the second toggle switch element
|
|
244
|
-
await toggleSwitchElements[1].click();
|
|
245
|
-
console.log('Second toggle switch clicked.');
|
|
246
|
-
await (0, test_1.expect)(this.enableRBADialog).toBeVisible({ timeout: 60000 });
|
|
247
|
-
await this.rbaEnableButton.click();
|
|
248
|
-
await (0, test_1.expect)(this.page.getByText('Enabling...')).not.toBeVisible({
|
|
249
|
-
timeout: 60000,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
else {
|
|
253
|
-
console.log('Second toggle switch not found or less than 2.');
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
else {
|
|
257
|
-
console.log("Second toggle text does not contain 'Disabled'.");
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
else {
|
|
261
|
-
console.log('Toggle text elements not found or less than 2.');
|
|
262
|
-
}
|
|
239
|
+
await (0, test_1.expect)(this.rbaToggle).toBeVisible({ timeout: 30000 });
|
|
240
|
+
await this.rbaToggle.click();
|
|
241
|
+
await (0, test_1.expect)(this.enableRBADialog).toBeVisible({ timeout: 60000 });
|
|
242
|
+
await this.rbaEnableButton.click();
|
|
243
|
+
await (0, test_1.expect)(this.page.getByText('Enabling...')).not.toBeVisible({
|
|
244
|
+
timeout: 60000,
|
|
245
|
+
});
|
|
263
246
|
}
|
|
264
247
|
async disableRBA() {
|
|
265
248
|
await (0, test_1.expect)(this.rbaHeading).toBeVisible({ timeout: 60000 });
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
if (toggleText === 'Enabled') {
|
|
274
|
-
// Locate all elements with class .cds--toggle__switch
|
|
275
|
-
const toggleSwitchElements = await this.page.$$('.cds--toggle__switch');
|
|
276
|
-
// Check if toggleSwitchElements has at least 2 elements
|
|
277
|
-
if (toggleSwitchElements.length >= 2) {
|
|
278
|
-
// Click the second toggle switch element
|
|
279
|
-
await toggleSwitchElements[1].click();
|
|
280
|
-
console.log('Second toggle switch clicked.');
|
|
281
|
-
await (0, test_1.expect)(this.disableRBADialog).toBeVisible({ timeout: 60000 });
|
|
282
|
-
await this.rbaDisableButton.click();
|
|
283
|
-
await (0, test_1.expect)(this.page.getByText('Disabling...')).not.toBeVisible({
|
|
284
|
-
timeout: 60000,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
287
|
-
else {
|
|
288
|
-
console.log('Second toggle switch not found or less than 2.');
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
else {
|
|
292
|
-
console.log("Second toggle text does not contain 'Enabled'.");
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
else {
|
|
296
|
-
console.log('Toggle text elements not found or less than 2.');
|
|
297
|
-
}
|
|
249
|
+
await (0, test_1.expect)(this.rbaToggle).toBeVisible({ timeout: 30000 });
|
|
250
|
+
await this.rbaToggle.click();
|
|
251
|
+
await (0, test_1.expect)(this.disableRBADialog).toBeVisible({ timeout: 60000 });
|
|
252
|
+
await this.rbaDisableButton.click();
|
|
253
|
+
await (0, test_1.expect)(this.page.getByText('Disabling...')).not.toBeVisible({
|
|
254
|
+
timeout: 60000,
|
|
255
|
+
});
|
|
298
256
|
}
|
|
299
257
|
async searchAndClickClientCredentialsLink(name) {
|
|
300
258
|
await (0, test_1.expect)(this.clientsList.first()).toBeVisible({ timeout: 60000 });
|
|
@@ -6,6 +6,7 @@ const sleep_1 = require("../../utils/sleep");
|
|
|
6
6
|
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
7
7
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
8
8
|
const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
9
|
+
const sessionRecovery_1 = require("../../utils/sessionRecovery");
|
|
9
10
|
class ModelerCreatePage {
|
|
10
11
|
page;
|
|
11
12
|
processApplicationPage;
|
|
@@ -364,9 +365,16 @@ class ModelerCreatePage {
|
|
|
364
365
|
this.copyPublicLinkButton = page.getByRole('button', {
|
|
365
366
|
name: 'Copy to clipboard',
|
|
366
367
|
});
|
|
368
|
+
// Legacy Carbon toast vs. the design-system's Sonner-based toast
|
|
369
|
+
// (data-sonner-toast, confirmed from a nightly DOM snapshot) -- same
|
|
370
|
+
// migration already handled for the deploy/instance-started toasts
|
|
371
|
+
// elsewhere in this file and in UtilitiesPage.ts's runProcess.
|
|
367
372
|
this.formLinkedSuccessMessage = page
|
|
368
373
|
.locator('.cds--toast-notification__subtitle')
|
|
369
|
-
.getByText('Form has been successfully linked.')
|
|
374
|
+
.getByText('Form has been successfully linked.')
|
|
375
|
+
.or(page
|
|
376
|
+
.locator('[data-sonner-toast]')
|
|
377
|
+
.getByText('Form has been successfully linked.'));
|
|
370
378
|
this.diagramNameBreadcrumb = (diagramName) => page.locator('button[data-test="breadcrumb-diagram"]', {
|
|
371
379
|
hasText: diagramName,
|
|
372
380
|
});
|
|
@@ -609,13 +617,32 @@ class ModelerCreatePage {
|
|
|
609
617
|
}
|
|
610
618
|
}
|
|
611
619
|
async clickDeployAndRunMainButton() {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
620
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
621
|
+
try {
|
|
622
|
+
// This runs straight after a navigation back to the diagram, so the
|
|
623
|
+
// modeler still has to boot and render its toolbar. The trace shows
|
|
624
|
+
// the button first resolving ~35s after that navigation, which the
|
|
625
|
+
// previous flat 30s click budget expired just short of. Wait for it
|
|
626
|
+
// to render before spending the click budget on it.
|
|
627
|
+
await (0, test_1.expect)(this.deployAndRunMainButton).toBeVisible({
|
|
628
|
+
timeout: 120000,
|
|
629
|
+
});
|
|
630
|
+
await this.deployAndRunMainButton.click({ timeout: 30000 });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
// Hub's own /login route can hang mid-wait when its session POST
|
|
635
|
+
// 5xx's, leaving the diagram (and this button) never rendering --
|
|
636
|
+
// the same session loss utils/sessionRecovery.ts already recovers
|
|
637
|
+
// from elsewhere. One retry after recovering; any other cause
|
|
638
|
+
// rethrows unchanged.
|
|
639
|
+
const recovered = await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
|
|
640
|
+
const restored = await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page);
|
|
641
|
+
if ((!recovered && !restored) || attempt === 1) {
|
|
642
|
+
throw error;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
}
|
|
619
646
|
}
|
|
620
647
|
async clickDeployAndRunSubButton() {
|
|
621
648
|
await this.deployAndRunSubButton.click({ timeout: 30000 });
|
|
@@ -1953,16 +1980,31 @@ class ModelerCreatePage {
|
|
|
1953
1980
|
await this.copyPublicLinkButton.click({ timeout: 60000 });
|
|
1954
1981
|
}
|
|
1955
1982
|
async getPublicationLink() {
|
|
1983
|
+
// 'clipboard-write' must be granted BEFORE the click that copies the
|
|
1984
|
+
// link -- the nightly trace showed the read always resolving empty
|
|
1985
|
+
// (both attempts), with the click itself never observed to fail. The
|
|
1986
|
+
// previous code only granted 'clipboard-read', after the click, which
|
|
1987
|
+
// does nothing for the write the click triggers.
|
|
1988
|
+
const context = this.page.context();
|
|
1989
|
+
await context.grantPermissions(['clipboard-read', 'clipboard-write']);
|
|
1956
1990
|
await this.clickPublicationTab();
|
|
1957
1991
|
await (0, test_1.expect)(this.copyPublicLinkButton).toBeVisible({ timeout: 60000 });
|
|
1958
1992
|
await this.clickCopyPublicLinkButton();
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1993
|
+
// Poll instead of one blind read after a fixed sleep -- the write is
|
|
1994
|
+
// async on the page's side and has no visible completion signal to wait
|
|
1995
|
+
// on directly.
|
|
1996
|
+
let clipboardText = '';
|
|
1997
|
+
for (let attempt = 0; attempt < 10 && !clipboardText; attempt++) {
|
|
1998
|
+
await (0, sleep_1.sleep)(500);
|
|
1999
|
+
clipboardText = await this.page.evaluate(async () => {
|
|
2000
|
+
return await navigator.clipboard.readText();
|
|
2001
|
+
});
|
|
2002
|
+
}
|
|
1965
2003
|
console.log('Captured public link:', clipboardText);
|
|
2004
|
+
if (!clipboardText) {
|
|
2005
|
+
throw new Error('getPublicationLink: clipboard read empty after clicking Copy to ' +
|
|
2006
|
+
'clipboard -- the copy never reached the clipboard.');
|
|
2007
|
+
}
|
|
1966
2008
|
return clipboardText;
|
|
1967
2009
|
}
|
|
1968
2010
|
async assertFormEmbedded() {
|
|
@@ -29,7 +29,18 @@ class PlayPage {
|
|
|
29
29
|
.or(page
|
|
30
30
|
.getByRole('tablist', { name: 'List of modes' })
|
|
31
31
|
.getByRole('tab', { name: 'Test', exact: true }));
|
|
32
|
-
|
|
32
|
+
// Not getByLabel(): a DevTools capture during camunda-hub#29604's
|
|
33
|
+
// investigation showed this button rendering with NO aria-label
|
|
34
|
+
// attribute at all, yet still carrying a
|
|
35
|
+
// `<span class="sr-only">Complete job</span>` that gives it a perfectly
|
|
36
|
+
// valid accessible name from content. getByLabel() only resolves via
|
|
37
|
+
// aria-label/aria-labelledby/<label> association, so it can't see that
|
|
38
|
+
// span -- getByRole's name option does the full ARIA accessible-name
|
|
39
|
+
// computation (including visually-hidden content), so it matches this
|
|
40
|
+
// button regardless of whether the aria-label attribute itself is set.
|
|
41
|
+
this.completeJobButton = this.diagram.getByRole('button', {
|
|
42
|
+
name: 'Complete job',
|
|
43
|
+
});
|
|
33
44
|
// Newer Modeler Play UI: the "Configure Scenario" floating card hosts a
|
|
34
45
|
// primary "Start" Carbon button (variants: "Start", "Start with
|
|
35
46
|
// variables", "Start with a form"). Source:
|
|
@@ -7,8 +7,21 @@ class SettingsPage {
|
|
|
7
7
|
logoutButton;
|
|
8
8
|
constructor(page) {
|
|
9
9
|
this.page = page;
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// DS shell renders a `User menu, <full name>` button; the legacy Carbon
|
|
11
|
+
// shell renders useCamundaTools' `Open Settings` button. Both still ship.
|
|
12
|
+
this.openSettingsButton = page
|
|
13
|
+
.getByLabel('Open Settings')
|
|
14
|
+
.or(page.getByRole('button', { name: /^User menu,/ }))
|
|
15
|
+
.first();
|
|
16
|
+
// DS shell's user-menu dropdown renders items via Radix, which sets
|
|
17
|
+
// role="menuitem" (confirmed in camunda-hub's nav-shell-ds.spec.saas.tsx);
|
|
18
|
+
// the legacy Carbon panel renders a plain role="button". .first() avoids
|
|
19
|
+
// a strict-mode violation if both shells' triggers are ever present at
|
|
20
|
+
// once (SM-8.10's SettingsPage uses the same guard for this union).
|
|
21
|
+
this.logoutButton = page
|
|
22
|
+
.getByRole('button', { name: 'Log out' })
|
|
23
|
+
.or(page.getByRole('menuitem', { name: 'Log out' }))
|
|
24
|
+
.first();
|
|
12
25
|
}
|
|
13
26
|
async clickOpenSettingsButton(retries = 3) {
|
|
14
27
|
for (let i = 0; i < retries; i++) {
|
|
@@ -34,6 +34,28 @@ async function suppressOnboardingTour(page) {
|
|
|
34
34
|
}, '^hub\\.onboarding\\..*\\.seen\\.');
|
|
35
35
|
}
|
|
36
36
|
async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5, skipOrgAssertion = false) {
|
|
37
|
+
// Fail fast on a testUser missing credentials instead of burning the full
|
|
38
|
+
// exponential-backoff retry budget (~8-13 min across 5 attempts)
|
|
39
|
+
// resubmitting a login Auth0 rejects with "Password is required" every
|
|
40
|
+
// time, then surfacing the generic "Login failed after N attempts" with no
|
|
41
|
+
// mention of which credential was empty. test-setup.spec.ts's per-user
|
|
42
|
+
// loop and requireTestUser() already guard this for specs that reach a
|
|
43
|
+
// numbered credential slot directly -- callers that pass testUsers.mainUser
|
|
44
|
+
// or another named user straight through (e.g. "Create AWS Cluster") had no
|
|
45
|
+
// such guard, and hit exactly this failure when C8_USERNAME/C8_PASSWORD
|
|
46
|
+
// (the org owner) came back unset.
|
|
47
|
+
if (!testUser.username || !testUser.password) {
|
|
48
|
+
const missing = [
|
|
49
|
+
testUser.username ? null : 'username',
|
|
50
|
+
testUser.password ? null : 'password',
|
|
51
|
+
]
|
|
52
|
+
.filter(Boolean)
|
|
53
|
+
.join(' and ');
|
|
54
|
+
throw new Error(`Cannot log in as ${testUser.username || '(unknown user)'}: ${missing} ` +
|
|
55
|
+
'not configured -- this SaaS org was created without that ' +
|
|
56
|
+
"user's credentials. See scripts/flakiness-saas-create-org.sh, " +
|
|
57
|
+
'which sizes the org from the slots each spec names.');
|
|
58
|
+
}
|
|
37
59
|
await suppressOnboardingTour(page);
|
|
38
60
|
// Callers derive `timeout` as (workerIndex + 1) * 1000, so it's a stable
|
|
39
61
|
// per-worker identifier: a Playwright worker is a long-lived process that
|
|
@@ -18,8 +18,10 @@ declare class AppsPage {
|
|
|
18
18
|
readonly consoleLink: Locator;
|
|
19
19
|
readonly camundaComponentsButton: Locator;
|
|
20
20
|
readonly camundaAppsButton: Locator;
|
|
21
|
+
readonly hubSidebar: Locator;
|
|
21
22
|
constructor(page: Page);
|
|
22
23
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
24
|
+
private clickHubClusterApp;
|
|
23
25
|
clickModeler(): Promise<void>;
|
|
24
26
|
/**
|
|
25
27
|
* Waits for the Modeler app to actually render, and recovers the one landing
|
|
@@ -23,6 +23,7 @@ class AppsPage {
|
|
|
23
23
|
consoleLink;
|
|
24
24
|
camundaComponentsButton;
|
|
25
25
|
camundaAppsButton;
|
|
26
|
+
hubSidebar;
|
|
26
27
|
constructor(page) {
|
|
27
28
|
this.page = page;
|
|
28
29
|
this.modelerLink = page.getByRole('link', { name: 'Modeler', exact: true });
|
|
@@ -34,6 +35,11 @@ class AppsPage {
|
|
|
34
35
|
this.consoleLink = page.getByRole('link', { name: 'Console', exact: true });
|
|
35
36
|
this.camundaComponentsButton = page.getByLabel('Camunda components');
|
|
36
37
|
this.camundaAppsButton = page.getByLabel('Camunda apps');
|
|
38
|
+
// Camunda Hub's persistent "Main navigation" sidebar lists each cluster
|
|
39
|
+
// with its own apps (Operate/Tasklist/Optimize/Identity) as child links
|
|
40
|
+
// -- there is no per-app switcher panel to open on Hub (see
|
|
41
|
+
// clickCamundaApps below). Ported from pages/8.9/AppsPage.ts.
|
|
42
|
+
this.hubSidebar = page.getByRole('navigation', { name: 'Main navigation' });
|
|
37
43
|
this.tasklistListItem = this.page
|
|
38
44
|
.getByRole('listitem')
|
|
39
45
|
.filter({ hasText: 'Tasklist' });
|
|
@@ -67,7 +73,83 @@ class AppsPage {
|
|
|
67
73
|
await (0, test_1.expect)(cluster).toBeVisible({ timeout: 60000 });
|
|
68
74
|
await cluster.click();
|
|
69
75
|
}
|
|
76
|
+
// Camunda Hub's sidebar groups by cluster, not by app: each cluster row
|
|
77
|
+
// expands to reveal its own per-app child links (Operate/Tasklist/...).
|
|
78
|
+
// camunda-hub's own nav-shell-ds.spec.saas.tsx documents the shape: the
|
|
79
|
+
// design system splits the group row into a label control and a chevron
|
|
80
|
+
// control, both labelled by the cluster name; either one opens it -- and
|
|
81
|
+
// the child links do not exist in the DOM at all until expanded (not just
|
|
82
|
+
// hidden), and are not nested under any container matchable by cluster
|
|
83
|
+
// name once expanded. So: find the toggle by the cluster's own name
|
|
84
|
+
// (`.first()` because both controls share that name), click it, then look
|
|
85
|
+
// up the app link sidebar-wide rather than scoped to a "group" container
|
|
86
|
+
// that doesn't reliably exist. Ported from pages/8.9/AppsPage.ts, where
|
|
87
|
+
// this shape is already nightly-proven.
|
|
88
|
+
async clickHubClusterApp(clusterName, appName) {
|
|
89
|
+
// When the sidebar is collapsed (the default on a fresh session), the
|
|
90
|
+
// group toggle opens a Radix popover -- a role="dialog" labelled with the
|
|
91
|
+
// cluster name and portalled OUTSIDE the "Main navigation" <nav> -- so a
|
|
92
|
+
// sidebar-scoped lookup finds nothing there. Look in that popover first;
|
|
93
|
+
// keep the sidebar-scoped lookup for the expanded-sidebar layout, where
|
|
94
|
+
// the links render inline. Not exact: the rendered accessible name
|
|
95
|
+
// carries more than the bare app name (e.g. "Operate in <cluster>
|
|
96
|
+
// (opens in a new tab)").
|
|
97
|
+
const appLink = this.page
|
|
98
|
+
.getByRole('dialog', { name: clusterName, exact: true })
|
|
99
|
+
.getByRole('link', { name: appName })
|
|
100
|
+
.or(this.hubSidebar.getByRole('link', { name: appName }))
|
|
101
|
+
.first();
|
|
102
|
+
// Decide whether to expand from clusterName's OWN toggle state, not from
|
|
103
|
+
// whether "some" appLink is already visible sidebar-wide: in a
|
|
104
|
+
// multi-cluster org another cluster's group can already be expanded with
|
|
105
|
+
// the same app name, and the click would then fire on the wrong
|
|
106
|
+
// cluster's link.
|
|
107
|
+
const collapsedToggle = this.hubSidebar
|
|
108
|
+
.getByRole('button', { name: clusterName, expanded: false })
|
|
109
|
+
.first();
|
|
110
|
+
if (await collapsedToggle.isVisible({ timeout: 5000 }).catch(() => false)) {
|
|
111
|
+
await collapsedToggle.click({ timeout: 10000 });
|
|
112
|
+
}
|
|
113
|
+
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
114
|
+
// Hub's cluster-app links carry target="_blank", so clicking them opens
|
|
115
|
+
// the app in a new tab while every caller keeps asserting on this.page.
|
|
116
|
+
// Follow the href in the current tab instead, matching the same-tab
|
|
117
|
+
// navigation the Console app switcher gave these callers.
|
|
118
|
+
const href = await appLink.getAttribute('href');
|
|
119
|
+
if (href) {
|
|
120
|
+
await this.page.goto(new URL(href, this.page.url()).toString());
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
await appLink.click({ timeout: 10000 });
|
|
124
|
+
}
|
|
70
125
|
async clickModeler() {
|
|
126
|
+
// Camunda Hub (hub.*) replaced Console + Web Modeler on SaaS and has no
|
|
127
|
+
// app switcher. Its home is the Modeler/Workspaces home, but a hostname
|
|
128
|
+
// check alone can't tell whether we're already there -- Hub's cluster
|
|
129
|
+
// pages are on the same hub.* hostname, and a no-op here would leave a
|
|
130
|
+
// caller that reaches clickModeler() from a cluster/API-client page
|
|
131
|
+
// stranded with the next locator running against the wrong page.
|
|
132
|
+
// Navigate home through the header logo link instead, same as
|
|
133
|
+
// ModelerHomePage.clickHomeBreadcrumb -- clicking it when already on the
|
|
134
|
+
// Modeler home is a harmless in-SPA no-op.
|
|
135
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
136
|
+
const header = this.page.locator('header[data-slot="app-header"]');
|
|
137
|
+
// The href stays literally "/login" until the client's auth state
|
|
138
|
+
// catches up just after login (camunda-hub NavShellDs.common.tsx) --
|
|
139
|
+
// clicking it in that window would log the test out instead of
|
|
140
|
+
// navigating home, so exclude that href and wait for the authenticated
|
|
141
|
+
// one rather than racing it.
|
|
142
|
+
const homeLink = header
|
|
143
|
+
.getByRole('link', { name: 'Camunda Modeler home' })
|
|
144
|
+
.and(this.page.locator(':not([href$="/login"])'))
|
|
145
|
+
.or(header
|
|
146
|
+
.getByRole('link', { name: 'Camunda Hub home' })
|
|
147
|
+
.and(this.page.locator(':not([href$="/login"])')))
|
|
148
|
+
.first();
|
|
149
|
+
await (0, test_1.expect)(homeLink).toBeVisible({ timeout: 30000 });
|
|
150
|
+
await homeLink.click({ timeout: 30000 });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
71
153
|
const maxRetries = 5;
|
|
72
154
|
// If the click is already done and we are sitting on Modeler, return
|
|
73
155
|
// immediately — observed in failing runs where the banner role lookup
|
|
@@ -148,6 +230,10 @@ class AppsPage {
|
|
|
148
230
|
}
|
|
149
231
|
}
|
|
150
232
|
async clickTasklist(clusterName) {
|
|
233
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
234
|
+
await this.clickHubClusterApp(clusterName, 'Tasklist');
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
151
237
|
const maxRetries = 5;
|
|
152
238
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
153
239
|
try {
|
|
@@ -185,6 +271,14 @@ class AppsPage {
|
|
|
185
271
|
throw new Error(`Failed to click the tasklist link after ${maxRetries} attempts.`);
|
|
186
272
|
}
|
|
187
273
|
async clickIdentity(clusterName) {
|
|
274
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
275
|
+
// Non-anchored, matching the substring lookup clickHubClusterApp uses
|
|
276
|
+
// for Operate/Tasklist/Optimize -- an anchored pattern fails the same
|
|
277
|
+
// way an exact-string match did for those apps if the rendered link's
|
|
278
|
+
// accessible name carries any extra content (icon/badge).
|
|
279
|
+
await this.clickHubClusterApp(clusterName, /Admin|Identity/);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
188
282
|
const maxRetries = 5;
|
|
189
283
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
190
284
|
try {
|
|
@@ -251,6 +345,10 @@ class AppsPage {
|
|
|
251
345
|
await this.clickCluster(this.optimizeListItem, clusterName);
|
|
252
346
|
}
|
|
253
347
|
async clickOperate(clusterName) {
|
|
348
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
349
|
+
await this.clickHubClusterApp(clusterName, 'Operate');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
254
352
|
const maxRetries = 3;
|
|
255
353
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
256
354
|
try {
|
|
@@ -287,6 +385,10 @@ class AppsPage {
|
|
|
287
385
|
throw new Error(`Failed to click the operate link after ${maxRetries} attempts.`);
|
|
288
386
|
}
|
|
289
387
|
async clickOptimize(clusterName) {
|
|
388
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
389
|
+
await this.clickHubClusterApp(clusterName, 'Optimize');
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
290
392
|
const maxRetries = 3;
|
|
291
393
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
292
394
|
try {
|
|
@@ -373,6 +475,13 @@ class AppsPage {
|
|
|
373
475
|
}
|
|
374
476
|
}
|
|
375
477
|
async clickCamundaApps(maxRetries = 3) {
|
|
478
|
+
// Camunda Hub's rewritten shells have no app-switcher button to open --
|
|
479
|
+
// Modeler is already Hub's home area and the other apps are reached
|
|
480
|
+
// per-cluster via the persistent sidebar, not through a global switcher
|
|
481
|
+
// panel. Mirrors the same hostname guard in clickModeler() above.
|
|
482
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
376
485
|
await (0, sleep_1.sleep)(2000);
|
|
377
486
|
const appButtons = [
|
|
378
487
|
this.camundaComponentsButton,
|
|
@@ -420,6 +529,19 @@ class AppsPage {
|
|
|
420
529
|
.catch(() => { });
|
|
421
530
|
}
|
|
422
531
|
async clickConsoleLink() {
|
|
532
|
+
// Camunda Hub replaced Console on SaaS: there is no "Console" entry in a
|
|
533
|
+
// switcher, and the org-scoped Console area is the sidebar's
|
|
534
|
+
// Clusters/Environments section. Going there is what callers assert on
|
|
535
|
+
// (HomePage.consoleBanner resolves to Hub's authenticated header).
|
|
536
|
+
if (new URL(this.page.url()).hostname.startsWith('hub.')) {
|
|
537
|
+
const consoleArea = this.hubSidebar
|
|
538
|
+
.locator('a[href$="/clusters"]')
|
|
539
|
+
.or(this.hubSidebar.locator('a[href$="/environments"]'))
|
|
540
|
+
.first();
|
|
541
|
+
await (0, test_1.expect)(consoleArea).toBeVisible({ timeout: 30000 });
|
|
542
|
+
await consoleArea.click({ timeout: 30000 });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
423
545
|
const maxRetries = 3;
|
|
424
546
|
for (let retries = 0; retries < maxRetries; retries++) {
|
|
425
547
|
try {
|
|
@@ -40,6 +40,7 @@ declare class ClusterDetailsPage {
|
|
|
40
40
|
readonly orchestrationClusterCheckbox: Locator;
|
|
41
41
|
readonly clientCredentialsLink: (clientCredentials: string) => Locator;
|
|
42
42
|
readonly clientRow: (name: string) => Locator;
|
|
43
|
+
readonly clientRowByDom: (name: string) => Locator;
|
|
43
44
|
readonly clientRowDeleteButton: (name: string) => Locator;
|
|
44
45
|
readonly reviewUpdateButton: Locator;
|
|
45
46
|
readonly selectTargetVersionDialog: Locator;
|
|
@@ -46,6 +46,7 @@ class ClusterDetailsPage {
|
|
|
46
46
|
orchestrationClusterCheckbox;
|
|
47
47
|
clientCredentialsLink;
|
|
48
48
|
clientRow;
|
|
49
|
+
clientRowByDom;
|
|
49
50
|
clientRowDeleteButton;
|
|
50
51
|
reviewUpdateButton;
|
|
51
52
|
selectTargetVersionDialog;
|
|
@@ -78,9 +79,6 @@ class ClusterDetailsPage {
|
|
|
78
79
|
.locator('label')
|
|
79
80
|
.filter({ hasText: /^Administration API - Resource: Secrets$/ });
|
|
80
81
|
this.createButton = page.getByRole('button', { name: 'Create', exact: true });
|
|
81
|
-
this.closeModalButton = page
|
|
82
|
-
.getByRole('dialog', { name: 'Client credentials', exact: true })
|
|
83
|
-
.getByLabel('Close');
|
|
84
82
|
this.createClientButton = page.getByRole('button', {
|
|
85
83
|
name: 'Create new Client',
|
|
86
84
|
});
|
|
@@ -98,9 +96,18 @@ class ClusterDetailsPage {
|
|
|
98
96
|
this.createClientCredentialsDialog = page.getByRole('dialog', {
|
|
99
97
|
name: 'Create new client credentials',
|
|
100
98
|
});
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
99
|
+
// Use a content-based locator so the test stays robust if the dialog
|
|
100
|
+
// title is ever renamed in the Console UI.
|
|
101
|
+
this.clientCredentialsDialog = page.getByRole('dialog').filter({
|
|
102
|
+
has: page.getByText('The Client Secret will not be shown again.'),
|
|
103
|
+
});
|
|
104
|
+
// Hub's client-credentials dialog (design-system Dialog, not the old
|
|
105
|
+
// Carbon one) exposes its close affix as an accessible button named
|
|
106
|
+
// "Close" rather than via an aria-label picked up by getByLabel -- see
|
|
107
|
+
// camunda-hub's own e2e suite, ConsoleClusterDetailsPage.ts
|
|
108
|
+
// (clientCredentialsDialogCloseButton), which already uses this pattern.
|
|
109
|
+
this.closeModalButton = this.clientCredentialsDialog.getByRole('button', {
|
|
110
|
+
name: 'Close',
|
|
104
111
|
});
|
|
105
112
|
this.clustersLink = page
|
|
106
113
|
.getByRole('banner')
|
|
@@ -154,10 +161,22 @@ class ClusterDetailsPage {
|
|
|
154
161
|
this.orchestrationClusterCheckbox = page
|
|
155
162
|
.locator('label')
|
|
156
163
|
.filter({ hasText: /^Orchestration Cluster API$/ });
|
|
164
|
+
// Camunda Hub's data table renders the client name as a link-styled
|
|
165
|
+
// <button data-slot="button"> rather than an <a>. The cell also holds a
|
|
166
|
+
// pointer-events-none row-overlay button with the same accessible name,
|
|
167
|
+
// so match the visible one by its data-slot.
|
|
157
168
|
this.clientCredentialsLink = (clientCredentials) => page
|
|
158
169
|
.getByRole('cell', { name: clientCredentials })
|
|
159
|
-
.
|
|
170
|
+
.locator('a, button[data-slot="button"]')
|
|
171
|
+
.first();
|
|
160
172
|
this.clientRow = (name) => this.clientsList.filter({ hasText: name });
|
|
173
|
+
// Same row, matched through the DOM instead of the accessibility tree.
|
|
174
|
+
// Hub's client-credentials dialog is a Radix dialog: while it is open the
|
|
175
|
+
// whole app shell behind it carries aria-hidden/inert, so `getByRole`
|
|
176
|
+
// resolves nothing there and the post-create scope check below can never
|
|
177
|
+
// pass. `locator('tr')` is unaffected. The client name is unique per run,
|
|
178
|
+
// so matching page-wide cannot pick up another table's row.
|
|
179
|
+
this.clientRowByDom = (name) => page.locator('tr').filter({ hasText: name });
|
|
161
180
|
this.clientRowDeleteButton = (name) => this.clientRow(name).getByRole('button', { name: 'Delete' });
|
|
162
181
|
this.reviewUpdateButton = page.getByRole('button', { name: 'Review Update' });
|
|
163
182
|
this.selectTargetVersionDialog = page
|
|
@@ -609,9 +628,9 @@ class ClusterDetailsPage {
|
|
|
609
628
|
}
|
|
610
629
|
await (0, test_1.expect)(this.clientCredentialsDialog.getByText('The Client Secret will not be shown again.')).toBeVisible({ timeout: 30000 });
|
|
611
630
|
// The clients table behind the dialog re-fetches after the create, and
|
|
612
|
-
// renders a skeleton (no
|
|
613
|
-
//
|
|
614
|
-
await (0, test_1.expect)(this.
|
|
631
|
+
// renders a skeleton (no rows at all) until that GET resolves — so the
|
|
632
|
+
// default 5s expect timeout can expire before the new row exists.
|
|
633
|
+
await (0, test_1.expect)(this.clientRowByDom(name)).toContainText(/(?=.*Orchestration)(?=.*Optimize)(?=.*Secrets)/, { timeout: 60000 });
|
|
615
634
|
}
|
|
616
635
|
async clickEnvVarsButton() {
|
|
617
636
|
await (0, test_1.expect)(this.envVarsTab).toBeVisible({ timeout: 40000 });
|