@camunda/e2e-test-suite 0.0.1252 → 0.0.1253
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.7/AppsPage.d.ts +1 -0
- package/dist/pages/8.7/AppsPage.js +23 -4
- package/dist/pages/8.7/ClusterDetailsPage.js +14 -2
- package/dist/pages/8.7/ClusterPage.js +8 -0
- package/dist/pages/8.7/ConnectorMarketplacePage.d.ts +1 -0
- package/dist/pages/8.7/ConnectorMarketplacePage.js +24 -2
- package/dist/pages/8.7/ConsoleOrganizationPage.js +11 -0
- package/dist/pages/8.7/FormJsPage.js +30 -4
- package/dist/pages/8.7/ModelerCreatePage.js +33 -1
- package/dist/pages/8.7/ModelerHomePage.d.ts +2 -0
- package/dist/pages/8.7/ModelerHomePage.js +18 -0
- package/dist/pages/8.7/ProcessApplicationPage.d.ts +1 -0
- package/dist/pages/8.7/ProcessApplicationPage.js +87 -0
- package/dist/pages/8.7/UtilitiesPage.js +7 -1
- package/dist/tests/8.7/web-modeler-user-flows.spec.js +3 -15
- package/dist/utils/modelerCluster.d.ts +13 -0
- package/dist/utils/modelerCluster.js +2 -1
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@ declare class AppsPage {
|
|
|
20
20
|
clickCluster(component: Locator, name: string): Promise<void>;
|
|
21
21
|
private expandHubClusterGroup;
|
|
22
22
|
private clickHubClusterApp;
|
|
23
|
+
private hubAppLink;
|
|
23
24
|
isHubClusterAppPresent(clusterName: string, appName: string): Promise<boolean>;
|
|
24
25
|
clickModeler(): Promise<void>;
|
|
25
26
|
clickTasklist(clusterName: string): Promise<void>;
|
|
@@ -149,12 +149,31 @@ class AppsPage {
|
|
|
149
149
|
// additional content from the design-system sidebar item, e.g. an icon
|
|
150
150
|
// or badge). The five app names are distinct enough that a substring
|
|
151
151
|
// match carries no ambiguity risk.
|
|
152
|
-
const appLink = this.
|
|
153
|
-
name: appName,
|
|
154
|
-
});
|
|
152
|
+
const appLink = this.hubAppLink(clusterName, appName);
|
|
155
153
|
await (0, test_1.expect)(appLink).toBeVisible({ timeout: 30000 });
|
|
154
|
+
// With Hub environments enabled the sidebar lists environments, whose
|
|
155
|
+
// app links open in a new tab ("Operate in <env> (opens in a new tab)").
|
|
156
|
+
// Callers assert the app on this page, so follow the link here instead.
|
|
157
|
+
const href = await appLink.getAttribute('href');
|
|
158
|
+
if (href && (await appLink.getAttribute('target')) === '_blank') {
|
|
159
|
+
await this.page.goto(href, { waitUntil: 'domcontentloaded' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
156
162
|
await appLink.click({ timeout: 10000 });
|
|
157
163
|
}
|
|
164
|
+
// Inline (expanded rail) the app link carries the app's name. The rail can
|
|
165
|
+
// also start collapsed (camunda-hub seeds SidebarProvider's defaultExpanded
|
|
166
|
+
// from whether its items had loaded at first render); the group toggle then
|
|
167
|
+
// opens a flyout portalled outside "Main navigation", whose links -- like
|
|
168
|
+
// environment rows' -- are named "<app> in <cluster>".
|
|
169
|
+
hubAppLink(clusterName, appName) {
|
|
170
|
+
return this.hubSidebar
|
|
171
|
+
.getByRole('link', { name: appName })
|
|
172
|
+
.or(this.page.getByRole('link', {
|
|
173
|
+
name: new RegExp(`^${escapeRegExp(appName)} in ${escapeRegExp(clusterName)}\\b`),
|
|
174
|
+
}))
|
|
175
|
+
.first();
|
|
176
|
+
}
|
|
158
177
|
// Checks whether an app link is present inside the requested cluster's Hub
|
|
159
178
|
// sidebar group, without clicking it. Unlike clickHubClusterApp, absence is
|
|
160
179
|
// an expected, valid outcome here -- this is how callers assert per-cluster
|
|
@@ -163,7 +182,7 @@ class AppsPage {
|
|
|
163
182
|
// rather than a disabled control -- so this never throws on a miss.
|
|
164
183
|
async isHubClusterAppPresent(clusterName, appName) {
|
|
165
184
|
await this.expandHubClusterGroup(clusterName);
|
|
166
|
-
const appLink = this.
|
|
185
|
+
const appLink = this.hubAppLink(clusterName, appName);
|
|
167
186
|
return appLink.isVisible({ timeout: 10000 }).catch(() => false);
|
|
168
187
|
}
|
|
169
188
|
async clickModeler() {
|
|
@@ -78,9 +78,15 @@ class ClusterDetailsPage {
|
|
|
78
78
|
.locator('label')
|
|
79
79
|
.filter({ hasText: /^Secrets$/ });
|
|
80
80
|
this.createButton = page.getByRole('button', { name: 'Create', exact: true });
|
|
81
|
+
// The Hub shell's dialog names its close button through visually hidden
|
|
82
|
+
// text rather than an aria-label, which getByLabel does not see.
|
|
81
83
|
this.closeModalButton = page
|
|
82
84
|
.getByRole('dialog', { name: 'Client credentials', exact: true })
|
|
83
|
-
.
|
|
85
|
+
.getByRole('button', { name: 'Close', exact: true })
|
|
86
|
+
.or(page
|
|
87
|
+
.getByRole('dialog', { name: 'Client credentials', exact: true })
|
|
88
|
+
.getByLabel('Close'))
|
|
89
|
+
.first();
|
|
84
90
|
this.createClientButton = page.getByRole('button', {
|
|
85
91
|
name: 'Create new Client',
|
|
86
92
|
});
|
|
@@ -446,7 +452,13 @@ class ClusterDetailsPage {
|
|
|
446
452
|
timeout: 20000,
|
|
447
453
|
});
|
|
448
454
|
await (0, test_1.expect)(this.clientCredentialsDialog.getByText('The Client Secret will not be shown again.')).toBeVisible();
|
|
449
|
-
|
|
455
|
+
// The Hub shell's credentials dialog is modal and marks the page behind it
|
|
456
|
+
// aria-hidden, so the new client's row is out of the accessibility tree
|
|
457
|
+
// until it closes -- include hidden rows to check its scopes meanwhile.
|
|
458
|
+
await (0, test_1.expect)(this.page
|
|
459
|
+
.getByRole('row', { includeHidden: true })
|
|
460
|
+
.filter({ hasNotText: 'Scopes' })
|
|
461
|
+
.filter({ hasText: name })).toContainText('Zeebe, Tasklist, Operate, Optimize, and Secrets');
|
|
450
462
|
}
|
|
451
463
|
async clickEnvVarsButton() {
|
|
452
464
|
await (0, test_1.expect)(this.envVarsTab).toBeVisible({ timeout: 40000 });
|
|
@@ -144,6 +144,14 @@ class ClusterPage {
|
|
|
144
144
|
}
|
|
145
145
|
}
|
|
146
146
|
async deleteCluster(name) {
|
|
147
|
+
// A full reload in the Hub shell bounces through /login, the identity
|
|
148
|
+
// provider and /login-callback (~35s) before the SPA pushes its returnUrl
|
|
149
|
+
// back to /clusters. Counting rows mid-bounce throws "Execution context
|
|
150
|
+
// was destroyed", so wait for the clusters page to have settled first.
|
|
151
|
+
await this.page.waitForURL((url) => /\/clusters\/?$/.test(url.pathname), {
|
|
152
|
+
timeout: 120000,
|
|
153
|
+
});
|
|
154
|
+
await (0, test_1.expect)(this.createNewClusterButton).toBeVisible({ timeout: 60000 });
|
|
147
155
|
const clusterInstance = this.cluster(name).first();
|
|
148
156
|
if ((await clusterInstance.count()) <= 0) {
|
|
149
157
|
console.log(`No clusters found with name ${name}, skipping deletion.`);
|
|
@@ -8,6 +8,7 @@ declare class ConnectorMarketplacePage {
|
|
|
8
8
|
readonly addToProjectButton: Locator;
|
|
9
9
|
readonly cancelButton: Locator;
|
|
10
10
|
readonly closeButton: Locator;
|
|
11
|
+
readonly browseAllMarketplaceDetailsButton: Locator;
|
|
11
12
|
constructor(page: Page);
|
|
12
13
|
clickSearchForConnectorTextbox(): Promise<void>;
|
|
13
14
|
fillSearchForConnectorTextbox(connectorName: string): Promise<void>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectorMarketplacePage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
4
5
|
class ConnectorMarketplacePage {
|
|
5
6
|
page;
|
|
6
7
|
searchForConnectorTextbox;
|
|
@@ -10,9 +11,22 @@ class ConnectorMarketplacePage {
|
|
|
10
11
|
addToProjectButton;
|
|
11
12
|
cancelButton;
|
|
12
13
|
closeButton;
|
|
14
|
+
browseAllMarketplaceDetailsButton;
|
|
13
15
|
constructor(page) {
|
|
14
16
|
this.page = page;
|
|
15
|
-
|
|
17
|
+
// The legacy marketplace modal, or camunda-hub's "Browse all" dialog
|
|
18
|
+
// (2026-09-23), which lists Marketplace connectors alongside the
|
|
19
|
+
// diagram's own elements and reusable assets.
|
|
20
|
+
this.searchForConnectorTextbox = page
|
|
21
|
+
.getByPlaceholder('Search for a connector')
|
|
22
|
+
.or(page.locator('#browse-all-search'))
|
|
23
|
+
.first();
|
|
24
|
+
// A Browse all Marketplace card opens its review step ("Add to project")
|
|
25
|
+
// through this button; self-hosted connectors only link to setup docs.
|
|
26
|
+
this.browseAllMarketplaceDetailsButton = page
|
|
27
|
+
.getByRole('list', { name: 'Marketplace connectors' })
|
|
28
|
+
.getByRole('button', { name: /^View details for .+ from Marketplace$/ })
|
|
29
|
+
.first();
|
|
16
30
|
this.downloadToProjectButton = page
|
|
17
31
|
.getByRole('button', { name: 'Download to project' })
|
|
18
32
|
.first();
|
|
@@ -42,7 +56,15 @@ class ConnectorMarketplacePage {
|
|
|
42
56
|
await this.replaceResourceButton.click({ timeout: 60000 });
|
|
43
57
|
}
|
|
44
58
|
async downloadConnectorToProject() {
|
|
45
|
-
await this.
|
|
59
|
+
await (0, test_1.expect)(this.downloadToProjectButton
|
|
60
|
+
.or(this.browseAllMarketplaceDetailsButton)
|
|
61
|
+
.first()).toBeVisible({ timeout: 60000 });
|
|
62
|
+
if (await this.downloadToProjectButton.isVisible()) {
|
|
63
|
+
await this.clickDownloadToProjectButton();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await this.browseAllMarketplaceDetailsButton.click({ timeout: 30000 });
|
|
67
|
+
}
|
|
46
68
|
let imported = false;
|
|
47
69
|
// A fresh download shows "Add to project"; an already-present resource
|
|
48
70
|
// shows "Replace resource". Pick whichever the ImportModal rendered —
|
|
@@ -515,6 +515,17 @@ class ConsoleOrganizationPage {
|
|
|
515
515
|
// to actually drop before targeting the next row: clicking a delete button
|
|
516
516
|
// while its row is being removed detaches the element from the DOM and the
|
|
517
517
|
// click never lands (retries against a continuously re-rendering table).
|
|
518
|
+
//
|
|
519
|
+
// The table fetches its rows after the tab renders, so sampling the count
|
|
520
|
+
// straight away read 0 while the previous run's groups were still loading
|
|
521
|
+
// -- they were left in place and re-creating them failed "name:duplicate".
|
|
522
|
+
// Give existing rows the chance to render before counting.
|
|
523
|
+
await (0, test_1.expect)(this.createGroupButton).toBeVisible({ timeout: 60000 });
|
|
524
|
+
await this.deleteButton
|
|
525
|
+
.first()
|
|
526
|
+
.waitFor({ state: 'visible', timeout: 20000 })
|
|
527
|
+
.then(() => true)
|
|
528
|
+
.catch(() => false);
|
|
518
529
|
let remaining = await this.deleteButton.count();
|
|
519
530
|
while (remaining > 0) {
|
|
520
531
|
await this.deleteButton.first().click({ timeout: 60000 });
|
|
@@ -4,6 +4,7 @@ exports.FormJsPage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
7
|
+
const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
7
8
|
class FormJsPage {
|
|
8
9
|
page;
|
|
9
10
|
processApplicationPage;
|
|
@@ -50,15 +51,16 @@ class FormJsPage {
|
|
|
50
51
|
// Inside a process application hub titles this modal for the application
|
|
51
52
|
// rather than for the form, so 'Deploy form' never matches and the dialog
|
|
52
53
|
// reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
// Role-based rather than Carbon's modal container: under the Hub shell the
|
|
55
|
+
// deploy dialog is a design-system dialog with no .cds--modal-container.
|
|
56
|
+
this.dialog = page.getByRole('dialog').filter({ hasText: 'Deploy' });
|
|
56
57
|
this.deploySubButton = this.dialog.getByRole('button', {
|
|
57
58
|
name: 'Deploy',
|
|
58
59
|
exact: true,
|
|
59
60
|
});
|
|
61
|
+
// Carbon toast, or the design system's (sonner) Toaster under the Hub shell.
|
|
60
62
|
this.deploySuccessNotification = page
|
|
61
|
-
.locator('.cds--toast-notification--success')
|
|
63
|
+
.locator('.cds--toast-notification--success, [data-sonner-toast][data-type="success"]')
|
|
62
64
|
.filter({ hasText: 'Form deployed!' });
|
|
63
65
|
}
|
|
64
66
|
async generateAIForm(request = 'Create a form with the following fields: 1. A text field with the label "Full Name" 2. A number with the label "Count" 3. A date input with the label "Date of birth"4. A Checkbox with the label "Agree"') {
|
|
@@ -119,6 +121,21 @@ class FormJsPage {
|
|
|
119
121
|
throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
|
|
120
122
|
'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
|
|
121
123
|
}
|
|
124
|
+
// Hub's environment-based dialog also renders a <label> per row, which
|
|
125
|
+
// the legacy toggle detection below would mistake for its own.
|
|
126
|
+
const environmentRow = this.dialog
|
|
127
|
+
.locator('[data-test^="environment-row-"]')
|
|
128
|
+
.first();
|
|
129
|
+
if (await environmentRow.isVisible().catch(() => false)) {
|
|
130
|
+
try {
|
|
131
|
+
await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
|
|
132
|
+
}
|
|
133
|
+
catch (error) {
|
|
134
|
+
this.processApplicationPage.forgetStageCluster();
|
|
135
|
+
throw error;
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
122
139
|
const legacyClusterToggle = this.dialog.locator('label').filter({
|
|
123
140
|
hasText: new RegExp(`^${clusterName}`),
|
|
124
141
|
});
|
|
@@ -143,6 +160,15 @@ class FormJsPage {
|
|
|
143
160
|
throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
|
|
144
161
|
'binding did not take. It will be rebound on the next attempt.');
|
|
145
162
|
}
|
|
163
|
+
// Hub's environment-based dialog lists every Environment assigned to
|
|
164
|
+
// the workspace and preselects the first, which need not be this one.
|
|
165
|
+
const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
|
|
166
|
+
if (await environmentRows
|
|
167
|
+
.first()
|
|
168
|
+
.isVisible()
|
|
169
|
+
.catch(() => false)) {
|
|
170
|
+
await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
|
|
171
|
+
}
|
|
146
172
|
return;
|
|
147
173
|
}
|
|
148
174
|
const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?HealthydevManage`);
|
|
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
|
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
7
7
|
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
8
|
+
const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
8
9
|
class ModelerCreatePage {
|
|
9
10
|
page;
|
|
10
11
|
processApplicationPage;
|
|
@@ -212,7 +213,14 @@ class ModelerCreatePage {
|
|
|
212
213
|
this.firstPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
|
|
213
214
|
this.secondPlacedElement = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
|
|
214
215
|
this.payloadInput = page.locator('[class="fjs-input"]');
|
|
215
|
-
|
|
216
|
+
// Since camunda-hub's "Browse all" (2026-09-23) a change-element menu with
|
|
217
|
+
// enough entries swaps its header's marketplace icon for a "Browse all"
|
|
218
|
+
// entry, whose dialog carries the Marketplace search and import itself
|
|
219
|
+
// (see ConnectorMarketplacePage).
|
|
220
|
+
this.marketPlaceButton = page
|
|
221
|
+
.getByTitle('Browse Marketplace for more Connectors')
|
|
222
|
+
.or(page.getByTitle('Browse all available elements and assets'))
|
|
223
|
+
.first();
|
|
216
224
|
this.publicHolidayConnectorOption = page
|
|
217
225
|
.locator('[data-test="modeler"]')
|
|
218
226
|
.getByText('Worldwide Public Holiday');
|
|
@@ -1638,6 +1646,21 @@ class ModelerCreatePage {
|
|
|
1638
1646
|
throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
|
|
1639
1647
|
'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
|
|
1640
1648
|
}
|
|
1649
|
+
// Hub's environment-based dialog also renders a <label> per row, which
|
|
1650
|
+
// the legacy toggle detection below would mistake for its own.
|
|
1651
|
+
const environmentRow = this.dialog
|
|
1652
|
+
.locator('[data-test^="environment-row-"]')
|
|
1653
|
+
.first();
|
|
1654
|
+
if (await environmentRow.isVisible().catch(() => false)) {
|
|
1655
|
+
try {
|
|
1656
|
+
await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
|
|
1657
|
+
}
|
|
1658
|
+
catch (error) {
|
|
1659
|
+
this.processApplicationPage.forgetStageCluster();
|
|
1660
|
+
throw error;
|
|
1661
|
+
}
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1641
1664
|
const legacyClusterToggle = this.dialog.locator('label').filter({
|
|
1642
1665
|
hasText: new RegExp(`^${clusterName}`),
|
|
1643
1666
|
});
|
|
@@ -1662,6 +1685,15 @@ class ModelerCreatePage {
|
|
|
1662
1685
|
throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
|
|
1663
1686
|
'binding did not take. It will be rebound on the next attempt.');
|
|
1664
1687
|
}
|
|
1688
|
+
// Hub's environment-based dialog lists every Environment assigned to
|
|
1689
|
+
// the workspace and preselects the first, which need not be this one.
|
|
1690
|
+
const environmentRows = this.dialog.locator('[data-test^="environment-row-"]');
|
|
1691
|
+
if (await environmentRows
|
|
1692
|
+
.first()
|
|
1693
|
+
.isVisible()
|
|
1694
|
+
.catch(() => false)) {
|
|
1695
|
+
await (0, modelerCluster_1.selectDeployEnvironment)(this.dialog, clusterName);
|
|
1696
|
+
}
|
|
1665
1697
|
return;
|
|
1666
1698
|
}
|
|
1667
1699
|
const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
|
|
@@ -26,6 +26,7 @@ declare class ModelerHomePage {
|
|
|
26
26
|
readonly projectBreadcrumb: Locator;
|
|
27
27
|
readonly workspaceBreadcrumb: Locator;
|
|
28
28
|
readonly openOrganizationsButton: Locator;
|
|
29
|
+
readonly hubOrganizationLink: Locator;
|
|
29
30
|
readonly optionsButton: Locator;
|
|
30
31
|
readonly manageButton: Locator;
|
|
31
32
|
readonly crossComponentProjectFolder: Locator;
|
|
@@ -71,6 +72,7 @@ declare class ModelerHomePage {
|
|
|
71
72
|
private becameVisible;
|
|
72
73
|
isModelerPageBannerVisible(timeout: number): Promise<boolean>;
|
|
73
74
|
clickHomeBreadcrumb(): Promise<void>;
|
|
75
|
+
openOrganizationManagement(timeout: number): Promise<void>;
|
|
74
76
|
clickOpenOrganizationsButton(): Promise<void>;
|
|
75
77
|
clickCrossComponentProjectFolder(): Promise<void>;
|
|
76
78
|
private createWorkspaceViaWizard;
|
|
@@ -47,6 +47,7 @@ class ModelerHomePage {
|
|
|
47
47
|
projectBreadcrumb;
|
|
48
48
|
workspaceBreadcrumb;
|
|
49
49
|
openOrganizationsButton;
|
|
50
|
+
hubOrganizationLink;
|
|
50
51
|
optionsButton;
|
|
51
52
|
manageButton;
|
|
52
53
|
crossComponentProjectFolder;
|
|
@@ -143,6 +144,10 @@ class ModelerHomePage {
|
|
|
143
144
|
.first();
|
|
144
145
|
this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
|
|
145
146
|
this.openOrganizationsButton = page.getByLabel('Open Organizations');
|
|
147
|
+
this.hubOrganizationLink = page
|
|
148
|
+
.getByRole('navigation', { name: 'Main navigation' })
|
|
149
|
+
.getByRole('link', { name: 'Organization' })
|
|
150
|
+
.first();
|
|
146
151
|
this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
|
|
147
152
|
// On the Camunda Hub Workspaces overview the folder is a workspace tile
|
|
148
153
|
// whose "Open workspace <name>" button navigates into it.
|
|
@@ -561,6 +566,19 @@ class ModelerHomePage {
|
|
|
561
566
|
// An announcement dialog covering the Modeler header makes every click on the
|
|
562
567
|
// org button bounce off the modal layer ("intercepts pointer events") until
|
|
563
568
|
// the timeout, so dismiss overlays before each attempt.
|
|
569
|
+
// Opens organization management. The Camunda Hub shell has no "Open
|
|
570
|
+
// Organizations" menu -- its org breadcrumb only switches organizations --
|
|
571
|
+
// and reaches organization management from the sidebar's "Organization"
|
|
572
|
+
// link instead.
|
|
573
|
+
async openOrganizationManagement(timeout) {
|
|
574
|
+
await (0, test_1.expect)(this.openOrganizationsButton.or(this.hubOrganizationLink).first()).toBeVisible({ timeout });
|
|
575
|
+
if (await this.hubOrganizationLink.isVisible()) {
|
|
576
|
+
await this.hubOrganizationLink.click({ timeout: 30000 });
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
await this.clickOpenOrganizationsButton();
|
|
580
|
+
await this.clickManageButton();
|
|
581
|
+
}
|
|
564
582
|
async clickOpenOrganizationsButton() {
|
|
565
583
|
await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.openOrganizationsButton, {
|
|
566
584
|
preAction: async () => {
|
|
@@ -24,6 +24,7 @@ declare class ProcessApplicationPage {
|
|
|
24
24
|
rememberResourceContainer(url: string): void;
|
|
25
25
|
forgetStageCluster(): void;
|
|
26
26
|
ensureStageCluster(clusterName: string): Promise<boolean>;
|
|
27
|
+
private ensureWorkspaceEnvironment;
|
|
27
28
|
private openStageDefinition;
|
|
28
29
|
openByName(name: string): Promise<void>;
|
|
29
30
|
}
|
|
@@ -237,7 +237,19 @@ class ProcessApplicationPage {
|
|
|
237
237
|
// suite is. Re-navigating is the recovery; waiting longer on a page that
|
|
238
238
|
// has already gone wrong is not, which is why the inner timeouts stay
|
|
239
239
|
// short. A genuinely unbindable cluster still fails, with the same error.
|
|
240
|
+
//
|
|
241
|
+
// Under Hub workspaces (/workspaces/<ws>/projects/<id>) there is no
|
|
242
|
+
// /connected-clusters route -- it renders "No match for the current
|
|
243
|
+
// location". With Hub's Environments feature on (camunda-hub#29210),
|
|
244
|
+
// WorkspaceProjectDetail hides the Connected clusters card altogether and
|
|
245
|
+
// the deploy dialog offers the environments assigned to the parent
|
|
246
|
+
// workspace instead, so the binding is made there.
|
|
247
|
+
const workspaceId = paUrl.match(/\/workspaces\/([^/]+)\/projects\//)?.[1];
|
|
240
248
|
await (0, test_1.expect)(async () => {
|
|
249
|
+
if (workspaceId) {
|
|
250
|
+
await this.ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
241
253
|
await this.page.goto(`${paUrl}/connected-clusters`, {
|
|
242
254
|
waitUntil: 'domcontentloaded',
|
|
243
255
|
});
|
|
@@ -248,6 +260,81 @@ class ProcessApplicationPage {
|
|
|
248
260
|
await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
|
|
249
261
|
return true;
|
|
250
262
|
}
|
|
263
|
+
// Assigns the Environment backed by clusterName to the workspace, from
|
|
264
|
+
// workspace management's Environments tab (camunda-hub
|
|
265
|
+
// WorkspaceEnvironmentsTab). Without the Environments feature the project
|
|
266
|
+
// page still carries the Connected clusters card, whose "Configure" button
|
|
267
|
+
// opens the Define stages dialog -- that path is taken instead.
|
|
268
|
+
//
|
|
269
|
+
// Save replaces the workspace's whole assignment with the picker's
|
|
270
|
+
// selection, and parallel workers share this workspace, so only this
|
|
271
|
+
// cluster's toggle is touched and the result re-read afterwards. A
|
|
272
|
+
// concurrent save that drops it anyway surfaces as a deploy dialog without
|
|
273
|
+
// the cluster, which forgets the binding and comes back here (see
|
|
274
|
+
// forgetStageCluster).
|
|
275
|
+
async ensureWorkspaceEnvironment(paUrl, workspaceId, clusterName) {
|
|
276
|
+
await this.page.goto(paUrl, { waitUntil: 'domcontentloaded' });
|
|
277
|
+
await this.waitForProcessApplication();
|
|
278
|
+
const configure = this.page.getByRole('button', {
|
|
279
|
+
name: 'Configure',
|
|
280
|
+
exact: true,
|
|
281
|
+
});
|
|
282
|
+
if (await configure.isVisible().catch(() => false)) {
|
|
283
|
+
await configure.click({ timeout: 30000 });
|
|
284
|
+
await this.defineDevelopmentStage(clusterName);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
const manageUrl = `${new URL(paUrl).origin}/workspaces/${workspaceId}/manage?tab=environments`;
|
|
288
|
+
await this.page.goto(manageUrl, { waitUntil: 'domcontentloaded' });
|
|
289
|
+
const heading = this.page.locator('[data-test="manage-environments-heading"]');
|
|
290
|
+
if (!(await heading
|
|
291
|
+
.waitFor({ state: 'visible', timeout: 60000 })
|
|
292
|
+
.then(() => true)
|
|
293
|
+
.catch(() => false))) {
|
|
294
|
+
throw new Error(`Could not bind "${clusterName}" as a deploy target: the project page ` +
|
|
295
|
+
'has no Connected clusters "Configure" control, and the workspace ' +
|
|
296
|
+
`Environments tab did not render at ${manageUrl}.`);
|
|
297
|
+
}
|
|
298
|
+
const editButton = this.page.locator('[data-test="edit-environments-button"]');
|
|
299
|
+
await (0, test_1.expect)(editButton).toBeEnabled({ timeout: 60000 });
|
|
300
|
+
await editButton.click({ timeout: 30000 });
|
|
301
|
+
const editor = this.page.getByRole('group', {
|
|
302
|
+
name: 'Edit assigned environments',
|
|
303
|
+
});
|
|
304
|
+
await (0, test_1.expect)(editor).toBeVisible({ timeout: 30000 });
|
|
305
|
+
const search = editor.locator('[data-test="picker-search"]');
|
|
306
|
+
if (await search.isVisible().catch(() => false)) {
|
|
307
|
+
await search.fill(clusterName);
|
|
308
|
+
}
|
|
309
|
+
// Picker card toggles are named "Select <environment> (<cluster>)" and
|
|
310
|
+
// flip to "Selected ..." with aria-pressed once picked.
|
|
311
|
+
const escapedClusterName = clusterName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
312
|
+
const toggle = editor
|
|
313
|
+
.locator('[data-test="picker-environment"]')
|
|
314
|
+
.getByRole('button', {
|
|
315
|
+
name: new RegExp(`^Select(ed)? .*\\(${escapedClusterName}\\)$`),
|
|
316
|
+
})
|
|
317
|
+
.first();
|
|
318
|
+
await (0, test_1.expect)(toggle).toBeVisible({ timeout: 60000 });
|
|
319
|
+
if ((await toggle.getAttribute('aria-pressed')) === 'true') {
|
|
320
|
+
await this.page
|
|
321
|
+
.getByRole('button', { name: 'Cancel', exact: true })
|
|
322
|
+
.click({ timeout: 30000 });
|
|
323
|
+
await (0, test_1.expect)(editor).toBeHidden({ timeout: 30000 });
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
await toggle.click({ timeout: 30000 });
|
|
327
|
+
await (0, test_1.expect)(toggle).toHaveAttribute('aria-pressed', 'true');
|
|
328
|
+
await this.page
|
|
329
|
+
.getByRole('button', { name: 'Save', exact: true })
|
|
330
|
+
.click({ timeout: 30000 });
|
|
331
|
+
await (0, test_1.expect)(editor).toBeHidden({ timeout: 60000 });
|
|
332
|
+
await (0, test_1.expect)(this.page
|
|
333
|
+
.getByRole('list', { name: 'Assigned environments' })
|
|
334
|
+
.getByRole('listitem')
|
|
335
|
+
.filter({ hasText: clusterName })
|
|
336
|
+
.first()).toBeVisible({ timeout: 60000 });
|
|
337
|
+
}
|
|
251
338
|
// The /connected-clusters deep link opens the Define stages dialog by itself
|
|
252
339
|
// while the application has no stage bound. Once one is bound -- a test that
|
|
253
340
|
// deploys to a second cluster -- it has to be reopened from the page, so try
|
|
@@ -556,7 +556,13 @@ async function modelAndRunConnectorsDocHandlingDiagram(modelerCreatePage, modele
|
|
|
556
556
|
await modelerCreatePage.clickMarketPlaceButton();
|
|
557
557
|
await connectorMarketplacePage.clickSearchForConnectorTextbox();
|
|
558
558
|
await connectorMarketplacePage.fillSearchForConnectorTextbox('REST Connector');
|
|
559
|
-
|
|
559
|
+
// The legacy marketplace modal's card, or its Browse all dialog card.
|
|
560
|
+
await (0, test_1.expect)(page
|
|
561
|
+
.getByText('REST ConnectorBy CamundaConnect, interact, and synchronize processes')
|
|
562
|
+
.or(page
|
|
563
|
+
.getByRole('list', { name: 'Marketplace connectors' })
|
|
564
|
+
.getByText('REST Connector', { exact: true }))
|
|
565
|
+
.first()).toBeVisible({
|
|
560
566
|
timeout: 60000,
|
|
561
567
|
});
|
|
562
568
|
await connectorMarketplacePage.downloadConnectorToProject();
|
|
@@ -106,11 +106,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
106
106
|
await modelerHomePage.clickMessageBanner();
|
|
107
107
|
});
|
|
108
108
|
await _8_7_1.test.step('Change User Roles and Assert This Updates in Web Modeler', async () => {
|
|
109
|
-
await
|
|
110
|
-
timeout: 120000,
|
|
111
|
-
});
|
|
112
|
-
await modelerHomePage.clickOpenOrganizationsButton();
|
|
113
|
-
await modelerHomePage.clickManageButton();
|
|
109
|
+
await modelerHomePage.openOrganizationManagement(120000);
|
|
114
110
|
await consoleOrganizationsPage.clickUsersTab();
|
|
115
111
|
await consoleOrganizationsPage.filterTable([
|
|
116
112
|
process.env.C8_USERNAME_TEST_3,
|
|
@@ -174,11 +170,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
174
170
|
timeout: 90000,
|
|
175
171
|
});
|
|
176
172
|
await modelerHomePage.clickMessageBanner();
|
|
177
|
-
await
|
|
178
|
-
timeout: 120000,
|
|
179
|
-
});
|
|
180
|
-
await modelerHomePage.clickOpenOrganizationsButton();
|
|
181
|
-
await modelerHomePage.clickManageButton();
|
|
173
|
+
await modelerHomePage.openOrganizationManagement(120000);
|
|
182
174
|
await consoleOrganizationsPage.clickUsersTab();
|
|
183
175
|
await consoleOrganizationsPage.filterTable([
|
|
184
176
|
process.env.C8_USERNAME_TEST_3,
|
|
@@ -234,11 +226,7 @@ _8_7_1.test.describe('Web Modeler User Flow Tests', () => {
|
|
|
234
226
|
timeout: 90000,
|
|
235
227
|
});
|
|
236
228
|
await modelerHomePage.clickMessageBanner();
|
|
237
|
-
await
|
|
238
|
-
timeout: 60000,
|
|
239
|
-
});
|
|
240
|
-
await modelerHomePage.clickOpenOrganizationsButton();
|
|
241
|
-
await modelerHomePage.clickManageButton();
|
|
229
|
+
await modelerHomePage.openOrganizationManagement(60000);
|
|
242
230
|
await consoleOrganizationsPage.clickUsersTab();
|
|
243
231
|
await consoleOrganizationsPage.filterTable([
|
|
244
232
|
process.env.C8_USERNAME_TEST_3,
|
|
@@ -15,3 +15,16 @@ import { Locator } from '@playwright/test';
|
|
|
15
15
|
* preselected.
|
|
16
16
|
*/
|
|
17
17
|
export declare function selectModelerCluster(dialog: Locator): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Picks a row in the environment-based deploy dialog (camunda-hub#29210). Each row is
|
|
20
|
+
* `[data-test="environment-row-<id>"]`, one per Environment assigned to the deploy's workspace, and
|
|
21
|
+
* the dialog preselects the first one -- so with no cluster name to match, the preselection is left
|
|
22
|
+
* alone.
|
|
23
|
+
*
|
|
24
|
+
* This suite's single-cluster topology gives each cluster exactly one Environment, whose row shows
|
|
25
|
+
* no separate cluster subtext -- so the row's only visible identifying text is the Environment's
|
|
26
|
+
* own name, matched here on the assumption that it is named after its cluster (true for how this
|
|
27
|
+
* suite's clusters are registered). A row that does carry a distinct cluster subtext also matches,
|
|
28
|
+
* since `hasText` scans the whole row.
|
|
29
|
+
*/
|
|
30
|
+
export declare function selectDeployEnvironment(dialog: Locator, clusterName: string): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.selectModelerCluster = void 0;
|
|
3
|
+
exports.selectDeployEnvironment = exports.selectModelerCluster = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
/**
|
|
6
6
|
* Points a Web Modeler deploy dialog at the cluster named by MODELER_CLUSTER_NAME.
|
|
@@ -94,3 +94,4 @@ async function selectDeployEnvironment(dialog, clusterName) {
|
|
|
94
94
|
await target.getByRole('radio').click({ timeout: 30000 });
|
|
95
95
|
await (0, test_1.expect)(target).toContainText(clusterName);
|
|
96
96
|
}
|
|
97
|
+
exports.selectDeployEnvironment = selectDeployEnvironment;
|