@camunda/e2e-test-suite 0.0.1250 → 0.0.1251
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/BrowseAllConnectorMarketplacePage.d.ts +17 -0
- package/dist/pages/BrowseAllConnectorMarketplacePage.js +145 -0
- package/dist/pages/SM-8.10/ClusterDetailsPage.d.ts +1 -1
- package/dist/pages/SM-8.10/ClusterDetailsPage.js +13 -6
- package/dist/pages/SM-8.10/ClusterPage.js +3 -1
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.d.ts +2 -0
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.js +16 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +4 -1
- package/dist/pages/SM-8.10/ModelerCreatePage.js +78 -51
- package/dist/pages/SM-8.10/ModelerHomePage.d.ts +2 -1
- package/dist/pages/SM-8.10/ModelerHomePage.js +24 -0
- package/dist/pages/SM-8.10/OperateHomePage.js +8 -1
- package/dist/pages/SM-8.10/OptimizeHomePage.js +13 -4
- package/dist/pages/SM-8.10/ProcessApplicationPage.d.ts +10 -5
- package/dist/pages/SM-8.10/ProcessApplicationPage.js +163 -68
- package/dist/pages/SM-8.10/UtilitiesPage.js +6 -6
- package/dist/pages/SM-8.7/NavigationPage.js +2 -1
- package/dist/tests/8.10/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.10/smoke-tests.spec.js +2 -1
- package/dist/tests/8.7/connectors-user-flows.spec.js +8 -4
- package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.7/smoke-tests.spec.js +2 -1
- package/dist/tests/8.8/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.8/smoke-tests.spec.js +2 -1
- package/dist/tests/8.9/connectors-user-flows.spec.js +4 -2
- package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +2 -1
- package/dist/tests/8.9/smoke-tests.spec.js +2 -1
- package/dist/tests/SM-8.10/cluster-management-user-flows.spec.js +13 -6
- package/dist/tests/SM-8.10/connectors-user-flows.spec.js +2 -1
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +3 -1
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.js +62 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +6 -3
- package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +6 -3
- package/dist/utils/modelerCluster.d.ts +4 -6
- package/dist/utils/modelerCluster.js +55 -32
- package/package.json +1 -1
- package/playwright.config.ts +18 -5
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { Locator, Page } from '@playwright/test';
|
|
2
|
+
declare class BrowseAllConnectorMarketplacePage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly searchForConnectorTextbox: Locator;
|
|
5
|
+
private readonly marketplaceConnectorList;
|
|
6
|
+
private readonly marketplaceConnectorCard;
|
|
7
|
+
private readonly marketplaceDetailFooter;
|
|
8
|
+
private readonly availableElementsHeading;
|
|
9
|
+
private readonly importModal;
|
|
10
|
+
constructor(page: Page);
|
|
11
|
+
isOpen(): Promise<boolean>;
|
|
12
|
+
waitForConnectorSearchResult(connectorName?: string): Promise<Locator>;
|
|
13
|
+
importConnectorToProject(connectorName?: string): Promise<void>;
|
|
14
|
+
private completeImportReviewIfNecessary;
|
|
15
|
+
private findConnectorCard;
|
|
16
|
+
}
|
|
17
|
+
export { BrowseAllConnectorMarketplacePage };
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.BrowseAllConnectorMarketplacePage = void 0;
|
|
4
|
+
class BrowseAllConnectorMarketplacePage {
|
|
5
|
+
page;
|
|
6
|
+
searchForConnectorTextbox;
|
|
7
|
+
marketplaceConnectorList;
|
|
8
|
+
marketplaceConnectorCard;
|
|
9
|
+
marketplaceDetailFooter;
|
|
10
|
+
availableElementsHeading;
|
|
11
|
+
importModal;
|
|
12
|
+
constructor(page) {
|
|
13
|
+
this.page = page;
|
|
14
|
+
this.searchForConnectorTextbox = page.getByRole('searchbox', {
|
|
15
|
+
name: 'Search elements and reusable assets',
|
|
16
|
+
});
|
|
17
|
+
this.marketplaceConnectorList = page.getByRole('list', {
|
|
18
|
+
name: 'Marketplace connectors',
|
|
19
|
+
});
|
|
20
|
+
this.marketplaceConnectorCard = this.marketplaceConnectorList
|
|
21
|
+
.getByRole('button', {
|
|
22
|
+
name: /^View details for .+ from Marketplace$/,
|
|
23
|
+
})
|
|
24
|
+
.first();
|
|
25
|
+
this.marketplaceDetailFooter = page.locator('[data-test="marketplace-detail-footer"]');
|
|
26
|
+
this.availableElementsHeading = page.getByRole('heading', {
|
|
27
|
+
name: 'Available elements',
|
|
28
|
+
});
|
|
29
|
+
this.importModal = page.locator('[data-test="import-modal"]');
|
|
30
|
+
}
|
|
31
|
+
async isOpen() {
|
|
32
|
+
return await this.searchForConnectorTextbox.isVisible();
|
|
33
|
+
}
|
|
34
|
+
async waitForConnectorSearchResult(connectorName) {
|
|
35
|
+
await this.marketplaceConnectorCard.waitFor({
|
|
36
|
+
state: 'visible',
|
|
37
|
+
timeout: 120000,
|
|
38
|
+
});
|
|
39
|
+
return await this.findConnectorCard(connectorName);
|
|
40
|
+
}
|
|
41
|
+
async importConnectorToProject(connectorName) {
|
|
42
|
+
const connectorCard = await this.waitForConnectorSearchResult(connectorName);
|
|
43
|
+
await connectorCard.click({ timeout: 60000 });
|
|
44
|
+
const browseAllDialog = this.page.getByRole('dialog').filter({
|
|
45
|
+
has: this.page.getByRole('button', { name: 'Back to all results' }),
|
|
46
|
+
});
|
|
47
|
+
await browseAllDialog.waitFor({ state: 'visible', timeout: 60000 });
|
|
48
|
+
const addButton = this.marketplaceDetailFooter.getByRole('button', {
|
|
49
|
+
name: /^(Add to project|Add remaining elements)$/,
|
|
50
|
+
});
|
|
51
|
+
const addedStatus = this.marketplaceDetailFooter.getByText('Added to this project', { exact: true });
|
|
52
|
+
const retryButton = this.marketplaceDetailFooter.getByRole('button', {
|
|
53
|
+
name: 'Try again',
|
|
54
|
+
});
|
|
55
|
+
const detailState = addButton.or(addedStatus).or(retryButton);
|
|
56
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
57
|
+
await detailState.waitFor({ state: 'visible', timeout: 120000 });
|
|
58
|
+
if (await addedStatus.isVisible()) {
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
if (await retryButton.isVisible()) {
|
|
62
|
+
if (attempt === 3) {
|
|
63
|
+
throw new Error('Browse All could not inspect the Marketplace connector after 3 attempts.');
|
|
64
|
+
}
|
|
65
|
+
await retryButton.click({ timeout: 60000 });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
await addButton.click({ timeout: 60000 });
|
|
69
|
+
if (await this.completeImportReviewIfNecessary(retryButton)) {
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
if (attempt === 3) {
|
|
73
|
+
throw new Error('Browse All could not import the Marketplace connector after 3 attempts.');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
await this.availableElementsHeading.waitFor({
|
|
77
|
+
state: 'visible',
|
|
78
|
+
timeout: 120000,
|
|
79
|
+
});
|
|
80
|
+
await browseAllDialog
|
|
81
|
+
.getByRole('button', { name: 'Close' })
|
|
82
|
+
.click({ timeout: 60000 });
|
|
83
|
+
await browseAllDialog.waitFor({ state: 'hidden', timeout: 60000 });
|
|
84
|
+
}
|
|
85
|
+
async completeImportReviewIfNecessary(retryButton) {
|
|
86
|
+
await this.importModal
|
|
87
|
+
.or(this.availableElementsHeading)
|
|
88
|
+
.or(retryButton)
|
|
89
|
+
.waitFor({ state: 'visible', timeout: 120000 });
|
|
90
|
+
if (await retryButton.isVisible()) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
if (!(await this.importModal.isVisible())) {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const confirmButton = this.importModal
|
|
97
|
+
.getByRole('button', {
|
|
98
|
+
name: /^(Add to project|Replace resource|Save as copy)$/,
|
|
99
|
+
})
|
|
100
|
+
.first();
|
|
101
|
+
const confirmationVisible = await confirmButton
|
|
102
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
103
|
+
.then(() => true)
|
|
104
|
+
.catch(() => false);
|
|
105
|
+
if (confirmationVisible) {
|
|
106
|
+
await confirmButton.click({ timeout: 60000 });
|
|
107
|
+
}
|
|
108
|
+
await this.importModal.waitFor({ state: 'hidden', timeout: 120000 });
|
|
109
|
+
await this.availableElementsHeading
|
|
110
|
+
.or(retryButton)
|
|
111
|
+
.waitFor({ state: 'visible', timeout: 120000 });
|
|
112
|
+
return !(await retryButton.isVisible());
|
|
113
|
+
}
|
|
114
|
+
async findConnectorCard(connectorName) {
|
|
115
|
+
if (!connectorName) {
|
|
116
|
+
return this.marketplaceConnectorCard;
|
|
117
|
+
}
|
|
118
|
+
const exactCard = this.marketplaceConnectorList.getByRole('button', {
|
|
119
|
+
name: `View details for ${connectorName} from Marketplace`,
|
|
120
|
+
exact: true,
|
|
121
|
+
});
|
|
122
|
+
const searchWords = connectorName
|
|
123
|
+
.replace(/\s+connector$/i, '')
|
|
124
|
+
.trim()
|
|
125
|
+
.split(/\s+/)
|
|
126
|
+
.map(escapeRegExp)
|
|
127
|
+
.join('.*');
|
|
128
|
+
const relatedCard = this.marketplaceConnectorList
|
|
129
|
+
.getByRole('button', {
|
|
130
|
+
name: new RegExp(`^View details for .*${searchWords}.* from Marketplace$`, 'i'),
|
|
131
|
+
})
|
|
132
|
+
.first();
|
|
133
|
+
if (await exactCard.isVisible()) {
|
|
134
|
+
return exactCard;
|
|
135
|
+
}
|
|
136
|
+
if (await relatedCard.isVisible()) {
|
|
137
|
+
return relatedCard;
|
|
138
|
+
}
|
|
139
|
+
return this.marketplaceConnectorCard;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
exports.BrowseAllConnectorMarketplacePage = BrowseAllConnectorMarketplacePage;
|
|
143
|
+
function escapeRegExp(value) {
|
|
144
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
145
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class ClusterDetailsPage {
|
|
3
3
|
private page;
|
|
4
|
-
readonly
|
|
4
|
+
readonly clusterDetailsHeading: Locator;
|
|
5
5
|
constructor(page: Page);
|
|
6
6
|
assertCustomPropertyIsNavigated(linkTitle: string, linkName: string, link: string | RegExp): Promise<void>;
|
|
7
7
|
}
|
|
@@ -4,21 +4,28 @@ exports.ClusterDetailsPage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
class ClusterDetailsPage {
|
|
6
6
|
page;
|
|
7
|
-
|
|
7
|
+
// Custom properties no longer have a heading/section of their own — the
|
|
8
|
+
// Environments/Clusters redesign folds them into the "Cluster details"
|
|
9
|
+
// card as rows (camunda-hub's ClusterDetailFields.sm.tsx), so this is the
|
|
10
|
+
// heading that confirms that card — and the properties inside it — rendered.
|
|
11
|
+
clusterDetailsHeading;
|
|
8
12
|
constructor(page) {
|
|
9
13
|
this.page = page;
|
|
10
|
-
this.
|
|
11
|
-
name: '
|
|
14
|
+
this.clusterDetailsHeading = this.page.getByRole('heading', {
|
|
15
|
+
name: 'Cluster details',
|
|
12
16
|
});
|
|
13
17
|
}
|
|
14
18
|
async assertCustomPropertyIsNavigated(linkTitle, linkName, link) {
|
|
15
19
|
const pageTabPromise = this.page.waitForEvent('popup', {
|
|
16
20
|
timeout: 60000,
|
|
17
21
|
});
|
|
22
|
+
// Each custom property is a <dt>/<dd> row (direct child of the details
|
|
23
|
+
// card's <dl>), keyed by the property's own description — not by a
|
|
24
|
+
// generic "Link 1"/"Link 2" row title, which this DOM never had.
|
|
18
25
|
await this.page
|
|
19
|
-
.
|
|
20
|
-
.filter({ hasText: linkTitle })
|
|
21
|
-
.getByRole('link', { name: linkName })
|
|
26
|
+
.locator('dl > div')
|
|
27
|
+
.filter({ has: this.page.locator('dt', { hasText: linkTitle }) })
|
|
28
|
+
.getByRole('link', { name: linkName, exact: true })
|
|
22
29
|
.click({ timeout: 60000 });
|
|
23
30
|
const camundaPage = await pageTabPromise;
|
|
24
31
|
await (0, test_1.expect)(camundaPage).toHaveURL(link);
|
|
@@ -9,7 +9,9 @@ class ClusterPage {
|
|
|
9
9
|
cluster;
|
|
10
10
|
constructor(page) {
|
|
11
11
|
this.page = page;
|
|
12
|
-
|
|
12
|
+
// The Clusters tab defaults to the card grid (`[data-test="cluster-card"]`),
|
|
13
|
+
// not a table — there is no `tbody` in that view.
|
|
14
|
+
this.clustersList = page.locator('[data-test="cluster-card"]');
|
|
13
15
|
this.cluster = this.clustersList.getByText(this.defaultClusterName).first();
|
|
14
16
|
}
|
|
15
17
|
async clickClusterLink() {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class ConnectorMarketplacePage {
|
|
3
3
|
private page;
|
|
4
|
+
private browseAllConnectorMarketplacePage;
|
|
5
|
+
private connectorName?;
|
|
4
6
|
readonly searchForConnectorTextbox: Locator;
|
|
5
7
|
readonly downloadToProjectButton: Locator;
|
|
6
8
|
readonly addToProjectButton: Locator;
|
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ConnectorMarketplacePage = void 0;
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
|
+
const BrowseAllConnectorMarketplacePage_1 = require("../BrowseAllConnectorMarketplacePage");
|
|
5
6
|
const sleep_1 = require("../../utils/sleep");
|
|
6
7
|
class ConnectorMarketplacePage {
|
|
7
8
|
page;
|
|
9
|
+
browseAllConnectorMarketplacePage;
|
|
10
|
+
connectorName;
|
|
8
11
|
searchForConnectorTextbox;
|
|
9
12
|
downloadToProjectButton;
|
|
10
13
|
addToProjectButton;
|
|
@@ -15,7 +18,10 @@ class ConnectorMarketplacePage {
|
|
|
15
18
|
noResultsMessage;
|
|
16
19
|
constructor(page) {
|
|
17
20
|
this.page = page;
|
|
18
|
-
this.
|
|
21
|
+
this.browseAllConnectorMarketplacePage =
|
|
22
|
+
new BrowseAllConnectorMarketplacePage_1.BrowseAllConnectorMarketplacePage(page);
|
|
23
|
+
this.searchForConnectorTextbox =
|
|
24
|
+
this.browseAllConnectorMarketplacePage.searchForConnectorTextbox.or(page.getByPlaceholder('Search for a connector'));
|
|
19
25
|
this.downloadToProjectButton = page
|
|
20
26
|
.getByRole('button', { name: 'Download to project' })
|
|
21
27
|
.first();
|
|
@@ -35,6 +41,7 @@ class ConnectorMarketplacePage {
|
|
|
35
41
|
await (0, sleep_1.sleep)(2000);
|
|
36
42
|
}
|
|
37
43
|
async fillSearchForConnectorTextbox(connectorName) {
|
|
44
|
+
this.connectorName = connectorName;
|
|
38
45
|
await this.searchForConnectorTextbox.fill(connectorName, { timeout: 60000 });
|
|
39
46
|
await (0, sleep_1.sleep)(2000);
|
|
40
47
|
}
|
|
@@ -48,6 +55,10 @@ class ConnectorMarketplacePage {
|
|
|
48
55
|
await this.replaceResourceButton.click({ timeout: 30000 });
|
|
49
56
|
}
|
|
50
57
|
async waitForConnectorSearchResults() {
|
|
58
|
+
if (await this.browseAllConnectorMarketplacePage.isOpen()) {
|
|
59
|
+
await this.browseAllConnectorMarketplacePage.waitForConnectorSearchResult(this.connectorName);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
51
62
|
const MAX_RETRIES = 4;
|
|
52
63
|
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
|
53
64
|
await (0, test_1.expect)(this.downloadToProjectButton
|
|
@@ -72,6 +83,10 @@ class ConnectorMarketplacePage {
|
|
|
72
83
|
throw new Error('Marketplace connector search failed after retries due to persistent error or no matching results');
|
|
73
84
|
}
|
|
74
85
|
async downloadConnectorToProject() {
|
|
86
|
+
if (await this.browseAllConnectorMarketplacePage.isOpen()) {
|
|
87
|
+
await this.browseAllConnectorMarketplacePage.importConnectorToProject(this.connectorName);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
75
90
|
await this.clickDownloadToProjectButton();
|
|
76
91
|
if (await this.confirmImport()) {
|
|
77
92
|
return;
|
|
@@ -2,6 +2,8 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
declare class ModelerCreatePage {
|
|
3
3
|
private page;
|
|
4
4
|
private processApplicationPage;
|
|
5
|
+
private pendingFileSaves;
|
|
6
|
+
private lastFileSaveActivity;
|
|
5
7
|
readonly generalPanel: Locator;
|
|
6
8
|
readonly taskDefinitionPanel: Locator;
|
|
7
9
|
readonly processIdInput: Locator;
|
|
@@ -36,7 +38,6 @@ declare class ModelerCreatePage {
|
|
|
36
38
|
readonly restConnectorOption: Locator;
|
|
37
39
|
readonly changeElementSearchInput: Locator;
|
|
38
40
|
readonly changeElementResults: Locator;
|
|
39
|
-
readonly changeElementTabStrip: Locator;
|
|
40
41
|
readonly reusableAssetsTab: Locator;
|
|
41
42
|
readonly marketPlaceButton: Locator;
|
|
42
43
|
readonly clientIdTextbox: Locator;
|
|
@@ -106,6 +107,8 @@ declare class ModelerCreatePage {
|
|
|
106
107
|
assertThreeElementsVisible(): Promise<void>;
|
|
107
108
|
clickForm(name: string): Promise<void>;
|
|
108
109
|
runProcessInstance(variables?: string, tenant?: string, businessId?: string): Promise<void>;
|
|
110
|
+
private static isFileSave;
|
|
111
|
+
private waitForFileSavesToSettle;
|
|
109
112
|
private suppressTestModeAnnouncement;
|
|
110
113
|
switchToPlay(): Promise<void>;
|
|
111
114
|
clickGeneralPropertiesPanel(): Promise<void>;
|
|
@@ -9,6 +9,8 @@ const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
|
9
9
|
class ModelerCreatePage {
|
|
10
10
|
page;
|
|
11
11
|
processApplicationPage;
|
|
12
|
+
pendingFileSaves = 0;
|
|
13
|
+
lastFileSaveActivity = 0;
|
|
12
14
|
generalPanel;
|
|
13
15
|
taskDefinitionPanel;
|
|
14
16
|
processIdInput;
|
|
@@ -43,7 +45,6 @@ class ModelerCreatePage {
|
|
|
43
45
|
restConnectorOption;
|
|
44
46
|
changeElementSearchInput;
|
|
45
47
|
changeElementResults;
|
|
46
|
-
changeElementTabStrip;
|
|
47
48
|
reusableAssetsTab;
|
|
48
49
|
marketPlaceButton;
|
|
49
50
|
clientIdTextbox;
|
|
@@ -108,6 +109,23 @@ class ModelerCreatePage {
|
|
|
108
109
|
constructor(page) {
|
|
109
110
|
this.page = page;
|
|
110
111
|
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
112
|
+
// Track the modeler's debounced autosave (PATCH /files/{id}) so a deploy
|
|
113
|
+
// can wait until the server holds the latest edits. See
|
|
114
|
+
// waitForFileSavesToSettle().
|
|
115
|
+
page.on('request', (request) => {
|
|
116
|
+
if (ModelerCreatePage.isFileSave(request)) {
|
|
117
|
+
this.pendingFileSaves++;
|
|
118
|
+
this.lastFileSaveActivity = Date.now();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
const settleFileSave = (request) => {
|
|
122
|
+
if (ModelerCreatePage.isFileSave(request)) {
|
|
123
|
+
this.pendingFileSaves = Math.max(0, this.pendingFileSaves - 1);
|
|
124
|
+
this.lastFileSaveActivity = Date.now();
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
page.on('requestfinished', settleFileSave);
|
|
128
|
+
page.on('requestfailed', settleFileSave);
|
|
111
129
|
this.generalPanel = page.locator('[data-group-id="group-general"]');
|
|
112
130
|
this.taskDefinitionPanel = page
|
|
113
131
|
.locator('div')
|
|
@@ -211,17 +229,17 @@ class ModelerCreatePage {
|
|
|
211
229
|
.locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
|
|
212
230
|
this.changeElementSearchInput = page.locator('.djs-popup-search input');
|
|
213
231
|
this.changeElementResults = page.locator('.djs-popup-results');
|
|
214
|
-
this.changeElementTabStrip = page.locator('.djs-popup-tabs-container');
|
|
215
232
|
// The tab renders its label as text content, and the product source
|
|
216
233
|
// spells it both "Reusable Assets" and "Reusable assets".
|
|
217
234
|
this.reusableAssetsTab = page.getByRole('tab', {
|
|
218
235
|
name: /reusable assets/i,
|
|
219
236
|
});
|
|
220
|
-
//
|
|
221
|
-
//
|
|
222
|
-
//
|
|
223
|
-
|
|
224
|
-
|
|
237
|
+
// Browse All replaces the legacy Marketplace entry only in large menus
|
|
238
|
+
// when the Design System is enabled; small or DS-off menus keep the
|
|
239
|
+
// original entry.
|
|
240
|
+
this.marketPlaceButton = page
|
|
241
|
+
.locator('[data-id="hub-browse-all"]')
|
|
242
|
+
.or(page.locator('[data-id="browseConnectors"]'));
|
|
225
243
|
this.clientIdTextbox = page.getByLabel('Client ID');
|
|
226
244
|
this.clientSecretTextbox = page.getByLabel('Client secret');
|
|
227
245
|
this.rememberCredentialsCheckbox = page.getByText('Remember credentials');
|
|
@@ -455,9 +473,13 @@ class ModelerCreatePage {
|
|
|
455
473
|
await legacyItem.click({ timeout: 90000 });
|
|
456
474
|
}
|
|
457
475
|
async runProcessInstance(variables = '', tenant = '', businessId = '') {
|
|
476
|
+
// ensureStageCluster may navigate away from the diagram, which would drop
|
|
477
|
+
// a save that is still debounced, so settle before and after it.
|
|
478
|
+
await this.waitForFileSavesToSettle();
|
|
458
479
|
await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
|
|
459
|
-
await this.
|
|
480
|
+
await this.waitForFileSavesToSettle();
|
|
460
481
|
const maxRetries = 3;
|
|
482
|
+
let executeSucceeded = false;
|
|
461
483
|
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
462
484
|
try {
|
|
463
485
|
await this.clickDeployAndRunMainButton();
|
|
@@ -482,9 +504,16 @@ class ModelerCreatePage {
|
|
|
482
504
|
});
|
|
483
505
|
await this.businessIdInput.fill(businessId);
|
|
484
506
|
}
|
|
485
|
-
await
|
|
486
|
-
|
|
487
|
-
|
|
507
|
+
const [executeResponse] = await Promise.all([
|
|
508
|
+
this.page.waitForResponse((response) => response.request().method() === 'POST' &&
|
|
509
|
+
// Workspace-era Hub builds execute via /v2/projects/{id}; older
|
|
510
|
+
// builds used /process-applications/{id}.
|
|
511
|
+
/\/(process-applications|projects)\/[^/]+\/execute$/.test(new URL(response.url()).pathname), { timeout: 30000 }),
|
|
512
|
+
this.clickDeployAndRunSubButton(),
|
|
513
|
+
]);
|
|
514
|
+
(0, test_1.expect)(executeResponse.ok(), await executeResponse.text()).toBeTruthy();
|
|
515
|
+
executeSucceeded = true;
|
|
516
|
+
break;
|
|
488
517
|
}
|
|
489
518
|
catch (error) {
|
|
490
519
|
if (attempt < maxRetries - 1) {
|
|
@@ -497,12 +526,33 @@ class ModelerCreatePage {
|
|
|
497
526
|
}
|
|
498
527
|
}
|
|
499
528
|
}
|
|
529
|
+
if (executeSucceeded) {
|
|
530
|
+
await this.instanceStartedAssertion();
|
|
531
|
+
}
|
|
500
532
|
}
|
|
501
533
|
// Test mode mounts a one-time "A new way to test your processes" modal
|
|
502
534
|
// (camunda-hub TestAnnouncementModal) that covers the setup steps behind it.
|
|
503
535
|
// It renders unless localStorage already carries the seen flag, which it
|
|
504
536
|
// reads on mount; a fresh Playwright context always starts empty, so set the
|
|
505
537
|
// flag before entering Test mode rather than clicking the modal away.
|
|
538
|
+
static isFileSave(request) {
|
|
539
|
+
return (request.method() === 'PATCH' &&
|
|
540
|
+
/\/api\/internal\/files\/[^/]+$/.test(new URL(request.url()).pathname));
|
|
541
|
+
}
|
|
542
|
+
// Deploy & run deploys the diagram the server holds, not what is on screen.
|
|
543
|
+
// Hub debounces edits for roughly a second before it sends
|
|
544
|
+
// PATCH /files/{id}, and the autosave label keeps showing the previous
|
|
545
|
+
// "Autosaved at ..." time during that window, so the label cannot prove the
|
|
546
|
+
// latest edits are stored. Wait until no file save is in flight and none
|
|
547
|
+
// has started for longer than the debounce, measured from this call, so a
|
|
548
|
+
// save queued by edits made just before it is observed and awaited.
|
|
549
|
+
async waitForFileSavesToSettle(quietMs = 2000, timeout = 60000) {
|
|
550
|
+
const start = Date.now();
|
|
551
|
+
await test_1.expect
|
|
552
|
+
.poll(() => this.pendingFileSaves === 0 &&
|
|
553
|
+
Date.now() - Math.max(start, this.lastFileSaveActivity) >= quietMs, { timeout, intervals: [250] })
|
|
554
|
+
.toBe(true);
|
|
555
|
+
}
|
|
506
556
|
async suppressTestModeAnnouncement() {
|
|
507
557
|
await this.page.evaluate(() => localStorage.setItem('play-testAnnouncementSeen', 'true'));
|
|
508
558
|
}
|
|
@@ -996,15 +1046,13 @@ class ModelerCreatePage {
|
|
|
996
1046
|
// before bpmn-js attaches its search handler during the popup re-render.
|
|
997
1047
|
async searchAndClickRestConnector() {
|
|
998
1048
|
await (0, test_1.expect)(this.changeElementSearchInput).toBeVisible({ timeout: 30000 });
|
|
999
|
-
await this.changeElementSearchInput.click();
|
|
1000
|
-
await this.changeElementSearchInput.fill('');
|
|
1001
1049
|
// 'Send REST Request' is the template's current name -- connectors #7917
|
|
1002
1050
|
// renamed it from 'REST Outbound Connector' on 2026-07-15, and "Outbound"
|
|
1003
1051
|
// now appears in no searchable field, so the old filter matched nothing.
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout:
|
|
1052
|
+
// Templates live under the popup's "Reusable assets" tab, so searching the
|
|
1053
|
+
// BPMN tab alone never lists them.
|
|
1054
|
+
await this.revealChangeElementTemplate(this.restConnectorOption, 'Send REST Request');
|
|
1055
|
+
await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout: 30000 });
|
|
1008
1056
|
await this.restConnectorOption.click({ timeout: 90000 });
|
|
1009
1057
|
}
|
|
1010
1058
|
async clickRestConnectorOption(recoverSession) {
|
|
@@ -1106,38 +1154,10 @@ class ModelerCreatePage {
|
|
|
1106
1154
|
name: /^Configure (test case|scenario)$/,
|
|
1107
1155
|
}))
|
|
1108
1156
|
.first();
|
|
1109
|
-
// Play settles into exactly ONE of three states, and which one is not
|
|
1110
|
-
// knowable up front, so race them instead of timing out on each in turn:
|
|
1111
|
-
// the setup overlay, the legacy Continue button, or the configure-test
|
|
1112
|
-
// panel (meaning setup is already done for this diagram).
|
|
1113
|
-
//
|
|
1114
|
-
// Waiting only for the first two and reading "neither" as "already ready"
|
|
1115
|
-
// was wrong once the diagram moved inside a process application.
|
|
1116
|
-
// ConfigurationOverlay renders null until isDeploymentStateValidated, and
|
|
1117
|
-
// it sets that flag synchronously ONLY when processApplicationId is
|
|
1118
|
-
// undefined -- exactly what a project-root diagram used to give it. With
|
|
1119
|
-
// the diagram in a process application the id is defined, so the overlay
|
|
1120
|
-
// must first await checkExistingDeployment(). The old 15s probe raced that
|
|
1121
|
-
// async call, concluded "already ready" and returned while the overlay was
|
|
1122
|
-
// still about to appear. ConfigureTestPanel returns null for as long as the
|
|
1123
|
-
// overlay is up, so the Start button never existed and
|
|
1124
|
-
// clickStartInstanceButton burned its entire retry budget looking for it.
|
|
1125
|
-
// camunda-hub's "Play" -> "Test Studio" rebrand renamed this container's
|
|
1126
|
-
// data-test from `play-configuration-overlay` to
|
|
1127
|
-
// `test-studio-configuration-overlay`, and a later build dropped both
|
|
1128
|
-
// container data-test attributes entirely (neither is present in the
|
|
1129
|
-
// failing run's trace DOM), so those two gates now match nothing. The
|
|
1130
|
-
// always-present anchor is the panel's heading, which the current build
|
|
1131
|
-
// renders as "Set up environment" (verb, two words) -- the old
|
|
1132
|
-
// `getByText('Setup environment')` (one word) never matched it and the
|
|
1133
|
-
// whole chain timed out. Match the heading with a spelling-tolerant regex
|
|
1134
|
-
// so a future flip between the two forms cannot re-break the gate, and keep
|
|
1135
|
-
// the legacy data-test attributes for older modeler builds. This locator
|
|
1136
|
-
// means "the setup panel is still on screen".
|
|
1137
1157
|
const configurationOverlay = this.page
|
|
1138
1158
|
.locator('[data-test="test-studio-configuration-overlay"]')
|
|
1139
1159
|
.or(this.page.locator('[data-test="play-configuration-overlay"]'))
|
|
1140
|
-
.or(this.page.getByText(/set
|
|
1160
|
+
.or(this.page.getByText(/set up test run/i))
|
|
1141
1161
|
.first();
|
|
1142
1162
|
const configureTestPanel = this.page.getByTestId('configure-test-panel');
|
|
1143
1163
|
await (0, test_1.expect)(configurationOverlay
|
|
@@ -1156,18 +1176,23 @@ class ModelerCreatePage {
|
|
|
1156
1176
|
if (isNewFlow) {
|
|
1157
1177
|
// Step 1: connect cluster if not yet connected (modal with cluster dropdown)
|
|
1158
1178
|
const connectClusterButton = this.page.getByRole('button', {
|
|
1159
|
-
name: '
|
|
1179
|
+
name: 'Select environment',
|
|
1160
1180
|
});
|
|
1161
1181
|
const needsConnect = await connectClusterButton
|
|
1162
1182
|
.isVisible({ timeout: 3000 })
|
|
1163
1183
|
.catch(() => false);
|
|
1164
1184
|
if (needsConnect) {
|
|
1165
1185
|
await connectClusterButton.click({ timeout });
|
|
1186
|
+
// camunda-hub's TestModeDeployEndpointConfigModal replaced the
|
|
1187
|
+
// "Configure environment" combobox dialog with a "Choose where to
|
|
1188
|
+
// run" dialog: environments now render as a radio group (one radio
|
|
1189
|
+
// per environment), not a dropdown. The first environment is
|
|
1190
|
+
// auto-selected once the list loads, but click it explicitly rather
|
|
1191
|
+
// than relying on that default.
|
|
1166
1192
|
const configureEnvDialog = this.page
|
|
1167
1193
|
.getByRole('dialog')
|
|
1168
|
-
.filter({ hasText: '
|
|
1169
|
-
await configureEnvDialog.getByRole('
|
|
1170
|
-
await this.page.getByRole('option').first().click({ timeout });
|
|
1194
|
+
.filter({ hasText: 'Choose where to run' });
|
|
1195
|
+
await configureEnvDialog.getByRole('radio').first().click({ timeout });
|
|
1171
1196
|
await configureEnvDialog
|
|
1172
1197
|
.getByRole('button', { name: 'Save' })
|
|
1173
1198
|
.click({ timeout });
|
|
@@ -1255,7 +1280,9 @@ class ModelerCreatePage {
|
|
|
1255
1280
|
if (await this.isVisibleWithin(option, 5000)) {
|
|
1256
1281
|
return true;
|
|
1257
1282
|
}
|
|
1258
|
-
|
|
1283
|
+
// The tab strip's container class changed across Hub builds; probe the
|
|
1284
|
+
// "Reusable assets" tab itself instead of its wrapper.
|
|
1285
|
+
if (await this.isVisibleWithin(this.reusableAssetsTab, 5000)) {
|
|
1259
1286
|
// A failed tab click leaves the search path intact, so do not throw.
|
|
1260
1287
|
await this.reusableAssetsTab.click({ timeout: 30000 }).catch(() => { });
|
|
1261
1288
|
if (await this.isVisibleWithin(option, 15000)) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Page, Locator } from '@playwright/test';
|
|
1
|
+
import { Browser, Page, Locator } from '@playwright/test';
|
|
2
2
|
declare class ModelerHomePage {
|
|
3
3
|
private page;
|
|
4
4
|
private defaultFolderName;
|
|
@@ -57,6 +57,7 @@ declare class ModelerHomePage {
|
|
|
57
57
|
username: string;
|
|
58
58
|
password: string;
|
|
59
59
|
}): Promise<void>;
|
|
60
|
+
assignWorkspaceEnvironmentAsOrgAdmin(browser: Browser): Promise<void>;
|
|
60
61
|
clickCrossComponentProjectFolder(credentials?: {
|
|
61
62
|
username: string;
|
|
62
63
|
password: string;
|
|
@@ -349,6 +349,30 @@ class ModelerHomePage {
|
|
|
349
349
|
// under flag-on that folder maps to a Workspace.
|
|
350
350
|
await this.ensureWorkspaceOpen(credentials);
|
|
351
351
|
}
|
|
352
|
+
// Only an org admin can assign Environments to a workspace (Hub 8.10), so a
|
|
353
|
+
// workspace created by a plain Member has nothing to deploy to until one does.
|
|
354
|
+
// Call while this page is inside that workspace: assigns an Environment as the
|
|
355
|
+
// default Identity first user (an org admin) in a separate browser context,
|
|
356
|
+
// leaving this page's session untouched.
|
|
357
|
+
async assignWorkspaceEnvironmentAsOrgAdmin(browser) {
|
|
358
|
+
const workspaceUrl = this.page.url();
|
|
359
|
+
const context = await browser.newContext({
|
|
360
|
+
baseURL: new URL(workspaceUrl).origin,
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
const adminPage = await context.newPage();
|
|
364
|
+
await new NavigationPage_1.NavigationPage(adminPage).goToModeler();
|
|
365
|
+
await new ProcessApplicationPage_1.ProcessApplicationPage(adminPage).assignEnvironmentToWorkspace(workspaceUrl, process.env.MODELER_CLUSTER_NAME);
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
await context.close();
|
|
369
|
+
}
|
|
370
|
+
// This page loaded the workspace before the admin assigned the
|
|
371
|
+
// Environment in the other session, so Hub still holds the empty
|
|
372
|
+
// environment list and Deploy & run has no target until it refetches.
|
|
373
|
+
await this.page.reload({ waitUntil: 'domcontentloaded' });
|
|
374
|
+
this.processApplicationPage.markWorkspaceEnvironmentAssignedByAdmin(workspaceUrl);
|
|
375
|
+
}
|
|
352
376
|
async clickCrossComponentProjectFolder(credentials) {
|
|
353
377
|
// Kept name: callers use this to (re-)enter the shared Workspace's projects
|
|
354
378
|
// listing after leaving Modeler. Creates the Workspace if it is somehow
|
|
@@ -119,7 +119,14 @@ class OperateHomePage {
|
|
|
119
119
|
// yields `"updatedValue""` -- invalid JSON, and Apply rejects it exactly
|
|
120
120
|
// like the per-character version did. Same recipe as
|
|
121
121
|
// pages/c8Run-8.10/OperateHomePage.ts.
|
|
122
|
-
|
|
122
|
+
//
|
|
123
|
+
// Select with Home + Shift+End rather than ControlOrMeta+A: headless
|
|
124
|
+
// Chromium on macOS does not apply Meta+A as select-all inside Monaco's
|
|
125
|
+
// native edit context, so the insert landed mid-string. The value is a
|
|
126
|
+
// single-line JSON string, so selecting the line selects the document on
|
|
127
|
+
// every platform.
|
|
128
|
+
await this.page.keyboard.press('Home');
|
|
129
|
+
await this.page.keyboard.press('Shift+End');
|
|
123
130
|
await this.page.keyboard.insertText(`"${newValue}`);
|
|
124
131
|
}
|
|
125
132
|
async clickSaveVariableButton() {
|
|
@@ -52,10 +52,19 @@ class OptimizeHomePage {
|
|
|
52
52
|
}
|
|
53
53
|
async assertProcessHasBeenImported(processId) {
|
|
54
54
|
await this.clickDashboardLink();
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
// The process list is paginated, so on a cluster with more than one page
|
|
56
|
+
// of processes the new one is not rendered until the list is filtered by
|
|
57
|
+
// name. Import is asynchronous, so reload and re-filter until it appears.
|
|
58
|
+
const search = this.page
|
|
59
|
+
.getByPlaceholder(/Search for name/i)
|
|
60
|
+
.or(this.page.getByPlaceholder('Filter table'))
|
|
61
|
+
.first();
|
|
62
|
+
const processLink = this.page.getByRole('link', { name: processId });
|
|
63
|
+
await (0, test_1.expect)(async () => {
|
|
64
|
+
await this.page.reload();
|
|
65
|
+
await search.fill(processId, { timeout: 30000 });
|
|
66
|
+
await (0, test_1.expect)(processLink).toBeVisible({ timeout: 15000 });
|
|
67
|
+
}).toPass({ timeout: 180000, intervals: [5000, 10000, 15000] });
|
|
59
68
|
}
|
|
60
69
|
async clickOpenUserSettingsButton() {
|
|
61
70
|
await this.openUserSettingsButton.click();
|