@camunda/e2e-test-suite 0.0.1286 → 0.0.1288

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.
Files changed (34) hide show
  1. package/dist/pages/8.9/AppsPage.d.ts +9 -3
  2. package/dist/pages/8.9/AppsPage.js +262 -47
  3. package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
  4. package/dist/pages/8.9/ClusterDetailsPage.js +92 -9
  5. package/dist/pages/8.9/ClusterSecretsPage.js +5 -1
  6. package/dist/pages/8.9/ConnectorMarketplacePage.d.ts +1 -0
  7. package/dist/pages/8.9/ConnectorMarketplacePage.js +46 -3
  8. package/dist/pages/8.9/ConsoleOrganizationPage.d.ts +3 -2
  9. package/dist/pages/8.9/ConsoleOrganizationPage.js +47 -22
  10. package/dist/pages/8.9/FormJsPage.d.ts +4 -0
  11. package/dist/pages/8.9/FormJsPage.js +53 -19
  12. package/dist/pages/8.9/IdpCreatePage.js +5 -0
  13. package/dist/pages/8.9/ModelerCreatePage.d.ts +5 -0
  14. package/dist/pages/8.9/ModelerCreatePage.js +153 -21
  15. package/dist/pages/8.9/ModelerHomePage.d.ts +1 -0
  16. package/dist/pages/8.9/ModelerHomePage.js +29 -4
  17. package/dist/pages/8.9/PlayPage.js +11 -1
  18. package/dist/pages/8.9/ProcessApplicationPage.js +48 -4
  19. package/dist/pages/8.9/UtilitiesPage.js +57 -17
  20. package/dist/pages/8.9/WorkspaceMembersPage.d.ts +21 -0
  21. package/dist/pages/8.9/WorkspaceMembersPage.js +264 -0
  22. package/dist/tests/8.9/access-user-role-management.spec.js +111 -26
  23. package/dist/tests/8.9/console-user-flows.spec.js +4 -4
  24. package/dist/tests/8.9/idp-user-flows.spec.js +2 -1
  25. package/dist/tests/8.9/rba-enabled-v1-user-flows.spec.js +13 -4
  26. package/dist/tests/8.9/rba-enabled-v2-user-flows.spec.js +13 -4
  27. package/dist/tests/8.9/test-setup.spec.js +57 -4
  28. package/dist/tests/8.9/utr-enabled-user-flows.spec.js +18 -0
  29. package/dist/tests/8.9/web-modeler-user-flows.spec.js +6 -2
  30. package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.js +11 -0
  31. package/dist/utils/consoleApiHelpers.d.ts +2 -0
  32. package/dist/utils/consoleApiHelpers.js +50 -4
  33. package/dist/utils/resetSession.js +6 -1
  34. package/package.json +1 -1
@@ -7,6 +7,7 @@ const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
7
7
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
8
8
  const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
9
  const modelerCluster_1 = require("../../utils/modelerCluster");
10
+ const sessionRecovery_1 = require("../../utils/sessionRecovery");
10
11
  class ModelerCreatePage {
11
12
  page;
12
13
  processApplicationPage;
@@ -113,6 +114,8 @@ class ModelerCreatePage {
113
114
  eighthPlacedElement;
114
115
  deployAndRunMainButton;
115
116
  deployAndRunSubButton;
117
+ deployRunActions;
118
+ deployAndRunMenuItem;
116
119
  dialog;
117
120
  backupDeployButton;
118
121
  globalConnectToolButton;
@@ -154,6 +157,13 @@ class ModelerCreatePage {
154
157
  name: 'Deploy & run',
155
158
  exact: true,
156
159
  });
160
+ // camunda-hub deploy-run-combo-action: both the Carbon ComboButton and
161
+ // the design-system SplitButton carry this test id.
162
+ this.deployRunActions = page.getByTestId('deploy-run-actions');
163
+ this.deployAndRunMenuItem = page.getByRole('menuitem', {
164
+ name: 'Deploy & run',
165
+ exact: true,
166
+ });
157
167
  this.deployAndRunSubButton = this.dialog.getByRole('button', {
158
168
  name: 'Deploy & run',
159
169
  exact: true,
@@ -164,9 +174,15 @@ class ModelerCreatePage {
164
174
  this.viewProcessInstanceLink = page.getByRole('button', {
165
175
  name: 'View process instance',
166
176
  });
167
- this.viewProcessLink = page.getByRole('button', {
168
- name: 'View process in Operate',
169
- });
177
+ // Hub's deploy confirmation carries no "View process in Operate" control:
178
+ // the 8.9 nightly trace shows POST /v2/projects/<id>/deploy returning 200
179
+ // and a "Project deployed!" toast. Accept the deployed toast as the
180
+ // confirmation, as pages/8.8 does.
181
+ this.viewProcessLink = page
182
+ .getByRole('button', { name: 'View process in Operate' })
183
+ .or(page.getByText('view process in Operate'))
184
+ .or(page.getByText(/^(Diagram|Project|Process application) deployed!$/))
185
+ .first();
170
186
  this.nameInput = page.getByLabel('Name', { exact: true });
171
187
  this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
172
188
  // Hub's newer nav (isNavV2, camunda-hub use-file-page-actions.ts) renders
@@ -190,7 +206,13 @@ class ModelerCreatePage {
190
206
  this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
191
207
  this.variableInput = page.locator('[id="variables-json"]');
192
208
  this.embedFormButton = page.getByRole('button', { name: 'Link form' });
193
- this.embedButton = page.locator('[data-test="confirm-move"]');
209
+ // With workspaces on, Hub's "Link a form" picker is LinkTargetModal, which
210
+ // confirms with `link-target-confirm`; `confirm-move` is the legacy
211
+ // TargetSelector's. Same as pages/8.8.
212
+ this.embedButton = page
213
+ .locator('[data-test="link-target-confirm"]')
214
+ .or(page.locator('[data-test="confirm-move"]'))
215
+ .first();
194
216
  this.newForm = page
195
217
  .locator('[data-test="item-New form"]')
196
218
  .getByText('New form');
@@ -214,10 +236,14 @@ class ModelerCreatePage {
214
236
  this.webhookIsActiveButton = page.getByRole('button', {
215
237
  name: 'Webhook is active in Test',
216
238
  });
239
+ // Hub's design-system panel renders the webhook "Path" as a code block
240
+ // (<div data-slot="code-block"><pre><code>URL) with a separate copy button;
241
+ // the <code> holds the URL. The "Path" block precedes the log blocks.
217
242
  this.copyWebhookUrlToClipboardButton = page
218
243
  .getByRole('textbox', {
219
244
  name: 'Copy to clipboard',
220
245
  })
246
+ .or(page.locator('[data-slot="code-block"] code'))
221
247
  .first();
222
248
  this.createTask = page.getByTitle('Create task');
223
249
  this.createEndEvent = page.getByTitle('Create end event');
@@ -230,7 +256,13 @@ class ModelerCreatePage {
230
256
  this.firstPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
231
257
  this.secondPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
232
258
  this.payloadInput = page.locator('[class="fjs-input"]');
233
- this.marketPlaceButton = page.getByTitle('Browse Marketplace for more Connectors');
259
+ // camunda-hub's change-element popup replaced the Marketplace entry with
260
+ // "Browse all" (browse-all-popup-menu-provider.ts); the 8.9 nightly trace
261
+ // carries only this title. Same locator as pages/8.8.
262
+ this.marketPlaceButton = page
263
+ .getByTitle('Browse Marketplace for more Connectors')
264
+ .or(page.getByTitle('Browse all available elements and assets'))
265
+ .first();
234
266
  // Match either marketplace display name — the connector was renamed from
235
267
  // "Public Holiday Outbound" to "Worldwide Public Holiday"; accepting both
236
268
  // keeps the flow green whichever the marketplace currently serves.
@@ -267,9 +299,14 @@ class ModelerCreatePage {
267
299
  name: 'Toggle public access',
268
300
  });
269
301
  this.publicationTab = page.getByRole('tab', { name: 'Publication' });
270
- this.copyPublicLinkButton = page.getByRole('textbox', {
302
+ // Legacy Carbon snippet (textbox) or Hub's design-system code block; both
303
+ // wrap the link in the <code> that getPublicationLink() reads.
304
+ this.copyPublicLinkButton = page
305
+ .getByRole('textbox', {
271
306
  name: 'Copy to clipboard',
272
- });
307
+ })
308
+ .or(page.locator('[data-slot="code-block"]'))
309
+ .first();
273
310
  this.closeButton = page.getByRole('button', { name: 'Close', exact: true });
274
311
  this.rbaEnabledDeployedDialog = page.getByRole('dialog', {
275
312
  name: 'Process successfully deployed',
@@ -355,9 +392,14 @@ class ModelerCreatePage {
355
392
  name: 'Deploy',
356
393
  });
357
394
  this.globalConnectToolButton = page.locator('[title="Activate global connect tool"]');
395
+ // Under the design system Hub raises this as a design-system toast, not a
396
+ // Carbon `cds--toast-*` node, so match the message text itself.
358
397
  this.formLinkedSuccessMessage = page
398
+ .getByText('Form has been successfully linked.')
399
+ .or(page
359
400
  .locator('.cds--toast-notification__subtitle')
360
- .getByText('Form has been successfully linked.');
401
+ .getByText('Form has been successfully linked.'))
402
+ .first();
361
403
  // The legacy breadcrumb shows the name directly; the nav-v2 kebab embeds
362
404
  // it in its own accessible name ("<name> actions").
363
405
  this.diagramNameBreadcrumb = (diagramName) => page
@@ -556,10 +598,14 @@ class ModelerCreatePage {
556
598
  // a form with the same name and the picker then lists two identical entries.
557
599
  // Scope to the first matching item so the link step stays deterministic on retry
558
600
  // instead of failing with a strict-mode violation.
601
+ // LinkTargetModal lists each form as a button labelled "Select <name>"
602
+ // inside `link-target-list` and renders no `item-<name>` node; the legacy
603
+ // id stays as a fallback for the TargetSelector picker.
559
604
  await this.page
560
- .locator(`[data-test="item-${name}"]`)
605
+ .locator('[data-test="link-target-list"]')
606
+ .getByRole('button', { name: `Select ${name}`, exact: true })
607
+ .or(this.page.locator(`[data-test="item-${name}"]`).first().getByText(name))
561
608
  .first()
562
- .getByText(name)
563
609
  .click({ timeout: 90000 });
564
610
  }
565
611
  async clickUserTask(id) {
@@ -641,6 +687,20 @@ class ModelerCreatePage {
641
687
  await (0, test_1.expect)(this.viewProcessLink).toBeVisible({ timeout: 30000 });
642
688
  await (0, test_1.expect)(this.viewProcessLink).toBeHidden({ timeout: 30000 });
643
689
  }
690
+ // For a user whose org role lacks instance/workflow create permissions,
691
+ // Hub's checkIfAllowedToRun is false: the split button's primary action
692
+ // becomes "Deploy" and "Deploy & run" moves into its menu, disabled
693
+ // (camunda-hub BPMNDeployAndRunButtonsDs.tsx).
694
+ async assertDeployAndRunNotPermitted() {
695
+ await (0, test_1.expect)(this.deployRunActions.getByRole('button', { name: 'Deploy', exact: true })).toBeVisible({ timeout: 60000 });
696
+ await (0, test_1.expect)(this.deployAndRunMainButton).toHaveCount(0);
697
+ await this.deployRunActions
698
+ .getByRole('button', { name: 'Additional actions' })
699
+ .click({ timeout: 30000 });
700
+ await (0, test_1.expect)(this.deployAndRunMenuItem).toBeVisible({ timeout: 30000 });
701
+ await (0, test_1.expect)(this.deployAndRunMenuItem).toBeDisabled();
702
+ await this.page.keyboard.press('Escape');
703
+ }
644
704
  async clickStartInstanceMainButton() {
645
705
  await this.deployAndRunMainButton.click({ timeout: 30000 });
646
706
  }
@@ -1457,6 +1517,36 @@ class ModelerCreatePage {
1457
1517
  await (0, test_1.expect)(this.playTab).toBeVisible({ timeout: 90000 });
1458
1518
  await this.playTab.click({ timeout: 30000 });
1459
1519
  }
1520
+ // Scopes the "Set up test run" deploy step to the diagram under test.
1521
+ // Hub's step 2 ("Choose resources to deploy") is a radio group whose
1522
+ // "All resources" option (`#deploy-mode-bundle`, value "bundle") arrives
1523
+ // pre-selected, so Play deploys the WHOLE shared project -- and a nightly
1524
+ // trace's network log shows the resulting
1525
+ // `POST /api/internal/v2/projects/<id>/deploy` answering 400 on every one
1526
+ // of the ~25 retry attempts, rejected for OTHER specs' resources left in
1527
+ // the project ("'Document_Handling_Connectors_User_Flow_GCP_Process...bpmn':
1528
+ // Element: StartEvent_1 > extensionElements > ioMapping > output - ERROR:
1529
+ // Expected expression but not found"). "Only this resource"
1530
+ // (`#deploy-mode-single-file`) deploys just this process. Matched by id
1531
+ // because the radio button carries no accessible name of its own: the
1532
+ // label text sits in a sibling <div>.
1533
+ async selectPlayDeploySingleResource() {
1534
+ const singleResourceOption = this.page.locator('#deploy-mode-single-file:not([disabled])');
1535
+ const hasOption = await singleResourceOption
1536
+ .waitFor({ state: 'visible', timeout: 15000 })
1537
+ .then(() => true)
1538
+ .catch(() => false);
1539
+ if (!hasOption) {
1540
+ return;
1541
+ }
1542
+ await (0, test_1.expect)(async () => {
1543
+ if ((await singleResourceOption.getAttribute('aria-checked')) === 'true') {
1544
+ return;
1545
+ }
1546
+ await singleResourceOption.click({ timeout: 10000 });
1547
+ await (0, test_1.expect)(singleResourceOption).toHaveAttribute('aria-checked', 'true', { timeout: 10000 });
1548
+ }).toPass({ timeout: 30000 });
1549
+ }
1460
1550
  async completePlayConfiguration(clusterName) {
1461
1551
  // Play is a tab on the diagram page, not its own URL, so the stage-cluster
1462
1552
  // detour's goto() lands back on the Modeler tab and every Play locator
@@ -1500,9 +1590,12 @@ class ModelerCreatePage {
1500
1590
  // only renders visibly while that item is expanded. It expands by itself
1501
1591
  // once the cluster step completes, but a manual heading click is the
1502
1592
  // recovery path if it did not.
1503
- const deployStepHeading = this.page.getByRole('button', {
1504
- name: 'Deploy process',
1505
- });
1593
+ // With Hub's Environments feature on, the panel is "Set up test run" and
1594
+ // this step is headed "Choose resources to deploy".
1595
+ const deployStepHeading = this.page
1596
+ .getByRole('button', { name: 'Deploy process' })
1597
+ .or(this.page.getByRole('button', { name: 'Choose resources to deploy' }))
1598
+ .first();
1506
1599
  // Both attributes on this button have been renamed by camunda-hub, in
1507
1600
  // separate commits: a90fd06 changed the label from "Configure scenario" to
1508
1601
  // "Configure test case", and b3958a1649 (#27259, 2026-08-07) then changed
@@ -1544,6 +1637,11 @@ class ModelerCreatePage {
1544
1637
  .locator('[data-test="test-studio-configuration-overlay"]')
1545
1638
  .or(this.page.locator('[data-test="play-configuration-overlay"]'))
1546
1639
  .or(this.page.getByText('Setup environment'))
1640
+ // With Hub's Environments feature on the card is titled "Set up test
1641
+ // run" and its steps are "Choose where to run" / "Choose resources to
1642
+ // deploy" / "Configure test case" -- the 8.9 nightly trace's DOM carries
1643
+ // that title and none of the three locators above. Same as pages/8.8.
1644
+ .or(this.page.getByText('Set up test run', { exact: true }))
1547
1645
  .first();
1548
1646
  const configureTestPanel = this.page.getByTestId('configure-test-panel');
1549
1647
  await (0, test_1.expect)(configurationOverlay
@@ -1578,6 +1676,33 @@ class ModelerCreatePage {
1578
1676
  .first();
1579
1677
  const clusterHealthyIcon = this.page.locator('[aria-label="Cluster healthy"]');
1580
1678
  const clusterUnavailableIcon = this.page.locator('[aria-label="Cluster unavailable"]');
1679
+ // With Hub's Environments feature on, step 1 ("Choose where to run")
1680
+ // offers "Select environment" instead, which opens the "Choose where to
1681
+ // run" dialog listing the workspace's environment rows (camunda-hub
1682
+ // test-studio ClusterStepContent / TestModeDeployEndpointConfigModal).
1683
+ // Same handling as pages/8.8.
1684
+ const selectEnvironmentButton = this.page.getByRole('button', {
1685
+ name: 'Select environment',
1686
+ });
1687
+ // The step shows "Verifying cluster availability" before either button
1688
+ // renders, so give it a moment instead of a non-waiting isVisible().
1689
+ await connectClusterButton
1690
+ .or(selectEnvironmentButton)
1691
+ .first()
1692
+ .waitFor({ state: 'visible', timeout: 15000 })
1693
+ .catch(() => { });
1694
+ if (await selectEnvironmentButton.isVisible().catch(() => false)) {
1695
+ await selectEnvironmentButton.click({ timeout });
1696
+ const chooseWhereToRunDialog = this.page.getByRole('dialog', {
1697
+ name: 'Choose where to run',
1698
+ });
1699
+ await (0, test_1.expect)(chooseWhereToRunDialog).toBeVisible({ timeout });
1700
+ await (0, modelerCluster_1.selectDeployEnvironment)(chooseWhereToRunDialog, clusterName);
1701
+ await chooseWhereToRunDialog
1702
+ .getByRole('button', { name: 'Save', exact: true })
1703
+ .click({ timeout });
1704
+ await (0, test_1.expect)(chooseWhereToRunDialog).toBeHidden({ timeout });
1705
+ }
1581
1706
  const hasConnectButton = await connectClusterButton
1582
1707
  .isVisible()
1583
1708
  .catch(() => false);
@@ -1685,6 +1810,7 @@ class ModelerCreatePage {
1685
1810
  // Wait for enabled; the button stays disabled until the cluster
1686
1811
  // connection is confirmed by the backend after the Save in step 1.
1687
1812
  await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 60000 });
1813
+ await this.selectPlayDeploySingleResource();
1688
1814
  // The click is retried against the completion probe rather than fired
1689
1815
  // once, because the same collapse re-renders the accordion around the
1690
1816
  // button: it can detach mid-click, which is the second nightly
@@ -1764,6 +1890,7 @@ class ModelerCreatePage {
1764
1890
  }
1765
1891
  async fillStartEventVariablesForDocHandling(zeebeUrl, zeebeClientId, zeebeClientSecret) {
1766
1892
  console.log(`URL: ${zeebeUrl}, ClientId: ${zeebeClientId}, Secret: ${zeebeClientSecret}`);
1893
+ await (0, sessionRecovery_1.recoverStuckModelerLogin)(this.page);
1767
1894
  await (0, test_1.expect)(this.connectorsDocHandlingStartEventElement).toBeVisible({
1768
1895
  timeout: 60000,
1769
1896
  });
@@ -1777,11 +1904,19 @@ class ModelerCreatePage {
1777
1904
  await this.fillZeebeClientId(zeebeClientId);
1778
1905
  await (0, sleep_1.sleep)(1000);
1779
1906
  }
1907
+ // Filling the previous output mapping re-lays out the list, so a header click
1908
+ // can land without opening its entry (the 8.8 trace kept output-2 collapsed
1909
+ // for all 63 polls). Re-drive the toggle until the source editor shows.
1910
+ async openOutputMappingEntry(header, sourceEditor) {
1911
+ await (0, test_1.expect)(async () => {
1912
+ if (!(await sourceEditor.isVisible().catch(() => false))) {
1913
+ await header.click({ timeout: 15000 });
1914
+ }
1915
+ await (0, test_1.expect)(sourceEditor).toBeVisible({ timeout: 10000 });
1916
+ }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
1917
+ }
1780
1918
  async fillZeebeClientId(clientId) {
1781
- await this.zeebeClientIdVariable.click();
1782
- await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toBeVisible({
1783
- timeout: 60000,
1784
- });
1919
+ await this.openOutputMappingEntry(this.zeebeClientIdVariable, this.variableAssignmentValueTextboxThirdVariable);
1785
1920
  await this.variableAssignmentValueTextboxThirdVariable.click();
1786
1921
  await (0, sleep_1.sleep)(1000);
1787
1922
  await this.variableAssignmentValueTextboxThirdVariable.fill('');
@@ -1789,10 +1924,7 @@ class ModelerCreatePage {
1789
1924
  await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toHaveText(clientId);
1790
1925
  }
1791
1926
  async fillZeebeSecretValue(zeebeSecret) {
1792
- await this.zeebeSecretVariable.click();
1793
- await (0, test_1.expect)(this.variableAssignmentValueTextboxSecondVariable).toBeVisible({
1794
- timeout: 60000,
1795
- });
1927
+ await this.openOutputMappingEntry(this.zeebeSecretVariable, this.variableAssignmentValueTextboxSecondVariable);
1796
1928
  await this.variableAssignmentValueTextboxSecondVariable.click();
1797
1929
  await (0, sleep_1.sleep)(1000);
1798
1930
  await this.variableAssignmentValueTextboxSecondVariable.fill('');
@@ -69,6 +69,7 @@ declare class ModelerHomePage {
69
69
  private clickCreateProcessApplication;
70
70
  private ensureInResourceContainer;
71
71
  returnToResourceContainer(): Promise<void>;
72
+ private isOnHubLoginRoute;
72
73
  startBlankBpmnDiagram(): Promise<void>;
73
74
  private becameVisible;
74
75
  private createWorkspaceViaWizard;
@@ -570,12 +570,37 @@ class ModelerHomePage {
570
570
  await this.clickWorkspaceBreadcrumb();
571
571
  return;
572
572
  }
573
- await this.page.goto(this.processApplicationUrl, {
574
- waitUntil: 'domcontentloaded',
575
- });
576
- await this.processApplicationPage.waitForProcessApplication();
573
+ // Every goto re-runs Hub's Auth0 hand-off, and CI runs show it stalling on
574
+ // /login-callback. Its code is single-use, so recover with a fresh goto,
575
+ // never a reload; any other failure still throws.
576
+ for (let attempt = 1;; attempt++) {
577
+ await this.page.goto(this.processApplicationUrl, {
578
+ waitUntil: 'domcontentloaded',
579
+ });
580
+ try {
581
+ await this.processApplicationPage.waitForProcessApplication(attempt === 1 ? 90000 : 150000);
582
+ break;
583
+ }
584
+ catch (error) {
585
+ const stalledInLogin = (await (0, sessionRecovery_1.restoreSessionIfLoggedOut)(this.page)) ||
586
+ this.isOnHubLoginRoute();
587
+ if (attempt === 2 || !stalledInLogin) {
588
+ throw error;
589
+ }
590
+ console.warn(`Hub's login hand-off stalled at ${this.page.url()}; ` +
591
+ 're-entering the process application.');
592
+ }
593
+ }
577
594
  await this.dismissOverlays();
578
595
  }
596
+ // Hub's own /login and /login-callback routes render only a spinner
597
+ // (camunda-hub App.tsx); the identity provider's pages are not these.
598
+ isOnHubLoginRoute() {
599
+ if ((0, sessionRecovery_1.isAtIdentityProvider)(this.page)) {
600
+ return false;
601
+ }
602
+ return /^\/login(-callback)?\/?$/.test(new URL(this.page.url()).pathname);
603
+ }
579
604
  // Single seam for "give me an empty BPMN diagram in this test's container".
580
605
  // A process application always ships an auto-created main process; this adds
581
606
  // a new diagram rather than adopting that one, so every existing modelling
@@ -14,7 +14,17 @@ class PlayPage {
14
14
  constructor(page) {
15
15
  this.page = page;
16
16
  this.diagram = page.getByTestId('diagram');
17
- this.completeJobButton = this.diagram.getByLabel('Complete job');
17
+ // Hub's Test Studio renders this canvas overlay control as
18
+ // <button data-testid="<elementId>-Complete job"><svg aria-hidden/>
19
+ // <span>Complete job</span></button> with NO aria-label, so getByLabel()
20
+ // cannot see it. Its accessible name still comes from the span, so match
21
+ // by role as well and keep getByLabel for the pre-design-system overlay.
22
+ // Same locator as pages/8.8.
23
+ this.completeJobButton = this.diagram
24
+ .getByLabel('Complete job')
25
+ .or(this.diagram.getByRole('button', { name: 'Complete job' }))
26
+ .or(this.diagram.getByTestId(/-Complete job$/))
27
+ .first();
18
28
  // Newer Modeler Play UI: the "Configure Scenario" floating card hosts a
19
29
  // primary "Start" Carbon button (variants: "Start", "Start with
20
30
  // variables", "Start with a form"). Source:
@@ -255,6 +255,20 @@ class ProcessApplicationPage {
255
255
  if (!container || container.developmentCluster === clusterName) {
256
256
  return false;
257
257
  }
258
+ // A caller that just reloaded can still be on Hub's /login-callback
259
+ // (the session refresh round-trip) -- the 8.9 nightly trace caught
260
+ // returnTo as /login-callback?code=..., and replaying that single-use
261
+ // code on the way back landed on the Auth0 login page, so every later
262
+ // lookup on the diagram timed out. Let Hub finish the exchange (measured
263
+ // at ~45s on CI, see assertTestUsesCorrectOrganization) before recording
264
+ // where to come back to.
265
+ if (this.page.url().includes('/login-callback')) {
266
+ await this.page
267
+ .waitForURL((url) => !url.pathname.startsWith('/login-callback'), {
268
+ timeout: 120000,
269
+ })
270
+ .catch(() => { });
271
+ }
258
272
  const returnTo = this.page.url();
259
273
  const paUrl = container.url.replace(/[?#].*$/, '').replace(/\/$/, '');
260
274
  // Retried as a whole, re-entering the page on every attempt. This binding
@@ -287,6 +301,17 @@ class ProcessApplicationPage {
287
301
  }).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
288
302
  container.developmentCluster = clusterName;
289
303
  await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
304
+ // A full navigation on Hub bounces through /login?returnUrl=... and
305
+ // /login-callback before it lands back on returnTo, and goto() resolves on
306
+ // the FIRST document. Callers act on the page straight away -- the 8.9
307
+ // Play nightly trace shows switchToPlay's page.evaluate dying with
308
+ // "Execution context was destroyed" mid-redirect -- so wait for the
309
+ // round-trip to finish.
310
+ await this.page
311
+ .waitForURL((url) => !/^\/login(-callback)?\/?$/.test(url.pathname), {
312
+ timeout: 120000,
313
+ })
314
+ .catch(() => { });
290
315
  return true;
291
316
  }
292
317
  // Assigns the Environment backed by clusterName to the workspace, from
@@ -325,10 +350,29 @@ class ProcessApplicationPage {
325
350
  const manageUrl = `${new URL(paUrl).origin}/workspaces/${workspaceId}/manage?tab=environments`;
326
351
  await this.page.goto(manageUrl, { waitUntil: 'domcontentloaded' });
327
352
  const heading = this.page.locator('[data-test="manage-environments-heading"]');
328
- if (!(await heading
329
- .waitFor({ state: 'visible', timeout: 60000 })
330
- .then(() => true)
331
- .catch(() => false))) {
353
+ // Hub only offers the Environments tab to users with an elevated or
354
+ // DevOps org role (camunda-hub WorkspaceManageBody, #28955) and rewrites
355
+ // `?tab=environments` to `?tab=members` for everyone else -- the 8.9 RBA
356
+ // nightly traces show exactly that redirect for the handed-off test user.
357
+ // Such a user cannot assign anything here; the spec's elevated user has
358
+ // to have done it already. Leave the check to the deploy dialog, whose
359
+ // selectDeployEnvironment fails with a precise "not assigned" error.
360
+ const tabOutcome = await Promise.race([
361
+ heading
362
+ .waitFor({ state: 'visible', timeout: 60000 })
363
+ .then(() => 'environments'),
364
+ this.page
365
+ .waitForURL((url) => url.searchParams.get('tab') === 'members', {
366
+ timeout: 60000,
367
+ })
368
+ .then(() => 'members'),
369
+ ]).catch(() => 'none');
370
+ if (tabOutcome === 'members') {
371
+ console.log(`The workspace Environments tab is not available to this user; ` +
372
+ `relying on "${clusterName}" having been assigned by an elevated user`);
373
+ return;
374
+ }
375
+ if (tabOutcome !== 'environments') {
332
376
  throw new Error(`Could not bind "${clusterName}" as a deploy target: the project page ` +
333
377
  'has no Connected clusters "Configure" control, and the workspace ' +
334
378
  `Environments tab did not render at ${manageUrl}.`);
@@ -36,6 +36,24 @@ async function suppressOnboardingTour(page) {
36
36
  }, '^hub\\.onboarding\\..*\\.seen\\.');
37
37
  }
38
38
  async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5) {
39
+ // A blank credential cannot be fixed by retrying, and in the browser it
40
+ // looks like a slow login: auth0 keeps the tab on its own page until every
41
+ // attempt times out. GitHub leaves a password empty when it drops the
42
+ // create-organization job output that carries it ("Skip output ... since it
43
+ // may contain secret"), because a generated password can contain a masked
44
+ // value by chance.
45
+ if (!testUser?.username || !testUser?.password) {
46
+ const missing = [
47
+ testUser?.username ? null : 'username',
48
+ testUser?.password ? null : 'password',
49
+ ]
50
+ .filter(Boolean)
51
+ .join(' and ');
52
+ throw new Error(`Cannot log in: this test user has no ${missing}. The credential never ` +
53
+ 'reached the runner -- check the create-organization job for a ' +
54
+ '"Skip output ... since it may contain secret" warning on the ' +
55
+ 'matching C8_*_PASSWORD output.');
56
+ }
39
57
  await suppressOnboardingTour(page);
40
58
  let lastError;
41
59
  // `timeout` is a stable per-worker identifier (derived from workerIndex),
@@ -52,8 +70,13 @@ async function loginWithRetry(page, loginPage, testUser, timeout, maxRetries = 5
52
70
  // for the tab's whole lifetime; without clearing it here alongside
53
71
  // cookies/storage, a flow that logs a different user into the same
54
72
  // page could have getUserToken() return the PREVIOUS user's token.
55
- (0, consoleApiHelpers_1.resetHubBearerToken)(page);
73
+ // Listen from before Hub loads: an API helper called later on an idle
74
+ // Hub page would otherwise find no request carrying the token.
75
+ (0, consoleApiHelpers_1.observeBearerTokens)(page);
56
76
  await page.goto('about:blank');
77
+ // After about:blank: until then the previous user's Hub page can still
78
+ // send a request, and its token must not count for the new session.
79
+ (0, consoleApiHelpers_1.resetHubBearerToken)(page);
57
80
  await page
58
81
  .evaluate(() => {
59
82
  try {
@@ -312,6 +335,20 @@ async function runProcess(clusterName, page, modelerHomePage, modelerCreatePage,
312
335
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeVisible({
313
336
  timeout: 60000,
314
337
  });
338
+ // camunda-hub 6813a7e (2026-09-25) made the "Instance started!" toast
339
+ // persistent (shouldPersist) whenever it carries the Operate action, so it
340
+ // no longer times out on its own -- the 8.9 nightly trace shows it still
341
+ // open when the 60s wait below expires. Close it via its "Dismiss" button.
342
+ // Same handling as pages/8.7 and pages/8.8.
343
+ const instanceStartedToast = page
344
+ .locator('[data-sonner-toast]')
345
+ .filter({ has: modelerCreatePage.viewProcessInstanceLink });
346
+ const dismissButton = instanceStartedToast.getByRole('button', {
347
+ name: 'Dismiss',
348
+ });
349
+ if (await dismissButton.isVisible().catch(() => false)) {
350
+ await dismissButton.click({ timeout: 10000 });
351
+ }
315
352
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeHidden({
316
353
  timeout: 60000,
317
354
  });
@@ -383,24 +420,27 @@ async function clickInvitationLinkInEmail(page, id) {
383
420
  }
384
421
  exports.clickInvitationLinkInEmail = clickInvitationLinkInEmail;
385
422
  async function assertLatestAlertEmail(id, mailSlurp, processName, alertText) {
386
- const maxRetries = 5;
387
- for (let retries = 0; retries < maxRetries; retries++) {
388
- try {
389
- const email = await mailSlurp.waitForLatestEmail(id, 120000);
390
- if (email && email.body) {
391
- (0, test_1.expect)(email.body).toContain(processName);
392
- (0, test_1.expect)(email.body).toContain(alertText);
393
- return email.body;
394
- }
395
- else {
396
- throw new Error('Email or email body is null or undefined.');
423
+ // waitForLatestEmail returns the newest email at once when the inbox is not
424
+ // empty, so a second call on the same inbox re-reads the first alert email.
425
+ // Search the inbox for an email that carries the expected text instead.
426
+ let body;
427
+ await (0, test_1.expect)(async () => {
428
+ const previews = await mailSlurp.getEmails(id, {
429
+ minCount: 1,
430
+ retryTimeout: 60000,
431
+ });
432
+ for (const preview of previews) {
433
+ const email = await mailSlurp.getEmail(preview.id);
434
+ if (email?.body?.includes(processName) &&
435
+ email.body.includes(alertText)) {
436
+ body = email.body;
437
+ return;
397
438
  }
398
439
  }
399
- catch (error) {
400
- console.warn(`Click attempt ${retries + 1} failed: ${error}`);
401
- }
402
- }
403
- throw new Error(`Failed to get latest alert (${processName}) email for ${id} after ${maxRetries} attempts.`);
440
+ throw new Error(`No alert email for "${processName}" containing "${alertText}" in ` +
441
+ `inbox ${id} yet (${previews.length} email(s) present)`);
442
+ }).toPass({ timeout: 360000, intervals: [5000, 10000, 20000] });
443
+ return body;
404
444
  }
405
445
  exports.assertLatestAlertEmail = assertLatestAlertEmail;
406
446
  async function assertTestUsesCorrectOrganization(page) {
@@ -0,0 +1,21 @@
1
+ import { Page, Locator } from '@playwright/test';
2
+ type WorkspaceRole = 'Workspace Admin' | 'Editor' | 'Commenter' | 'Viewer';
3
+ declare class WorkspaceMembersPage {
4
+ private page;
5
+ readonly membersTab: Locator;
6
+ readonly manageMembersHeading: Locator;
7
+ readonly addMembersButton: Locator;
8
+ readonly addMemberSearchbox: Locator;
9
+ readonly addMemberSubmitButton: Locator;
10
+ constructor(page: Page);
11
+ showAllWorkspaces(): Promise<void>;
12
+ openManageMembers(workspaceName: string): Promise<void>;
13
+ workspaceId(): string;
14
+ private memberRow;
15
+ isMember(email: string): Promise<boolean>;
16
+ addMember(email: string, role: WorkspaceRole): Promise<void>;
17
+ private selectRoleIn;
18
+ setMemberRole(email: string, role: WorkspaceRole): Promise<void>;
19
+ removeMember(email: string): Promise<void>;
20
+ }
21
+ export { WorkspaceMembersPage, WorkspaceRole };