@camunda/e2e-test-suite 0.0.1253 → 0.0.1255

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.
@@ -4,6 +4,7 @@ exports.ConsoleOrganizationPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
7
+ const sessionRecovery_1 = require("../../utils/sessionRecovery");
7
8
  class ConsoleOrganizationPage {
8
9
  page;
9
10
  usersTab;
@@ -137,10 +138,26 @@ class ConsoleOrganizationPage {
137
138
  name: 'assign',
138
139
  })
139
140
  .last();
140
- this.assignMembersSearchbox = page.getByRole('searchbox', {
141
+ // The "Assign members" dialog now renders through cmdk (camunda-hub's
142
+ // Command component: data-slot="command"/"command-input") instead of the
143
+ // legacy Carbon combobox. The input keeps the same placeholder text, but
144
+ // its role is "combobox", not "searchbox" -- and the wrapping
145
+ // <label cmdk-label> that aria-labelledby points at is rendered empty, so
146
+ // the accessible-name lookup itself is unreliable; match by placeholder
147
+ // instead, which cmdk always sets from the same string. Nightly trace:
148
+ // getByRole('searchbox', {name: '...'}) found nothing and the click
149
+ // timed out even though the input was on screen.
150
+ this.assignMembersSearchbox = page
151
+ .getByPlaceholder('Search by full name or email address')
152
+ .or(page.getByRole('searchbox', {
141
153
  name: 'search by full name or email address',
142
- });
143
- this.assignMembersSearchResult = page.locator('.cds--list-box__menu-item');
154
+ }))
155
+ .first();
156
+ // cmdk's CommandItem renders role="option" (cmdk-item); keep the legacy
157
+ // Carbon class as a fallback.
158
+ this.assignMembersSearchResult = page
159
+ .getByRole('option')
160
+ .or(page.locator('.cds--list-box__menu-item'));
144
161
  this.organizationManagementLink = page.getByRole('link', {
145
162
  name: 'organization management',
146
163
  });
@@ -148,9 +165,15 @@ class ConsoleOrganizationPage {
148
165
  this.deleteSubButton = page
149
166
  .getByRole('dialog')
150
167
  .getByRole('button', { name: 'delete' });
151
- this.filterTableSearchbox = page.getByRole('searchbox', {
152
- name: 'Filter table',
153
- });
168
+ // Hub's org-management "Users" tab (hub.ultrawombat.com/org/.../
169
+ // management/users) renders its own table filter as a design-system
170
+ // input labelled plainly "Search", not "Filter table" -- confirmed from
171
+ // the nightly trace's DOM snapshot (an <input type="search"
172
+ // aria-label="Search"> directly inside the "Users" tab panel). Keep the
173
+ // legacy name as a fallback.
174
+ this.filterTableSearchbox = page
175
+ .getByRole('searchbox', { name: 'Search', exact: true })
176
+ .or(page.getByRole('searchbox', { name: 'Filter table' }));
154
177
  }
155
178
  async clickCreateAuthorizedResourceButton() {
156
179
  await (0, test_1.expect)(this.createButton).toBeVisible({ timeout: 60000 });
@@ -433,6 +456,14 @@ class ConsoleOrganizationPage {
433
456
  async clickGroupsTab() {
434
457
  for (let attempt = 0; attempt < 3; attempt++) {
435
458
  try {
459
+ // Hub can drop the SPA session mid-test and bounce this tab to
460
+ // /login-callback then its own hung /login?returnUrl=... instead of
461
+ // rendering Organization management -- the same session loss
462
+ // utils/sessionRecovery.ts already recovers from elsewhere. Recover it
463
+ // before clicking, or every retry below just reloads the dead login
464
+ // page and re-fails the same way.
465
+ await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
466
+ await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page);
436
467
  await this.groupsTab.click({ timeout: 60000 });
437
468
  return;
438
469
  }
@@ -122,11 +122,21 @@ class FormJsPage {
122
122
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
123
123
  }
124
124
  // Hub's environment-based dialog also renders a <label> per row, which
125
- // the legacy toggle detection below would mistake for its own.
125
+ // the legacy toggle detection below would mistake for its own. A bare
126
+ // isVisible() here does not wait -- it can race the dialog's own content
127
+ // rendering (this.dialog itself is visible a beat before its rows are)
128
+ // and report false even though this IS the environment-based dialog,
129
+ // falling through to the legacy branch below and failing on a
130
+ // [title="<cluster>"] locator that dialog never had. Give it a real
131
+ // window to render before deciding.
126
132
  const environmentRow = this.dialog
127
133
  .locator('[data-test^="environment-row-"]')
128
134
  .first();
129
- if (await environmentRow.isVisible().catch(() => false)) {
135
+ const hasEnvironmentRow = await environmentRow
136
+ .waitFor({ state: 'visible', timeout: 10000 })
137
+ .then(() => true)
138
+ .catch(() => false);
139
+ if (hasEnvironmentRow) {
130
140
  try {
131
141
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
132
142
  }
@@ -163,10 +173,12 @@ class FormJsPage {
163
173
  // Hub's environment-based dialog lists every Environment assigned to
164
174
  // the workspace and preselects the first, which need not be this one.
165
175
  const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
166
- if (await environmentRows
176
+ const hasEnvironmentRows = await environmentRows
167
177
  .first()
168
- .isVisible()
169
- .catch(() => false)) {
178
+ .waitFor({ state: 'visible', timeout: 10000 })
179
+ .then(() => true)
180
+ .catch(() => false);
181
+ if (hasEnvironmentRows) {
170
182
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
171
183
  }
172
184
  return;
@@ -175,10 +187,16 @@ class FormJsPage {
175
187
  const cluster = this.dialog.locator(`[title="${clusterName}"]`);
176
188
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
177
189
  await cluster.click();
178
- await (0, test_1.expect)(this.dialog
190
+ // ClustersListDs.tsx (camunda-hub) replaced the legacy Carbon toggle's
191
+ // role="switch" with a Radix RadioGroupItem (role="radio") -- the
192
+ // wrapping <label> is unchanged, but a role="switch" lookup now finds
193
+ // nothing. Accept either role.
194
+ const selectedClusterControl = this.dialog
179
195
  .locator('label')
180
- .filter({ hasText: new RegExp(`^${clusterName}`) })
181
- .getByRole('switch')).toBeChecked({ timeout: 30000 });
196
+ .filter({ hasText: new RegExp(`^${clusterName}`) });
197
+ await (0, test_1.expect)(selectedClusterControl
198
+ .getByRole('switch')
199
+ .or(selectedClusterControl.getByRole('radio'))).toBeChecked({ timeout: 30000 });
182
200
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
183
201
  timeout: healthCheckTimeout,
184
202
  });
@@ -208,8 +226,17 @@ class FormJsPage {
208
226
  return;
209
227
  }
210
228
  await scopeSelect.click();
211
- await this.dialog
229
+ // camunda-hub's DeployScopeDs.tsx renders this combobox through the
230
+ // design system's Select, whose SelectContent mounts inside a Radix
231
+ // Portal (see @camunda/c4-components select.tsx) -- the option list
232
+ // renders as a direct child of <body>, OUTSIDE the dialog's own DOM
233
+ // subtree. A dialog-scoped lookup for the option therefore times out
234
+ // even though the click above opened it. Look page-wide first; keep the
235
+ // dialog-scoped lookup for the legacy Carbon Dropdown (DeployScope.tsx),
236
+ // which renders its menu inline.
237
+ await this.page
212
238
  .getByRole('option', { name: 'Only this resource' })
239
+ .or(this.dialog.getByRole('option', { name: 'Only this resource' }))
213
240
  .or(this.dialog.getByText('Only this resource', { exact: true }))
214
241
  .first()
215
242
  .click({ timeout: 30000 });
@@ -4,6 +4,7 @@ exports.LoginPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const UtilitiesPage_1 = require("./UtilitiesPage");
6
6
  const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
7
+ const sessionRecovery_1 = require("../../utils/sessionRecovery");
7
8
  class LoginPage {
8
9
  page;
9
10
  usernameInput;
@@ -53,6 +54,7 @@ class LoginPage {
53
54
  }
54
55
  async login(credentials = {}) {
55
56
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
57
+ (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
56
58
  // See loginWithTestUser below: this re-authenticates the same page and
57
59
  // must clear the Hub bearer-token cache too, or getUserToken() can
58
60
  // return whatever user was previously logged in on it.
@@ -68,6 +70,7 @@ class LoginPage {
68
70
  }
69
71
  async loginWithTestUser(credentials = {}) {
70
72
  const { username = process.env.C8_USERNAME_TEST, password = process.env.C8_PASSWORD_TEST, } = credentials;
73
+ (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
71
74
  // See loginWithRetry in UtilitiesPage.ts: the Hub bearer-token cache in
72
75
  // consoleApiHelpers.ts is keyed by Page for the tab's whole lifetime, so
73
76
  // this re-authentication (a different user than whatever was logged in
@@ -123,6 +126,7 @@ class LoginPage {
123
126
  }
124
127
  async loginWithoutOrgAssertion(credentials = {}) {
125
128
  const { username = process.env.C8_USERNAME, password = process.env.C8_PASSWORD, } = credentials;
129
+ (0, sessionRecovery_1.trackSignedInUser)(this.page, username);
126
130
  // See loginWithTestUser above: this re-authenticates the same page and
127
131
  // must clear the Hub bearer-token cache too, or getUserToken() can
128
132
  // return whatever user was previously logged in on it.
@@ -477,14 +477,19 @@ class ModelerCreatePage {
477
477
  await this.userTaskOption.click();
478
478
  }
479
479
  async clickForm(name) {
480
+ // The "Link a form" picker renders each entry as a button keyed by the
481
+ // form's resource id (data-test="link-target-file-<uuid>"), not by name --
482
+ // `item-${name}` never matches any element the picker renders. The name is
483
+ // only exposed as this accessible name, so match on that instead.
484
+ //
480
485
  // Baseline upgrade runs use fixed form names, so a Playwright retry re-creates
481
486
  // a form with the same name and the picker then lists two identical entries.
482
487
  // Scope to the first matching item so the link step stays deterministic on retry
483
488
  // instead of failing with a strict-mode violation.
484
489
  await this.page
485
- .locator(`[data-test="item-${name}"]`)
490
+ .locator('[data-test="link-target-list"]')
491
+ .getByRole('button', { name: `Select ${name}` })
486
492
  .first()
487
- .getByText(name)
488
493
  .click({ timeout: 90000 });
489
494
  }
490
495
  async clickUserTask(id) {
@@ -525,20 +530,49 @@ class ModelerCreatePage {
525
530
  await (0, test_1.expect)(this.deployDiagramDialog).toBeVisible({
526
531
  timeout: 30000,
527
532
  });
528
- //Select the correct cluster if multiple exist
529
- const clusterLabel = this.deployDiagramDialog
530
- .locator('label')
531
- .filter({ hasText: new RegExp(`^${clusterName}`) });
532
- await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
533
- await clusterLabel.click();
534
- await (0, test_1.expect)(clusterLabel.getByRole('switch')).toBeChecked({
535
- timeout: 30000,
536
- });
537
- //Check healthy status of the selected cluster
538
- const regex = new RegExp(`${clusterName}.*?HealthydevManage`);
539
- await (0, test_1.expect)(this.deployDiagramDialog).toHaveText(regex, {
540
- timeout: 120000,
541
- });
533
+ // Hub's environment-based deploy dialog (camunda-hub#29210) lists the
534
+ // Environments assigned to the workspace as a role="radiogroup" under
535
+ // #environment-selection-heading, one row per
536
+ // [data-test="environment-row-*"] -- confirmed from a nightly trace's DOM
537
+ // snapshot: the radio button and its <label> are SIBLINGS inside the row
538
+ // (the label is no longer a wrapping parent), so a label-scoped
539
+ // getByRole('switch'|'radio') lookup finds nothing, and the row carries
540
+ // no "Manage" link at all, so the old concatenated-text health regex
541
+ // could never match either. Prefer this dialog; fall back to the legacy
542
+ // per-deploy cluster toggle list otherwise.
543
+ // A bare isVisible() does not wait -- it can race the dialog's own
544
+ // content rendering (the dialog itself becomes visible a beat before its
545
+ // rows do) and report false even though this IS the environment-based
546
+ // dialog, falling through to the legacy branch below and failing on a
547
+ // locator that dialog never had. Give it a real window to render before
548
+ // deciding.
549
+ const environmentRows = this.deployDiagramDialog.locator('[data-test^="environment-row-"]');
550
+ const hasEnvironmentRows = await environmentRows
551
+ .first()
552
+ .waitFor({ state: 'visible', timeout: 10000 })
553
+ .then(() => true)
554
+ .catch(() => false);
555
+ if (hasEnvironmentRows) {
556
+ await (0, modelerCluster_1.selectDeployEnvironment)(this.deployDiagramDialog, clusterName);
557
+ const targetRow = environmentRows.filter({ hasText: clusterName }).first();
558
+ await (0, test_1.expect)(targetRow.getByText('Healthy', { exact: true })).toBeVisible({
559
+ timeout: 120000,
560
+ });
561
+ }
562
+ else {
563
+ //Select the correct cluster if multiple exist
564
+ const clusterLabel = this.deployDiagramDialog
565
+ .locator('label')
566
+ .filter({ hasText: new RegExp(`^${clusterName}`) });
567
+ await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
568
+ await clusterLabel.click();
569
+ await (0, test_1.expect)(clusterLabel.getByRole('switch').or(clusterLabel.getByRole('radio'))).toBeChecked({ timeout: 30000 });
570
+ //Check healthy status of the selected cluster
571
+ const regex = new RegExp(`${clusterName}.*?HealthydevManage`);
572
+ await (0, test_1.expect)(this.deployDiagramDialog).toHaveText(regex, {
573
+ timeout: 120000,
574
+ });
575
+ }
542
576
  await (0, test_1.expect)(this.deploySubButton).toBeVisible({
543
577
  timeout: 120000,
544
578
  });
@@ -1647,11 +1681,21 @@ class ModelerCreatePage {
1647
1681
  'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
1648
1682
  }
1649
1683
  // Hub's environment-based dialog also renders a <label> per row, which
1650
- // the legacy toggle detection below would mistake for its own.
1684
+ // the legacy toggle detection below would mistake for its own. A bare
1685
+ // isVisible() here does not wait -- it can race the dialog's own content
1686
+ // rendering (this.dialog itself is visible a beat before its rows are)
1687
+ // and report false even though this IS the environment-based dialog,
1688
+ // falling through to the legacy branch below and failing on a
1689
+ // [title="<cluster>"] locator that dialog never had. Give it a real
1690
+ // window to render before deciding.
1651
1691
  const environmentRow = this.dialog
1652
1692
  .locator('[data-test^="environment-row-"]')
1653
1693
  .first();
1654
- if (await environmentRow.isVisible().catch(() => false)) {
1694
+ const hasEnvironmentRow = await environmentRow
1695
+ .waitFor({ state: 'visible', timeout: 10000 })
1696
+ .then(() => true)
1697
+ .catch(() => false);
1698
+ if (hasEnvironmentRow) {
1655
1699
  try {
1656
1700
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
1657
1701
  }
@@ -1688,10 +1732,12 @@ class ModelerCreatePage {
1688
1732
  // Hub's environment-based dialog lists every Environment assigned to
1689
1733
  // the workspace and preselects the first, which need not be this one.
1690
1734
  const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
1691
- if (await environmentRows
1735
+ const hasEnvironmentRows = await environmentRows
1692
1736
  .first()
1693
- .isVisible()
1694
- .catch(() => false)) {
1737
+ .waitFor({ state: 'visible', timeout: 10000 })
1738
+ .then(() => true)
1739
+ .catch(() => false);
1740
+ if (hasEnvironmentRows) {
1695
1741
  await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
1696
1742
  }
1697
1743
  return;
@@ -1702,9 +1748,11 @@ class ModelerCreatePage {
1702
1748
  .filter({ hasText: new RegExp(`^${clusterName}`) });
1703
1749
  await (0, test_1.expect)(clusterLabel).toBeVisible({ timeout: 30000 });
1704
1750
  await clusterLabel.click();
1705
- await (0, test_1.expect)(clusterLabel.getByRole('switch')).toBeChecked({
1706
- timeout: 30000,
1707
- });
1751
+ // ClustersListDs.tsx (camunda-hub) replaced the legacy Carbon toggle's
1752
+ // role="switch" with a Radix RadioGroupItem (role="radio") -- the
1753
+ // wrapping <label> is unchanged, but a role="switch" lookup now finds
1754
+ // nothing. Accept either role.
1755
+ await (0, test_1.expect)(clusterLabel.getByRole('switch').or(clusterLabel.getByRole('radio'))).toBeChecked({ timeout: 30000 });
1708
1756
  await (0, test_1.expect)(this.dialog).toHaveText(healthyRegex, {
1709
1757
  timeout: healthCheckTimeout,
1710
1758
  });
@@ -18,7 +18,8 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
18
18
  await (0, _setup_1.captureScreenshot)(page, testInfo);
19
19
  await (0, _setup_1.captureFailureVideo)(page, testInfo);
20
20
  });
21
- (0, _8_10_1.test)('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
21
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
22
+ _8_10_1.test.skip('Enable Alpha Features', async ({ page, loginPage, consoleOrganizationsPage, homePage, }, testInfo) => {
22
23
  _8_10_1.test.skip(process.env.IS_PROD === 'true', 'Skipping test because not required on PROD test org');
23
24
  _8_10_1.test.skip(process.env.IS_AG === 'true', 'Skipping test because not required when IS_AG is true');
24
25
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, users_1.testUsers.mainUser, (testInfo.workerIndex + 1) * 1000);
@@ -165,7 +166,9 @@ _8_10_1.test.describe('Cluster Setup Tests', () => {
165
166
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, Math.min((testInfo.workerIndex + 1) * 5000, 60000));
166
167
  });
167
168
  await _8_10_1.test.step('Navigate to Web Modeler', async () => {
168
- await (0, test_1.expect)(homePage.camundaComponentsButton).toBeVisible({
169
+ // Camunda Hub replaced Console + Web Modeler on SaaS: it lands on the
170
+ // Modeler home directly and has no app switcher to wait for.
171
+ await (0, test_1.expect)(homePage.camundaComponentsButton.or(homePage.hubHeader)).toBeVisible({
169
172
  timeout: 120000,
170
173
  });
171
174
  await appsPage.openModelerWithRetry(modelerHomePage.modelerPageBanner, 180000, 3, undefined, () => modelerHomePage.dismissBlockingDialog());
@@ -58,6 +58,22 @@ _8_7_1.test.describe('Cluster Setup Tests', () => {
58
58
  for (const [index, user] of (0, users_1.getTestUsers)().entries()) {
59
59
  (0, _8_7_1.test)(`Create Project Folder for User ${index + 1}`, async ({ page, loginPage, homePage, appsPage, modelerHomePage, }, testInfo) => {
60
60
  await _8_7_1.test.step('Log in', async () => {
61
+ // getTestUsers() resolves straight from process.env.C8_USERNAME_TEST_N /
62
+ // C8_PASSWORD_TEST_N. When this SaaS org was created without one of
63
+ // those slots the password half comes back '', and loginWithRetry then
64
+ // spends its full backoff budget (~13 minutes across both Playwright
65
+ // retries) re-submitting a login Auth0 rejects with "Password is
66
+ // required" every single attempt, before surfacing the generic "Login
67
+ // failed after 5 attempts" -- no mention of which slot or that a
68
+ // password was even missing. Fail immediately instead, naming the
69
+ // slot, the same way requireTestUser() already does for specs that
70
+ // reach a numbered credential slot directly.
71
+ if (!user.username || !user.password) {
72
+ throw new Error(`User ${index + 1} (${user.username || 'username also missing'}) has no ` +
73
+ 'password configured -- this SaaS org was created without that ' +
74
+ 'user slot. See scripts/flakiness-saas-create-org.sh, which sizes ' +
75
+ 'the org from the slots each spec names.');
76
+ }
61
77
  await (0, UtilitiesPage_1.loginWithRetry)(page, loginPage, user, (testInfo.workerIndex + 1) * 1000);
62
78
  });
63
79
  await _8_7_1.test.step('Navigate to Web Modeler', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1253",
3
+ "version": "0.0.1255",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",