@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.
@@ -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. Its header banner (homePage.consoleBanner) is
505
- // already visible from any Hub page, so there is no navigation action
506
- // needed here -- mirrors the same hostname guard in clickModeler().
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
- // Locate all elements with class .cds--toggle__text
232
- const toggleTextElements = await this.page.$$('.cds--toggle__text');
233
- // Check if toggleTextElements has at least 2 elements
234
- if (toggleTextElements.length >= 2) {
235
- // Get the text content of the second toggle text element
236
- const toggleText = await toggleTextElements[1].textContent();
237
- // Check if the text content is 'Disabled'
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
- // Locate all elements with class .cds--toggle__text
267
- const toggleTextElements = await this.page.$$('.cds--toggle__text');
268
- // Check if toggleTextElements has at least 2 elements
269
- if (toggleTextElements.length >= 2) {
270
- // Get the text content of the second toggle text element
271
- const toggleText = await toggleTextElements[1].textContent();
272
- // Check if the text content is 'Enabled'
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
- // This runs straight after a navigation back to the diagram, so the
613
- // modeler still has to boot and render its toolbar. The trace shows the
614
- // button first resolving ~35s after that navigation, which the previous
615
- // flat 30s click budget expired just short of. Wait for it to render
616
- // before spending the click budget on it.
617
- await (0, test_1.expect)(this.deployAndRunMainButton).toBeVisible({ timeout: 120000 });
618
- await this.deployAndRunMainButton.click({ timeout: 30000 });
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
- await (0, sleep_1.sleep)(1000);
1960
- const context = this.page.context();
1961
- await context.grantPermissions(['clipboard-read']);
1962
- const clipboardText = await this.page.evaluate(async () => {
1963
- return await navigator.clipboard.readText();
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
- this.completeJobButton = this.diagram.getByLabel('Complete job');
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
- this.openSettingsButton = page.getByLabel('Open Settings');
11
- this.logoutButton = page.getByRole('button', { name: 'Log out' });
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's accessible name ("Console")
139
- // matches Hub's persistent sidebar link unchanged, so that assertion
140
- // still applies on both shells.
141
- await (0, test_1.expect)(appsPage.consoleLink).toBeVisible();
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1274",
3
+ "version": "0.0.1275",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",