@camunda/e2e-test-suite 0.0.1274 → 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/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/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
|
|
@@ -27,9 +27,13 @@ _8_7_1.test.describe('RBA Enabled User Flows Test', () => {
|
|
|
27
27
|
'RBA On User Flow - Permission for One Process': mainUser,
|
|
28
28
|
'RBA On User Flow - Permission for All Processes': allProcessesUser,
|
|
29
29
|
};
|
|
30
|
-
_8_7_1.test.beforeEach(async ({ page, homePage, clusterPage, loginPage }, testInfo) => {
|
|
30
|
+
_8_7_1.test.beforeEach(async ({ page, homePage, clusterPage, loginPage, appsPage }, testInfo) => {
|
|
31
31
|
const currentUser = usersByTitle[testInfo.title] ?? mainUser;
|
|
32
32
|
await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, currentUser, (testInfo.workerIndex + 1) * 1000);
|
|
33
|
+
// Whatever route login lands on may be sidebarMode 'workspace' (no
|
|
34
|
+
// Environments/Clusters item at all) rather than 'default' -- the same
|
|
35
|
+
// gap clickConsoleLink() now closes by navigating to '/workspaces'.
|
|
36
|
+
await appsPage.clickConsoleLink();
|
|
33
37
|
await (0, test_1.expect)(homePage.clusterTab).toBeVisible({ timeout: 120000 });
|
|
34
38
|
await homePage.clickClusters();
|
|
35
39
|
(0, test_1.expect)(clusterNames[testInfo.title]).toBeTruthy();
|
|
@@ -135,10 +135,13 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
135
135
|
// clickCamundaApps() is a no-op on Hub (no global app-switcher panel to
|
|
136
136
|
// open -- see its own comment), so modelerLink has nothing to assert
|
|
137
137
|
// there: Hub's authenticated shell already proves Modeler access by
|
|
138
|
-
// having been reached at all. consoleLink
|
|
139
|
-
//
|
|
140
|
-
//
|
|
141
|
-
|
|
138
|
+
// having been reached at all. consoleLink used to be unconditional on
|
|
139
|
+
// Hub, but camunda-hub commit cb1f9eb ("restructure SaaS Console
|
|
140
|
+
// navigation") gates it to Owner/Admin/DevOps org roles once the
|
|
141
|
+
// environmentsEnabled flag is on -- Analyst is deliberately excluded
|
|
142
|
+
// (organization-store.saas.ts CLUSTER_OPERATOR_ROLE_IDS), so the
|
|
143
|
+
// just-downgraded-to-Analyst user here should no longer see it either.
|
|
144
|
+
await (0, test_1.expect)(appsPage.consoleLink).not.toBeVisible();
|
|
142
145
|
await appsPage.assertOperatePresent(clusterName, false);
|
|
143
146
|
await appsPage.assertTasklistPresent(clusterName, false);
|
|
144
147
|
await appsPage.clickModeler();
|