@camunda/e2e-test-suite 0.0.1287 → 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.
- package/dist/pages/8.9/AppsPage.d.ts +9 -3
- package/dist/pages/8.9/AppsPage.js +262 -47
- package/dist/pages/8.9/ClusterDetailsPage.d.ts +5 -0
- package/dist/pages/8.9/ClusterDetailsPage.js +92 -9
- package/dist/pages/8.9/ClusterSecretsPage.js +5 -1
- package/dist/pages/8.9/ConnectorMarketplacePage.d.ts +1 -0
- package/dist/pages/8.9/ConnectorMarketplacePage.js +46 -3
- package/dist/pages/8.9/ConsoleOrganizationPage.d.ts +3 -2
- package/dist/pages/8.9/ConsoleOrganizationPage.js +47 -22
- package/dist/pages/8.9/FormJsPage.d.ts +4 -0
- package/dist/pages/8.9/FormJsPage.js +53 -19
- package/dist/pages/8.9/IdpCreatePage.js +5 -0
- package/dist/pages/8.9/ModelerCreatePage.d.ts +5 -0
- package/dist/pages/8.9/ModelerCreatePage.js +153 -21
- package/dist/pages/8.9/ModelerHomePage.d.ts +1 -0
- package/dist/pages/8.9/ModelerHomePage.js +29 -4
- package/dist/pages/8.9/PlayPage.js +11 -1
- package/dist/pages/8.9/ProcessApplicationPage.js +48 -4
- package/dist/pages/8.9/UtilitiesPage.js +57 -17
- package/dist/pages/8.9/WorkspaceMembersPage.d.ts +21 -0
- package/dist/pages/8.9/WorkspaceMembersPage.js +264 -0
- package/dist/tests/8.9/access-user-role-management.spec.js +111 -26
- package/dist/tests/8.9/console-user-flows.spec.js +4 -4
- package/dist/tests/8.9/idp-user-flows.spec.js +2 -1
- package/dist/tests/8.9/rba-enabled-v1-user-flows.spec.js +13 -4
- package/dist/tests/8.9/rba-enabled-v2-user-flows.spec.js +13 -4
- package/dist/tests/8.9/test-setup.spec.js +57 -4
- package/dist/tests/8.9/utr-enabled-user-flows.spec.js +18 -0
- package/dist/tests/8.9/web-modeler-user-flows.spec.js +6 -2
- package/dist/utils/consoleApiHelpers.d.ts +2 -0
- package/dist/utils/consoleApiHelpers.js +50 -4
- package/dist/utils/resetSession.js +6 -1
- 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
|
-
|
|
168
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
1504
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
400
|
-
|
|
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 };
|