@camunda/e2e-test-suite 0.0.1275 → 0.0.1277

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.
@@ -10,6 +10,7 @@ const sessionRecovery_1 = require("../../utils/sessionRecovery");
10
10
  const hubResourceRename_1 = require("../../utils/hubResourceRename");
11
11
  const hubDeployDialog_1 = require("../../utils/hubDeployDialog");
12
12
  const consoleApiHelpers_1 = require("../../utils/consoleApiHelpers");
13
+ const modelerCluster_1 = require("../../utils/modelerCluster");
13
14
  class ModelerCreatePage {
14
15
  page;
15
16
  processApplicationPage;
@@ -82,6 +83,7 @@ class ModelerCreatePage {
82
83
  publicAccessToggle;
83
84
  publicationTab;
84
85
  copyPublicLinkButton;
86
+ publicLinkCodeBlock;
85
87
  closeButton;
86
88
  rbaEnabledDeployedDialog;
87
89
  viewProcessLink;
@@ -168,12 +170,26 @@ class ModelerCreatePage {
168
170
  this.viewProcessInstanceLink = page.getByRole('button', {
169
171
  name: 'View process instance',
170
172
  });
171
- this.viewProcessLink = page.getByRole('button', {
172
- name: 'View process in Operate',
173
- });
173
+ // Hub's deploy no longer raises the Carbon "View process in Operate"
174
+ // action: a nightly trace whose POST /internal/v2/files/<id>/deploy
175
+ // returned 200 contains no such control anywhere in the DOM. Its dialog
176
+ // deploys the whole PROJECT, so the confirmation is worded for the
177
+ // deployed entity instead. Accept either, matching pages/8.7.
178
+ this.viewProcessLink = page
179
+ .getByRole('button', { name: 'View process in Operate' })
180
+ .or(page.getByText('view process in Operate'))
181
+ .or(page.getByText(/^(Diagram|Project|Process application) deployed!$/))
182
+ .first();
174
183
  this.deployErrorMessage = page.getByText('The diagram failed to deploy & run');
175
184
  this.nameInput = page.getByLabel('Name', { exact: true });
176
- this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
185
+ // Hub emits no breadcrumb-* data-test attributes (a DOM audit of a live
186
+ // run found none of them); its trail is the `hubBreadcrumbs` landmark and
187
+ // the current diagram is its last entry. Keep the legacy id for the
188
+ // pre-design-system shells.
189
+ this.diagramBreadcrumb = page
190
+ .locator('[data-test="breadcrumb-diagram"]')
191
+ .or((0, hubResourceRename_1.hubBreadcrumbs)(page).getByRole('button').last())
192
+ .first();
177
193
  this.renameDiagramNameButton = page.getByText('Rename');
178
194
  // Hub's rename dialog field, with the legacy inline editable kept for
179
195
  // shells where the design-system migration has not shipped.
@@ -224,10 +240,17 @@ class ModelerCreatePage {
224
240
  this.webhookIsActiveButton = page.getByRole('button', {
225
241
  name: 'Webhook is active in Test',
226
242
  });
243
+ // The webhook panel's "Path" is no longer a readonly textbox labelled
244
+ // "Copy to clipboard": Hub renders it as a design-system code block
245
+ // (<div data-slot="code-block"><pre><code>URL</code></pre>) with the copy
246
+ // action as a separate icon button, so only the <code> carries the URL
247
+ // the caller reads with innerText(). `.first()` keeps the "Path" block,
248
+ // which precedes the "Last activities" one in DOM order.
227
249
  this.copyWebhookUrlToClipboardButton = page
228
250
  .getByRole('textbox', {
229
251
  name: 'Copy to clipboard',
230
252
  })
253
+ .or(page.locator('[data-slot="code-block"] code'))
231
254
  .first();
232
255
  this.createTask = page.getByTitle('Create task');
233
256
  this.createEndEvent = page.getByTitle('Create end event');
@@ -240,7 +263,15 @@ class ModelerCreatePage {
240
263
  this.firstPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
241
264
  this.secondPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
242
265
  this.payloadInput = page.locator('[class="fjs-input"]');
243
- this.marketPlaceButton = page.getByTitle('Browse Marketplace for more Connectors');
266
+ // camunda-hub replaced the change-element popup's marketplace icon with a
267
+ // "Browse all" entry in the popup header: a nightly trace's DOM shows
268
+ // <button title="Browse all available elements and assets"> inside
269
+ // .djs-popup-header-group and no "Browse Marketplace ..." node at all.
270
+ // Keep the legacy title for pre-Hub shells. Same shape as pages/8.7.
271
+ this.marketPlaceButton = page
272
+ .getByTitle('Browse Marketplace for more Connectors')
273
+ .or(page.getByTitle('Browse all available elements and assets'))
274
+ .first();
244
275
  // Match either marketplace display name — the connector was renamed from
245
276
  // "Public Holiday Outbound" to "Worldwide Public Holiday"; accepting both
246
277
  // keeps the flow green whichever the marketplace currently serves.
@@ -281,6 +312,13 @@ class ModelerCreatePage {
281
312
  this.copyPublicLinkButton = page.getByRole('button', {
282
313
  name: 'Copy to clipboard',
283
314
  });
315
+ // The Publication tab's link, held by the code block's <code> rather than
316
+ // by the copy button. Filtered on the scheme so it resolves only once the
317
+ // tab has actually loaded a link.
318
+ this.publicLinkCodeBlock = page
319
+ .locator('[data-slot="code-block"] code')
320
+ .filter({ hasText: /^https?:\/\// })
321
+ .first();
284
322
  this.rbaEnabledDeployedDialog = page.getByRole('dialog', {
285
323
  name: 'Process successfully deployed',
286
324
  });
@@ -619,6 +657,13 @@ class ModelerCreatePage {
619
657
  await this.deployMenuItem.click({ timeout: 60000 });
620
658
  }
621
659
  async deployDiagram(clusterName) {
660
+ // Hub's deploy dialog lists the ENVIRONMENTS assigned to this workspace,
661
+ // not the org's clusters, so a cluster created after the workspace (e.g.
662
+ // hto-user-flows' API-created "Public Form V2 Cluster") is simply absent
663
+ // from it and selectCluster reports the stage binding as not taken.
664
+ // runProcessInstance() and FormJsPage.deployForm() already ensure that
665
+ // link at the point of use; this path was the one that did not.
666
+ await (0, consoleApiHelpers_1.ensureClusterLinkedToSharedWorkspace)(this.page, clusterName);
622
667
  await this.processApplicationPage.ensureStageCluster(clusterName);
623
668
  await this.clickDeployButtonWithRetry();
624
669
  await (0, test_1.expect)(this.deployDiagramDialog).toBeVisible({
@@ -722,7 +767,13 @@ class ModelerCreatePage {
722
767
  catch (error) {
723
768
  console.warn(`Attempt ${attempt} failed: ${error instanceof Error ? error.message : error}`);
724
769
  if (attempt === maxRetries) {
725
- throw new Error(`runProcessInstance failed after ${maxRetries} attempts.`);
770
+ // Carry the last attempt's message. Reporting only the attempt
771
+ // count discarded the one thing needed to act on this: the nightly
772
+ // failed here 10 times over and neither the report, the trace's
773
+ // action log nor the network log (no 4xx/5xx at all) retained why,
774
+ // because the per-attempt reason only ever went to console.warn.
775
+ throw new Error(`runProcessInstance failed after ${maxRetries} attempts. ` +
776
+ `Last error: ${error instanceof Error ? error.message : String(error)}`);
726
777
  }
727
778
  await this.page.reload();
728
779
  await this.page.waitForTimeout(retryDelayMs);
@@ -1451,6 +1502,36 @@ class ModelerCreatePage {
1451
1502
  await this.suppressTestModeAnnouncement();
1452
1503
  await this.playTab.click();
1453
1504
  }
1505
+ // Scopes the "Set up test run" deploy step to the diagram under test.
1506
+ // Hub's step 2 ("Choose resources to deploy") is a radio group whose
1507
+ // "All resources" option (`#deploy-mode-bundle`, value "bundle") arrives
1508
+ // pre-selected, so Play deploys the WHOLE shared project -- and a nightly
1509
+ // trace's network log shows the resulting
1510
+ // `POST /api/internal/v2/projects/<id>/deploy` answering 400 on every one
1511
+ // of the ~25 retry attempts, rejected for OTHER specs' resources left in
1512
+ // the project ("'Document_Handling_Connectors_User_Flow_GCP_Process...bpmn':
1513
+ // Element: StartEvent_1 > extensionElements > ioMapping > output - ERROR:
1514
+ // Expected expression but not found"). "Only this resource"
1515
+ // (`#deploy-mode-single-file`) deploys just this process. Matched by id
1516
+ // because the radio button carries no accessible name of its own: the
1517
+ // label text sits in a sibling <div>.
1518
+ async selectPlayDeploySingleResource() {
1519
+ const singleResourceOption = this.page.locator('#deploy-mode-single-file:not([disabled])');
1520
+ const hasOption = await singleResourceOption
1521
+ .waitFor({ state: 'visible', timeout: 15000 })
1522
+ .then(() => true)
1523
+ .catch(() => false);
1524
+ if (!hasOption) {
1525
+ return;
1526
+ }
1527
+ await (0, test_1.expect)(async () => {
1528
+ if ((await singleResourceOption.getAttribute('aria-checked')) === 'true') {
1529
+ return;
1530
+ }
1531
+ await singleResourceOption.click({ timeout: 10000 });
1532
+ await (0, test_1.expect)(singleResourceOption).toHaveAttribute('aria-checked', 'true', { timeout: 10000 });
1533
+ }).toPass({ timeout: 30000 });
1534
+ }
1454
1535
  async completePlayConfiguration(clusterName) {
1455
1536
  // Play is a tab on the diagram page, not its own URL, so the stage-cluster
1456
1537
  // detour's goto() lands back on the Modeler tab and every Play locator
@@ -1500,9 +1581,12 @@ class ModelerCreatePage {
1500
1581
  // only renders visibly while that item is expanded. It expands by itself
1501
1582
  // once the cluster step completes, but a manual heading click is the
1502
1583
  // recovery path if it did not.
1503
- const deployStepHeading = this.page.getByRole('button', {
1504
- name: 'Deploy process',
1505
- });
1584
+ // With Hub's Environments feature on, the panel is "Set up test run" and
1585
+ // this step is headed "Choose resources to deploy".
1586
+ const deployStepHeading = this.page
1587
+ .getByRole('button', { name: 'Deploy process' })
1588
+ .or(this.page.getByRole('button', { name: 'Choose resources to deploy' }))
1589
+ .first();
1506
1590
  // Play settles into exactly ONE of three states, and which one is not
1507
1591
  // knowable up front, so race them instead of timing out on each in turn:
1508
1592
  // the setup overlay, the legacy Continue button, or the configure-test
@@ -1530,6 +1614,11 @@ class ModelerCreatePage {
1530
1614
  .locator('[data-test="test-studio-configuration-overlay"]')
1531
1615
  .or(this.page.locator('[data-test="play-configuration-overlay"]'))
1532
1616
  .or(this.page.getByText('Setup environment'))
1617
+ // With Hub's Environments feature on the card is titled "Set up test
1618
+ // run" and its steps are "Choose where to run" / "Choose resources to
1619
+ // deploy" / "Configure test case" -- a nightly trace's DOM carries that
1620
+ // H5 and none of the three locators above. Same shape as pages/8.7.
1621
+ .or(this.page.getByText('Set up test run', { exact: true }))
1533
1622
  .first();
1534
1623
  const configureTestPanel = this.page.getByTestId('configure-test-panel');
1535
1624
  await (0, test_1.expect)(configurationOverlay
@@ -1550,8 +1639,34 @@ class ModelerCreatePage {
1550
1639
  const connectClusterButton = this.page.getByRole('button', {
1551
1640
  name: 'Connect cluster',
1552
1641
  });
1642
+ // With Hub's Environments feature on, step 1 ("Choose where to run")
1643
+ // offers "Select environment" instead, which opens the "Choose where to
1644
+ // run" dialog listing the workspace's environment rows (camunda-hub
1645
+ // test-studio ClusterStepContent / TestModeDeployEndpointConfigModal).
1646
+ const selectEnvironmentButton = this.page.getByRole('button', {
1647
+ name: 'Select environment',
1648
+ });
1649
+ // The step shows "Verifying cluster availability" before either button
1650
+ // renders, so give it a moment instead of a non-waiting isVisible().
1651
+ await connectClusterButton
1652
+ .or(selectEnvironmentButton)
1653
+ .first()
1654
+ .waitFor({ state: 'visible', timeout: 15000 })
1655
+ .catch(() => { });
1656
+ if (await selectEnvironmentButton.isVisible().catch(() => false)) {
1657
+ await selectEnvironmentButton.click({ timeout });
1658
+ const chooseWhereToRunDialog = this.page.getByRole('dialog', {
1659
+ name: 'Choose where to run',
1660
+ });
1661
+ await (0, test_1.expect)(chooseWhereToRunDialog).toBeVisible({ timeout });
1662
+ await (0, modelerCluster_1.selectDeployEnvironment)(chooseWhereToRunDialog, clusterName);
1663
+ await chooseWhereToRunDialog
1664
+ .getByRole('button', { name: 'Save', exact: true })
1665
+ .click({ timeout });
1666
+ await (0, test_1.expect)(chooseWhereToRunDialog).toBeHidden({ timeout });
1667
+ }
1553
1668
  const needsConnect = await connectClusterButton
1554
- .isVisible({ timeout: 3000 })
1669
+ .isVisible()
1555
1670
  .catch(() => false);
1556
1671
  if (needsConnect) {
1557
1672
  await connectClusterButton.click({ timeout });
@@ -1639,6 +1754,7 @@ class ModelerCreatePage {
1639
1754
  // Wait for enabled; the button stays disabled until the cluster
1640
1755
  // connection is confirmed by the backend after the Save in step 1.
1641
1756
  await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 60000 });
1757
+ await this.selectPlayDeploySingleResource();
1642
1758
  // The click is retried against the completion probe rather than fired
1643
1759
  // once, because the same collapse re-renders the accordion around the
1644
1760
  // button: it can detach mid-click, which is the second nightly
@@ -1719,11 +1835,26 @@ class ModelerCreatePage {
1719
1835
  await this.fillZeebeClientId(zeebeClientId);
1720
1836
  await (0, sleep_1.sleep)(1000);
1721
1837
  }
1838
+ // Opens one of the start event's output-mapping list items. The header
1839
+ // title is a <code> inside `.bio-properties-panel-collapsible-entry-header`
1840
+ // whose click toggles the item, but filling the previous item's FEEL editor
1841
+ // clears/raises its inline "Variable assignment must be defined." error and
1842
+ // re-lays out the list underneath: a nightly trace shows the click on
1843
+ // `<code>zeebeClientId</code>` reported as done while
1844
+ // `data-entry-id="StartEvent_1-output-2"` kept its collapsed class (its
1845
+ // siblings output-0/output-1 both carried `open`), so the editor below
1846
+ // stayed hidden for all 63 polls. Re-drive the toggle until the entry
1847
+ // actually reports open.
1848
+ async openOutputMappingEntry(header, sourceEditor) {
1849
+ await (0, test_1.expect)(async () => {
1850
+ if (!(await sourceEditor.isVisible().catch(() => false))) {
1851
+ await header.click({ timeout: 15000 });
1852
+ }
1853
+ await (0, test_1.expect)(sourceEditor).toBeVisible({ timeout: 10000 });
1854
+ }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
1855
+ }
1722
1856
  async fillZeebeClientId(clientId) {
1723
- await this.zeebeClientIdVariable.click();
1724
- await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toBeVisible({
1725
- timeout: 60000,
1726
- });
1857
+ await this.openOutputMappingEntry(this.zeebeClientIdVariable, this.variableAssignmentValueTextboxThirdVariable);
1727
1858
  await this.variableAssignmentValueTextboxThirdVariable.click();
1728
1859
  await (0, sleep_1.sleep)(1000);
1729
1860
  await this.variableAssignmentValueTextboxThirdVariable.fill('');
@@ -1731,10 +1862,7 @@ class ModelerCreatePage {
1731
1862
  await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toHaveText(clientId);
1732
1863
  }
1733
1864
  async fillZeebeSecretValue(zeebeSecret) {
1734
- await this.zeebeSecretVariable.click();
1735
- await (0, test_1.expect)(this.variableAssignmentValueTextboxSecondVariable).toBeVisible({
1736
- timeout: 60000,
1737
- });
1865
+ await this.openOutputMappingEntry(this.zeebeSecretVariable, this.variableAssignmentValueTextboxSecondVariable);
1738
1866
  await this.variableAssignmentValueTextboxSecondVariable.click();
1739
1867
  await (0, sleep_1.sleep)(1000);
1740
1868
  await this.variableAssignmentValueTextboxSecondVariable.fill('');
@@ -1809,6 +1937,27 @@ class ModelerCreatePage {
1809
1937
  }
1810
1938
  async getPublicationLink() {
1811
1939
  await this.clickPublicationTab();
1940
+ // Hub renders the publication link as a design-system code block
1941
+ // (<div data-slot="code-block"><pre><code>URL</code></pre>) with the copy
1942
+ // action as a separate icon button, so clicking "Copy to clipboard" no
1943
+ // longer puts the URL where navigator.clipboard.readText() can see it: a
1944
+ // nightly trace has the <code> holding
1945
+ // https://hel-1.tasklist.ultrawombat.com/<uuid>/tasklist/new/<process>
1946
+ // while the test logged "Navigating to public form: " (empty) and then
1947
+ // page.goto('') reloaded the app root, landing on the login screen.
1948
+ // Read the URL out of the DOM, same as pages/8.9. The filter also waits
1949
+ // out the tab's "Updating public process status" spinner, which renders
1950
+ // before the link arrives.
1951
+ const hasCodeBlock = await this.publicLinkCodeBlock
1952
+ .waitFor({ state: 'visible', timeout: 60000 })
1953
+ .then(() => true)
1954
+ .catch(() => false);
1955
+ if (hasCodeBlock) {
1956
+ const link = (await this.publicLinkCodeBlock.innerText()).trim();
1957
+ (0, test_1.expect)(link).toMatch(/^https?:\/\//);
1958
+ console.log('Captured public link:', link);
1959
+ return link;
1960
+ }
1812
1961
  await (0, test_1.expect)(this.copyPublicLinkButton).toBeVisible({ timeout: 60000 });
1813
1962
  await this.clickCopyPublicLinkButton();
1814
1963
  await (0, sleep_1.sleep)(1000);
@@ -12,9 +12,20 @@ class PlayPage {
12
12
  startInstanceOverlayButton;
13
13
  constructor(page) {
14
14
  this.page = page;
15
- this.completeJobButton = page
16
- .getByTestId('diagram')
17
- .getByLabel('Complete job');
15
+ // Hub's Test Studio renders this canvas overlay control as
16
+ // <button data-testid="<elementId>-Complete job"><svg aria-hidden/>
17
+ // <span>Complete job</span></button> -- it carries NO aria-label, so
18
+ // getByLabel() cannot see it (confirmed from a nightly trace: the DOM
19
+ // holds "Complete job" only as the span's text, while the string
20
+ // otherwise appears solely inside our own call log). Its accessible name
21
+ // still comes from that span, so match by role as well and keep
22
+ // getByLabel for the pre-design-system overlay.
23
+ const diagram = page.getByTestId('diagram');
24
+ this.completeJobButton = diagram
25
+ .getByLabel('Complete job')
26
+ .or(diagram.getByRole('button', { name: 'Complete job' }))
27
+ .or(diagram.getByTestId(/-Complete job$/))
28
+ .first();
18
29
  // Newer Modeler Play UI: the "Configure Scenario" floating card hosts a
19
30
  // primary "Start" Carbon button (variants: "Start", "Start with
20
31
  // variables", "Start with a form"). Source:
@@ -7,8 +7,25 @@ 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
+ // Hub's design-system shell renders the header's user menu as
11
+ // <button aria-label="User menu, <full name>" aria-haspopup="menu"> --
12
+ // confirmed in a nightly trace's DOM snapshot, where "Open Settings"
13
+ // appears only inside our own call log and nowhere in the page. The
14
+ // legacy Carbon shell still ships useCamundaTools' "Open Settings"
15
+ // button, so accept either. Same shape as pages/8.7.
16
+ this.openSettingsButton = page
17
+ .getByLabel('Open Settings')
18
+ .or(page.locator('[data-slot="user-menu-trigger"]'))
19
+ .or(page.getByRole('button', { name: /^User menu,/ }))
20
+ .first();
21
+ // The design-system user menu is a Radix dropdown, whose entries carry
22
+ // role="menuitem" (camunda-hub's nav-shell-ds.spec.saas.tsx); the legacy
23
+ // Carbon panel renders a plain role="button". `.first()` avoids a
24
+ // strict-mode violation if both shells' controls are ever present.
25
+ this.logoutButton = page
26
+ .getByRole('button', { name: 'Log out' })
27
+ .or(page.getByRole('menuitem', { name: 'Log out' }))
28
+ .first();
12
29
  }
13
30
  async clickOpenSettingsButton(retries = 3) {
14
31
  for (let i = 0; i < retries; i++) {
@@ -317,6 +317,20 @@ async function runProcess(clusterName, page, modelerHomePage, modelerCreatePage,
317
317
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeVisible({
318
318
  timeout: 60000,
319
319
  });
320
+ // camunda-hub 6813a7e (2026-09-25) made the "Instance started!" toast
321
+ // persistent (shouldPersist) whenever it carries the Operate action, so it
322
+ // no longer times out on its own -- a nightly trace shows the button
323
+ // resolving visible on all 63 polls of the 60s wait below. Close it via its
324
+ // "Dismiss" button. Same handling as pages/8.7.
325
+ const instanceStartedToast = page
326
+ .locator('[data-sonner-toast]')
327
+ .filter({ has: modelerCreatePage.viewProcessInstanceLink });
328
+ const dismissButton = instanceStartedToast.getByRole('button', {
329
+ name: 'Dismiss',
330
+ });
331
+ if (await dismissButton.isVisible().catch(() => false)) {
332
+ await dismissButton.click({ timeout: 10000 });
333
+ }
320
334
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeHidden({
321
335
  timeout: 60000,
322
336
  });
@@ -450,9 +464,35 @@ async function assertTestUsesCorrectOrganization(page) {
450
464
  await (0, test_1.expect)(homePage.modelerBanner).toBeVisible({ timeout: 30000 });
451
465
  }
452
466
  else {
453
- await (0, test_1.expect)(homePage.consoleBanner.or(homePage.hubHeader)).toBeVisible({
454
- timeout: 60000,
455
- });
467
+ // A lost session lands here on Auth0's "Welcome" screen, where no header
468
+ // exists at all -- the nightly screenshot for RBA "No User Permission" is
469
+ // that login card, so the 60s wait below was reporting a missing locator
470
+ // for what is really a sign-in that did not survive. Detect it and say so,
471
+ // rather than blaming the header. (SaaS INT's Auth0 tenant also 429s
472
+ // /u/login/identifier under concurrent CI load, which produces exactly
473
+ // this state.)
474
+ const loginCard = page
475
+ .getByRole('heading', { name: 'Welcome', exact: true })
476
+ .or(page.getByRole('button', { name: 'Continue with Google' }));
477
+ const landed = await homePage.consoleBanner
478
+ .or(homePage.hubHeader)
479
+ .first()
480
+ .waitFor({ state: 'visible', timeout: 60000 })
481
+ .then(() => true)
482
+ .catch(() => false);
483
+ if (!landed) {
484
+ if (await loginCard
485
+ .first()
486
+ .isVisible()
487
+ .catch(() => false)) {
488
+ throw new Error('Session lost: the Auth0 login page is showing, so no Camunda header ' +
489
+ 'can render. The sign-in did not survive (SaaS INT throttles ' +
490
+ '/u/login/identifier under concurrent CI load).');
491
+ }
492
+ await (0, test_1.expect)(homePage.consoleBanner.or(homePage.hubHeader)).toBeVisible({
493
+ timeout: 5000,
494
+ });
495
+ }
456
496
  }
457
497
  // Camunda Hub replaces Console on SaaS (8.10 release notes), and the Hub
458
498
  // shell has neither the "Camunda logo Console" link nor the "Open
@@ -105,6 +105,10 @@ _8_8_1.test.describe('Console User Flow Tests @tasklistV2', () => {
105
105
  });
106
106
  await _8_8_1.test.step('Create First Email Alert', async () => {
107
107
  await clusterDetailsPage.createAlert(true);
108
+ // Hub's alerts table keeps the recipient, date and trigger count in a
109
+ // collapsed expansion row, so the detail assertions below only have a
110
+ // second row to read once it is expanded.
111
+ await clusterDetailsPage.expandAlerts();
108
112
  await test_1.expect
109
113
  .poll(async () => clusterDetailsPage.alertsList.count(), {
110
114
  timeout: 30000,
@@ -165,6 +169,7 @@ _8_8_1.test.describe('Console User Flow Tests @tasklistV2', () => {
165
169
  await (0, test_1.expect)(clusterDetailsPage.alertsList.first()).toBeVisible({
166
170
  timeout: 20000,
167
171
  });
172
+ await clusterDetailsPage.expandAlerts();
168
173
  (0, test_1.expect)(await clusterDetailsPage.alertsList.count()).toBeGreaterThan(1);
169
174
  await (0, test_1.expect)(clusterDetailsPage.alertsList.nth(0)).toContainText(`Email`);
170
175
  await clusterDetailsPage.assertAlertText('Amount triggered1');
@@ -205,6 +210,7 @@ _8_8_1.test.describe('Console User Flow Tests @tasklistV2', () => {
205
210
  await (0, test_1.expect)(clusterDetailsPage.alertsList.first()).toBeVisible({
206
211
  timeout: 20000,
207
212
  });
213
+ await clusterDetailsPage.expandAlerts();
208
214
  (0, test_1.expect)(await clusterDetailsPage.alertsList.count()).toBeGreaterThan(1);
209
215
  await clusterDetailsPage.assertAlertText('Amount triggered3');
210
216
  }).toPass({ timeout: 240000 });
@@ -31,7 +31,9 @@ _8_8_1.test.describe('Web Modeler User Flow Tests', () => {
31
31
  await (0, mailSlurpClient_1.deleteInbox)(inboxes[testInfo.title]);
32
32
  }
33
33
  });
34
- (0, _8_8_1.test)('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
34
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
35
+ // Also tracked in https://github.com/camunda/product-hub/issues/3655
36
+ _8_8_1.test.skip('Form.js Integration with User Task and AI Generated Form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
35
37
  _8_8_1.test.slow();
36
38
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
37
39
  const processName = 'User_Task_Process_With_AI_Form' + randomString;
@@ -88,7 +90,9 @@ _8_8_1.test.describe('Web Modeler User Flow Tests', () => {
88
90
  await operateTabTaskDetailsPage.clickCompleteTaskButton();
89
91
  });
90
92
  });
91
- (0, _8_8_1.test)('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
93
+ // Skipped due to bug #29346: https://github.com/camunda/camunda-hub/issues/29346
94
+ // Also tracked in https://github.com/camunda/product-hub/issues/3655
95
+ _8_8_1.test.skip('Form.js Integration with AI generated Start form @ai-external', async ({ page, homePage, modelerHomePage, appsPage, modelerCreatePage, formJsPage, }) => {
92
96
  _8_8_1.test.slow();
93
97
  const randomString = await (0, _setup_1.generateRandomStringAsync)(3);
94
98
  const processName = 'AI-generated-Start-form' + randomString;
@@ -810,24 +810,51 @@ async function listWorkspaceEnvironments(page, workspaceId) {
810
810
  // only the new id would silently unassign every other cluster the suite had
811
811
  // already linked (the AWS cluster, a sibling Tasklist-mode project's default).
812
812
  async function linkClusterToWorkspaceViaApi(page, workspaceId, clusterName) {
813
- const environments = await listOrganizationEnvironments(page);
813
+ // Hub discovers a cluster into an environment asynchronously, so a cluster
814
+ // this suite just created via the API is healthy well before its
815
+ // environment exists. A single probe therefore failed specs that create
816
+ // their own cluster: the nightly reported `No environment backs cluster
817
+ // "Second RBA V2 Cluster"` while three OTHER clusters were listed ACTIVE,
818
+ // and `"Fourth RBA V2 Cluster"; available: <none>` at a moment when the
819
+ // list came back empty entirely. Poll until this cluster's environment
820
+ // shows up. The throw below still stands once the deadline passes, so a
821
+ // genuinely absent cluster fails exactly as before -- only the race is
822
+ // absorbed.
823
+ const deadline = Date.now() + 120000;
824
+ let environments = await listOrganizationEnvironments(page);
814
825
  if (environments === null) {
815
826
  console.log('Environments feature is off for this org; leaving the workspace unlinked');
816
827
  return;
817
828
  }
818
- const matches = environments.filter((environment) => environment.clusterName === clusterName);
819
- // Prefer an ACTIVE environment over a stale NOT_REPORTED duplicate left by
820
- // an earlier cluster of the same name.
821
- const match = matches.find((environment) => environment.discoveryState === 'ACTIVE') ??
822
- matches[0];
829
+ const pickMatch = (candidates) => {
830
+ const matched = candidates.filter((environment) => environment.clusterName === clusterName);
831
+ // Prefer an ACTIVE environment over a stale NOT_REPORTED duplicate left by
832
+ // an earlier cluster of the same name.
833
+ return (matched.find((environment) => environment.discoveryState === 'ACTIVE') ??
834
+ matched[0]);
835
+ };
836
+ let match = pickMatch(environments);
837
+ while (!match && Date.now() < deadline) {
838
+ await page.waitForTimeout(5000);
839
+ const refreshed = await listOrganizationEnvironments(page);
840
+ if (refreshed === null) {
841
+ return;
842
+ }
843
+ environments = refreshed;
844
+ match = pickMatch(environments);
845
+ }
823
846
  if (!match) {
824
847
  throw new Error(`No environment backs cluster "${clusterName}"; available: ` +
825
848
  (environments
826
849
  .map((e) => `${e.name}@${e.clusterName} (${e.discoveryState})`)
827
850
  .join(', ') || '<none>'));
828
851
  }
852
+ // Bind to a const so the narrowing survives the closures below: `match` is
853
+ // a `let` reassigned by the discovery poll, so TypeScript widens it back to
854
+ // `HubEnvironment | undefined` inside callbacks.
855
+ const backing = match;
829
856
  const assigned = await listWorkspaceEnvironments(page, workspaceId);
830
- if (assigned.some((environment) => environment.id === match.id)) {
857
+ if (assigned.some((environment) => environment.id === backing.id)) {
831
858
  // Logged rather than returning silently: a quiet no-op here is
832
859
  // indistinguishable from the helper never having run at all, which is
833
860
  // exactly the ambiguity that hid an earlier bug.
@@ -845,10 +872,10 @@ async function linkClusterToWorkspaceViaApi(page, workspaceId, clusterName) {
845
872
  const current = attempt === 1
846
873
  ? assigned
847
874
  : await listWorkspaceEnvironments(page, workspaceId);
848
- if (attempt > 1 && current.some((e) => e.id === match.id)) {
875
+ if (attempt > 1 && current.some((e) => e.id === backing.id)) {
849
876
  return;
850
877
  }
851
- const ids = [...new Set([...current.map((e) => e.id), match.id])];
878
+ const ids = [...new Set([...current.map((e) => e.id), backing.id])];
852
879
  const response = await clusterApiRequest(page, 'PUT', url, {
853
880
  environmentIds: ids,
854
881
  });
@@ -860,8 +887,8 @@ async function linkClusterToWorkspaceViaApi(page, workspaceId, clusterName) {
860
887
  `${response.status()} ${response.statusText()}`);
861
888
  }
862
889
  const after = await listWorkspaceEnvironments(page, workspaceId);
863
- if (after.some((e) => e.id === match.id)) {
864
- console.log(`Linked cluster "${clusterName}" (environment ${match.id}) to workspace ${workspaceId}`);
890
+ if (after.some((e) => e.id === backing.id)) {
891
+ console.log(`Linked cluster "${clusterName}" (environment ${backing.id}) to workspace ${workspaceId}`);
865
892
  return;
866
893
  }
867
894
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1275",
3
+ "version": "0.0.1277",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",