@camunda/e2e-test-suite 0.0.1443 → 0.0.1445
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/.github/c8_versions.json +1 -0
- package/README.md +2 -0
- package/dist/fixtures/SM-8.11.d.ts +77 -0
- package/dist/fixtures/SM-8.11.js +176 -0
- package/dist/pages/SM-8.11/Authorization.d.ts +39 -0
- package/dist/pages/SM-8.11/Authorization.js +349 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.d.ts +8 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.js +65 -0
- package/dist/pages/SM-8.11/ClusterPage.d.ts +11 -0
- package/dist/pages/SM-8.11/ClusterPage.js +37 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.d.ts +27 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.js +187 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.d.ts +41 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.js +153 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.d.ts +11 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.js +34 -0
- package/dist/pages/SM-8.11/FormJsPage.d.ts +33 -0
- package/dist/pages/SM-8.11/FormJsPage.js +109 -0
- package/dist/pages/SM-8.11/HomePage.d.ts +20 -0
- package/dist/pages/SM-8.11/HomePage.js +49 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.d.ts +53 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.js +264 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.d.ts +92 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.js +485 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.js +78 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.d.ts +12 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.js +32 -0
- package/dist/pages/SM-8.11/KeycloakUtils.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakUtils.js +94 -0
- package/dist/pages/SM-8.11/LoginPage.d.ts +21 -0
- package/dist/pages/SM-8.11/LoginPage.js +112 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.d.ts +44 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.js +176 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.d.ts +238 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.js +1950 -0
- package/dist/pages/SM-8.11/ModelerHomePage.d.ts +97 -0
- package/dist/pages/SM-8.11/ModelerHomePage.js +956 -0
- package/dist/pages/SM-8.11/NavigationPage.d.ts +58 -0
- package/dist/pages/SM-8.11/NavigationPage.js +389 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.d.ts +10 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.js +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.d.ts +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.js +120 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.d.ts +39 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.js +154 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.js +115 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.js +116 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.d.ts +61 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.js +433 -0
- package/dist/pages/SM-8.11/OCTenantPage.d.ts +73 -0
- package/dist/pages/SM-8.11/OCTenantPage.js +399 -0
- package/dist/pages/SM-8.11/OperateHomePage.d.ts +26 -0
- package/dist/pages/SM-8.11/OperateHomePage.js +139 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.d.ts +30 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.js +276 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.js +383 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.d.ts +17 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.js +62 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.d.ts +14 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.js +63 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.d.ts +19 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.js +76 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.d.ts +50 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.js +209 -0
- package/dist/pages/SM-8.11/PlayPage.d.ts +47 -0
- package/dist/pages/SM-8.11/PlayPage.js +389 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.d.ts +35 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.js +340 -0
- package/dist/pages/SM-8.11/SettingsPage.d.ts +12 -0
- package/dist/pages/SM-8.11/SettingsPage.js +34 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.d.ts +70 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.js +273 -0
- package/dist/pages/SM-8.11/TaskPanelPage.d.ts +29 -0
- package/dist/pages/SM-8.11/TaskPanelPage.js +320 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.d.ts +15 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.js +77 -0
- package/dist/pages/SM-8.11/UtilitiesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/UtilitiesPage.js +573 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.d.ts +6 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.js +77 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.js +48 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.js +226 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.js +307 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.js +174 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.js +410 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.js +220 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.js +461 -0
- package/dist/tests/SM-8.11/login.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/login.spec.js +24 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.js +263 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.js +607 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.js +505 -0
- package/dist/tests/SM-8.11/play.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/play.spec.js +144 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.js +712 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.js +167 -0
- package/dist/tests/SM-8.11/test-setup.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/test-setup.spec.js +24 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.js +223 -0
- package/package.json +1 -1
- package/playwright.config.ts +21 -0
|
@@ -0,0 +1,1950 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ModelerCreatePage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
|
|
6
|
+
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
7
|
+
const sleep_1 = require("../../utils/sleep");
|
|
8
|
+
const modelerCluster_1 = require("../../utils/modelerCluster");
|
|
9
|
+
class ModelerCreatePage {
|
|
10
|
+
page;
|
|
11
|
+
processApplicationPage;
|
|
12
|
+
pendingFileSaves = 0;
|
|
13
|
+
lastFileSaveActivity = 0;
|
|
14
|
+
generalPanel;
|
|
15
|
+
taskDefinitionPanel;
|
|
16
|
+
processIdInput;
|
|
17
|
+
elemendIdInput;
|
|
18
|
+
startEventElement;
|
|
19
|
+
appendTaskButton;
|
|
20
|
+
changeTypeButton;
|
|
21
|
+
userTaskOption;
|
|
22
|
+
serviceTaskOption;
|
|
23
|
+
appendEndEventButton;
|
|
24
|
+
dialog;
|
|
25
|
+
startInstanceMainButton;
|
|
26
|
+
deployAndRunMainButton;
|
|
27
|
+
deployAndRunSubButton;
|
|
28
|
+
viewProcessInstanceLink;
|
|
29
|
+
nameInput;
|
|
30
|
+
jobTypeInput;
|
|
31
|
+
diagramBreadcrumb;
|
|
32
|
+
renameDiagramNameButton;
|
|
33
|
+
diagramNameInput;
|
|
34
|
+
moreActionsButton;
|
|
35
|
+
renameMenuItem;
|
|
36
|
+
renameInput;
|
|
37
|
+
renameSubmitButton;
|
|
38
|
+
variableInput;
|
|
39
|
+
businessIdInput;
|
|
40
|
+
embedFormButton;
|
|
41
|
+
embedButton;
|
|
42
|
+
newForm;
|
|
43
|
+
deploySubButton;
|
|
44
|
+
cancelButton;
|
|
45
|
+
restConnectorOption;
|
|
46
|
+
changeElementSearchInput;
|
|
47
|
+
changeElementResults;
|
|
48
|
+
reusableAssetsTab;
|
|
49
|
+
marketPlaceButton;
|
|
50
|
+
clientIdTextbox;
|
|
51
|
+
clientSecretTextbox;
|
|
52
|
+
rememberCredentialsCheckbox;
|
|
53
|
+
webhookMessageStartEventConnectorOption;
|
|
54
|
+
webhookIdInput;
|
|
55
|
+
implementationSection;
|
|
56
|
+
implementationOptions;
|
|
57
|
+
assignmentSection;
|
|
58
|
+
priorityInput;
|
|
59
|
+
secondElement;
|
|
60
|
+
firstElement;
|
|
61
|
+
thirdElement;
|
|
62
|
+
appendElementButton;
|
|
63
|
+
firstPlacedGateway;
|
|
64
|
+
secondPlacedGateway;
|
|
65
|
+
secondPlacedElement;
|
|
66
|
+
appendPreButton;
|
|
67
|
+
appendAnythingEntry;
|
|
68
|
+
appendPopup;
|
|
69
|
+
appendGatewayButton;
|
|
70
|
+
parallelGatewayOption;
|
|
71
|
+
connectToOtherElementButton;
|
|
72
|
+
canvas;
|
|
73
|
+
intermediateBoundaryEvent;
|
|
74
|
+
intermediateWebhookConnectorOption;
|
|
75
|
+
correlationKeyProcessInput;
|
|
76
|
+
correlationKeyPayloadInput;
|
|
77
|
+
playTab;
|
|
78
|
+
continueToPlayButton;
|
|
79
|
+
candidateUsersInput;
|
|
80
|
+
assigneeInput;
|
|
81
|
+
candidateGroupsInput;
|
|
82
|
+
tenantIdInput;
|
|
83
|
+
timerEventOption;
|
|
84
|
+
timerEventSettings;
|
|
85
|
+
timerType;
|
|
86
|
+
timerValue;
|
|
87
|
+
eighthPlacedElement;
|
|
88
|
+
processIdSecondLocator;
|
|
89
|
+
deployMenuItem;
|
|
90
|
+
additionalActionsButton;
|
|
91
|
+
closeModalButton;
|
|
92
|
+
deployErrorMessage;
|
|
93
|
+
implementTab;
|
|
94
|
+
bpmnReplaceMenu;
|
|
95
|
+
connectorsDocHandlingStartEventElement;
|
|
96
|
+
startEventVariableInputWithThreeVariables;
|
|
97
|
+
zeebeRestAddressVariable;
|
|
98
|
+
variableAssignmentValueTextbox;
|
|
99
|
+
zeebeSecretVariable;
|
|
100
|
+
variableAssignmentValueTextboxSecondVariable;
|
|
101
|
+
zeebeClientIdVariable;
|
|
102
|
+
variableAssignmentValueTextboxThirdVariable;
|
|
103
|
+
firstPlaceRESTConnector;
|
|
104
|
+
authenticationPanel;
|
|
105
|
+
templateNotFoundTag;
|
|
106
|
+
oauthTokenEndpointInput;
|
|
107
|
+
closeDetailsPanel;
|
|
108
|
+
openDetailsPanel;
|
|
109
|
+
constructor(page) {
|
|
110
|
+
this.page = page;
|
|
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);
|
|
129
|
+
this.generalPanel = page.locator('[data-group-id="group-general"]');
|
|
130
|
+
this.taskDefinitionPanel = page
|
|
131
|
+
.locator('div')
|
|
132
|
+
.filter({ hasText: /^Task definition$/ })
|
|
133
|
+
.first();
|
|
134
|
+
this.processIdInput = page.getByLabel('ID', { exact: true });
|
|
135
|
+
this.elemendIdInput = page.getByLabel('ID', { exact: true });
|
|
136
|
+
this.startEventElement = page
|
|
137
|
+
.locator('.djs-shape:has(> .djs-visual > circle) > .djs-hit')
|
|
138
|
+
.first();
|
|
139
|
+
// The improved canvas exposes the append affordance of the selected element
|
|
140
|
+
// as an overlay indicator ("+" beside the shape) that stands in for the
|
|
141
|
+
// create pad's call-to-action entry; the `.djs-create-pad-icon` wrapper this
|
|
142
|
+
// used to hover is no longer rendered.
|
|
143
|
+
this.appendPreButton = page
|
|
144
|
+
.locator('.djs-append-indicator.selected')
|
|
145
|
+
.first();
|
|
146
|
+
this.appendTaskButton = page.getByRole('button', { name: 'Append task' });
|
|
147
|
+
this.appendAnythingEntry = page.locator('.djs-create-pad-entries [data-entry-id="append"]');
|
|
148
|
+
this.appendPopup = page.locator('.djs-popup.bpmn-append');
|
|
149
|
+
this.changeTypeButton = page.getByRole('button', {
|
|
150
|
+
name: 'Change element',
|
|
151
|
+
exact: true,
|
|
152
|
+
});
|
|
153
|
+
// Anchor on the built-in entry's data-id: the popup also lists the
|
|
154
|
+
// project's deployed resources (`data-id="resources-replace-bpmnProcess-N"`)
|
|
155
|
+
// labelled with the diagram name, which makes a role+name match on
|
|
156
|
+
// 'User task' ambiguous as soon as such a diagram exists.
|
|
157
|
+
this.userTaskOption = page.locator('.djs-popup-results [data-id="replace-with-user-task"]');
|
|
158
|
+
this.serviceTaskOption = page.getByRole('option', { name: 'Service task' });
|
|
159
|
+
this.appendEndEventButton = page.getByRole('button', {
|
|
160
|
+
name: 'Append end event',
|
|
161
|
+
});
|
|
162
|
+
this.appendGatewayButton = page.getByRole('button', {
|
|
163
|
+
name: 'Append gateway',
|
|
164
|
+
});
|
|
165
|
+
// bpmn-js renders popup entries as <li role="option" data-id="..."> and
|
|
166
|
+
// binds the click handler to that li. getByText resolved the nested
|
|
167
|
+
// <span class="djs-popup-label"> instead, whose box kept failing the
|
|
168
|
+
// visible/stable actionability check inside the scrolling results list --
|
|
169
|
+
// which is why this timed out on some runs and not others. Target the li,
|
|
170
|
+
// by the data-id bpmn-js owns, with the role as a fallback. The suffix
|
|
171
|
+
// match covers both the append and change-element popups
|
|
172
|
+
// (append-* / replace-with-parallel-gateway).
|
|
173
|
+
this.parallelGatewayOption = page
|
|
174
|
+
.locator('.djs-popup-results [data-id$="parallel-gateway"]')
|
|
175
|
+
.or(page.getByRole('option', { name: /parallel gateway/i }))
|
|
176
|
+
.first();
|
|
177
|
+
this.dialog = page.getByRole('dialog');
|
|
178
|
+
this.startInstanceMainButton = page.getByRole('button', {
|
|
179
|
+
name: 'Run',
|
|
180
|
+
exact: true,
|
|
181
|
+
});
|
|
182
|
+
this.deployAndRunMainButton = page.getByRole('button', {
|
|
183
|
+
name: 'Deploy & run',
|
|
184
|
+
exact: true,
|
|
185
|
+
});
|
|
186
|
+
this.deployAndRunSubButton = this.dialog.getByRole('button', {
|
|
187
|
+
name: 'Deploy & run',
|
|
188
|
+
exact: true,
|
|
189
|
+
});
|
|
190
|
+
this.viewProcessInstanceLink = page.getByRole('link', {
|
|
191
|
+
name: 'View process instance',
|
|
192
|
+
});
|
|
193
|
+
this.nameInput = page.getByLabel('Name', { exact: true });
|
|
194
|
+
this.jobTypeInput = page.getByRole('textbox', { name: /job type/i });
|
|
195
|
+
this.diagramBreadcrumb = page.locator('[data-test="breadcrumb-diagram"]');
|
|
196
|
+
this.renameDiagramNameButton = page.getByText('Rename');
|
|
197
|
+
this.diagramNameInput = page.locator('[data-test="editable-input"]');
|
|
198
|
+
// The rename kebab sits beside the breadcrumb in the app header. The
|
|
199
|
+
// editor's action bar is a <header> too, so it also carries the banner
|
|
200
|
+
// role -- getByRole('banner') alone matches three headers in the editor
|
|
201
|
+
// and pulls in "Additional actions" and "Browse more actions". Only the
|
|
202
|
+
// app header holds the breadcrumb nav, so filter on that. The kebab's own
|
|
203
|
+
// name is shell-dependent ("More actions" today, "<segment label> actions"
|
|
204
|
+
// once the design system derives it from the label), so match either.
|
|
205
|
+
this.moreActionsButton = page
|
|
206
|
+
.getByRole('banner')
|
|
207
|
+
.filter({ has: page.getByRole('navigation') })
|
|
208
|
+
.getByRole('button', { name: /actions$/i });
|
|
209
|
+
this.renameMenuItem = page.getByRole('menuitem', { name: 'Rename' });
|
|
210
|
+
this.renameInput = page.locator('[data-test="rename-input"]');
|
|
211
|
+
this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
|
|
212
|
+
this.variableInput = page.locator('[id="variables-json"]');
|
|
213
|
+
this.businessIdInput = page.locator('[id^="business-id-input"]');
|
|
214
|
+
this.embedFormButton = page.getByRole('button', { name: 'Link form' });
|
|
215
|
+
// Workspaces-enabled Web Modeler renders the "Link a form" picker via
|
|
216
|
+
// LinkTargetModal, whose primary action carries data-test="link-target-confirm"
|
|
217
|
+
// instead of the legacy move dialog's "confirm-move".
|
|
218
|
+
this.embedButton = page
|
|
219
|
+
.locator('[data-test="confirm-move"]')
|
|
220
|
+
.or(page.locator('[data-test="link-target-confirm"]'));
|
|
221
|
+
this.newForm = page.locator('[data-test="item-New form"]');
|
|
222
|
+
this.continueToPlayButton = page.getByRole('button', { name: 'Continue' });
|
|
223
|
+
this.deploySubButton = page
|
|
224
|
+
.getByRole('dialog')
|
|
225
|
+
.getByRole('button', { name: 'Deploy' });
|
|
226
|
+
this.cancelButton = page.getByRole('button', { name: 'Cancel' });
|
|
227
|
+
this.restConnectorOption = page
|
|
228
|
+
.locator('.djs-popup-results')
|
|
229
|
+
.locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
|
|
230
|
+
this.changeElementSearchInput = page.locator('.djs-popup-search input');
|
|
231
|
+
this.changeElementResults = page.locator('.djs-popup-results');
|
|
232
|
+
// The tab renders its label as text content, and the product source
|
|
233
|
+
// spells it both "Reusable Assets" and "Reusable assets".
|
|
234
|
+
this.reusableAssetsTab = page.getByRole('tab', {
|
|
235
|
+
name: /reusable assets/i,
|
|
236
|
+
});
|
|
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"]'));
|
|
243
|
+
this.clientIdTextbox = page.getByLabel('Client ID');
|
|
244
|
+
this.clientSecretTextbox = page.getByLabel('Client secret');
|
|
245
|
+
this.rememberCredentialsCheckbox = page.getByText('Remember credentials');
|
|
246
|
+
// A name query matches every project-local copy of this template too
|
|
247
|
+
// ("Webhook Message Start Event Connector - Copy", left behind by earlier
|
|
248
|
+
// marketplace imports), so it resolves to several options and clicking
|
|
249
|
+
// throws in strict mode. Target the bundled template by its stable entry
|
|
250
|
+
// id, the same way restConnectorOption does.
|
|
251
|
+
this.webhookMessageStartEventConnectorOption = page
|
|
252
|
+
.locator('.djs-popup-results')
|
|
253
|
+
.locator('[data-id="replace.template-io.camunda.connectors.webhook.WebhookConnectorStartMessage.v1"]');
|
|
254
|
+
this.webhookIdInput = page.getByLabel('Webhook ID');
|
|
255
|
+
this.firstElement = page.locator('.djs-element.djs-shape').nth(0);
|
|
256
|
+
this.secondElement = page.locator('.djs-element.djs-shape').nth(1);
|
|
257
|
+
this.thirdElement = page.locator('.djs-element.djs-shape').nth(2);
|
|
258
|
+
this.implementationSection = page.locator('[data-group-id="group-userTaskImplementation"]');
|
|
259
|
+
this.implementationOptions = page.locator('#bio-properties-panel-userTaskImplementation');
|
|
260
|
+
this.assignmentSection = page.locator('[data-group-id="group-assignmentDefinition"]');
|
|
261
|
+
// The user-task priority entry renders as <input type="number">, whose ARIA
|
|
262
|
+
// role is spinbutton, not textbox — so a textbox role query never resolves.
|
|
263
|
+
// Target it by its stable properties-panel entry id instead.
|
|
264
|
+
this.priorityInput = page.locator('#bio-properties-panel-priorityDefinitionPriority');
|
|
265
|
+
this.appendElementButton = page
|
|
266
|
+
.locator('[class="djs-context-pad open"]')
|
|
267
|
+
.first();
|
|
268
|
+
this.firstPlacedGateway = page
|
|
269
|
+
.locator('[data-element-id*="Gateway"]')
|
|
270
|
+
.first();
|
|
271
|
+
this.secondPlacedGateway = page
|
|
272
|
+
.locator('[data-element-id*="Gateway"]')
|
|
273
|
+
.last();
|
|
274
|
+
this.secondPlacedElement = page.locator('.djs-element.djs-shape').last();
|
|
275
|
+
this.connectToOtherElementButton = page.getByRole('button', {
|
|
276
|
+
name: 'Connect to other element',
|
|
277
|
+
});
|
|
278
|
+
this.canvas = page.locator('rect').nth(1);
|
|
279
|
+
this.intermediateBoundaryEvent = page.getByRole('button', {
|
|
280
|
+
name: 'Append intermediate/boundary',
|
|
281
|
+
});
|
|
282
|
+
this.intermediateWebhookConnectorOption = page
|
|
283
|
+
.locator('.djs-popup-results')
|
|
284
|
+
.locator('[data-id="replace.template-io.camunda.connectors.webhook.WebhookConnectorIntermediate.v1"]');
|
|
285
|
+
this.correlationKeyPayloadInput = page.getByLabel('Correlation key (payload)');
|
|
286
|
+
this.correlationKeyProcessInput = page.getByLabel('Correlation key (process)');
|
|
287
|
+
// Modeler renamed the third mode tab "Play" -> "Test" (camunda-hub
|
|
288
|
+
// a90fd06). Accept either label; the "Test" branch is scoped to the modes
|
|
289
|
+
// tablist because the details panel also renders a tab named "Test".
|
|
290
|
+
this.playTab = page
|
|
291
|
+
.getByRole('tab', { name: 'Play' })
|
|
292
|
+
.or(page
|
|
293
|
+
.getByRole('tablist', { name: 'List of modes' })
|
|
294
|
+
.getByRole('tab', { name: 'Test', exact: true }));
|
|
295
|
+
this.candidateUsersInput = page.getByRole('textbox', {
|
|
296
|
+
name: 'candidate users',
|
|
297
|
+
});
|
|
298
|
+
this.assigneeInput = page.getByRole('textbox', { name: 'assignee' });
|
|
299
|
+
this.candidateGroupsInput = page.getByRole('textbox', {
|
|
300
|
+
name: 'candidate groups',
|
|
301
|
+
});
|
|
302
|
+
this.tenantIdInput = page.getByLabel('Tenant ID (Optional)');
|
|
303
|
+
this.timerEventOption = page
|
|
304
|
+
.locator('.djs-popup-results')
|
|
305
|
+
.getByRole('option', {
|
|
306
|
+
name: 'Timer intermediate catch event',
|
|
307
|
+
});
|
|
308
|
+
this.timerEventSettings = page.getByText(/^Timer$/).first();
|
|
309
|
+
this.timerType = page.getByLabel('Type');
|
|
310
|
+
this.timerValue = page.getByLabel('Value');
|
|
311
|
+
this.eighthPlacedElement = page
|
|
312
|
+
.locator('g:nth-child(8) > .djs-element > .djs-hit')
|
|
313
|
+
.first();
|
|
314
|
+
this.processIdSecondLocator = page.getByLabel('Process ID');
|
|
315
|
+
this.additionalActionsButton = page.getByRole('button', {
|
|
316
|
+
name: 'Additional actions',
|
|
317
|
+
});
|
|
318
|
+
this.deployMenuItem = page.getByRole('menuitem', {
|
|
319
|
+
name: 'Deploy',
|
|
320
|
+
});
|
|
321
|
+
this.closeModalButton = page
|
|
322
|
+
.getByRole('dialog')
|
|
323
|
+
.getByRole('button', { name: 'Close' });
|
|
324
|
+
this.deployErrorMessage = page.getByText('The diagram failed to deploy & run');
|
|
325
|
+
this.implementTab = page.getByRole('tab', { name: 'Implement' });
|
|
326
|
+
this.bpmnReplaceMenu = page.getByTestId('bpmn-replace');
|
|
327
|
+
this.connectorsDocHandlingStartEventElement = page.locator('g:nth-child(5) > .djs-element > .djs-hit');
|
|
328
|
+
this.startEventVariableInputWithThreeVariables =
|
|
329
|
+
page.getByText('Output mapping3');
|
|
330
|
+
this.zeebeRestAddressVariable = page
|
|
331
|
+
.locator('[data-test="properties-panel"]')
|
|
332
|
+
.getByText('zeebeRestAdress');
|
|
333
|
+
this.variableAssignmentValueTextbox = page.getByRole('textbox', {
|
|
334
|
+
name: 'Variable assignment value',
|
|
335
|
+
});
|
|
336
|
+
this.zeebeSecretVariable = page
|
|
337
|
+
.locator('[data-test="properties-panel"]')
|
|
338
|
+
.getByText('zeebeSecret');
|
|
339
|
+
this.variableAssignmentValueTextboxSecondVariable = page.locator('#bio-properties-panel-StartEvent_1-output-1-source div');
|
|
340
|
+
this.zeebeClientIdVariable = page
|
|
341
|
+
.locator('[data-test="properties-panel"]')
|
|
342
|
+
.getByText('zeebeClientId');
|
|
343
|
+
this.variableAssignmentValueTextboxThirdVariable = page.locator('#bio-properties-panel-StartEvent_1-output-2-source div');
|
|
344
|
+
this.firstPlaceRESTConnector = page.locator('g:nth-child(2) > .djs-element > .djs-hit');
|
|
345
|
+
// Scoped to the group header (not any div whose text is "Authentication")
|
|
346
|
+
// so its class list can be read: the header carries `open` exactly while
|
|
347
|
+
// the group is expanded, which openAuthenticationPanel keys on.
|
|
348
|
+
this.authenticationPanel = page
|
|
349
|
+
.locator('.bio-properties-panel-group-header')
|
|
350
|
+
.filter({ hasText: /^Authentication$/ })
|
|
351
|
+
.first();
|
|
352
|
+
this.templateNotFoundTag = page
|
|
353
|
+
.locator('[data-test="properties-panel"]')
|
|
354
|
+
.getByText('Not found', { exact: true });
|
|
355
|
+
this.oauthTokenEndpointInput = page.getByLabel('OAuth 2.0 token endpoint');
|
|
356
|
+
// Hub's design-system details panel renders a second close control in its
|
|
357
|
+
// header, titled "Close Details panel" (PanelHeaderDs builds
|
|
358
|
+
// `Close ${title} panel`), which getByTitle also matches because its
|
|
359
|
+
// substring match is case-insensitive -- hence the strict-mode violation.
|
|
360
|
+
// Key on the sidebar toggle's data-test plus its state-dependent title so
|
|
361
|
+
// the locator stays unambiguous and still distinguishes collapse from
|
|
362
|
+
// expand. The title is matched case-insensitively (`i`) because the
|
|
363
|
+
// design-system toggle renders "Close details panel" while the legacy one
|
|
364
|
+
// renders "Close Details Panel".
|
|
365
|
+
this.closeDetailsPanel = page.locator('[data-test="details-panel-toggle"][title="Close Details Panel" i]');
|
|
366
|
+
this.openDetailsPanel = page.locator('[data-test="details-panel-toggle"][title="Open Details Panel" i]');
|
|
367
|
+
}
|
|
368
|
+
async deployProcessInstance(tenant = '') {
|
|
369
|
+
await this.clickDeployButton();
|
|
370
|
+
await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
|
|
371
|
+
await (0, modelerCluster_1.selectModelerCluster)(this.dialog);
|
|
372
|
+
if (tenant.length > 0) {
|
|
373
|
+
await this.clickTenantIdInput();
|
|
374
|
+
await this.fillTenantIdInput(tenant);
|
|
375
|
+
}
|
|
376
|
+
await this.clickDeploySubButton();
|
|
377
|
+
await (0, test_1.expect)(this.page.getByText('Process successfully deployed')).toBeVisible({
|
|
378
|
+
timeout: 30000,
|
|
379
|
+
});
|
|
380
|
+
await this.clickCloseModalButton();
|
|
381
|
+
}
|
|
382
|
+
async clickCloseModalButton() {
|
|
383
|
+
await this.closeModalButton.click({ timeout: 60000 });
|
|
384
|
+
}
|
|
385
|
+
// Function to model a simple Camunda User Task Diagram with or without a form
|
|
386
|
+
async modelCamundaUserTaskDiagram(processName, processId = '', formName = '') {
|
|
387
|
+
await (0, test_1.expect)(this.generalPanel).toBeVisible({
|
|
388
|
+
timeout: 180000,
|
|
389
|
+
});
|
|
390
|
+
await this.enterDiagramName(processName);
|
|
391
|
+
await (0, sleep_1.sleep)(1000);
|
|
392
|
+
await this.clickCanvas();
|
|
393
|
+
await this.clickGeneralPropertiesPanel();
|
|
394
|
+
await this.assertNameInput(processName);
|
|
395
|
+
await this.clickStartEventElement();
|
|
396
|
+
if (processId.length > 0) {
|
|
397
|
+
await this.clickStartEventElement();
|
|
398
|
+
await this.clickGeneralPropertiesPanel();
|
|
399
|
+
await this.clickIdInput();
|
|
400
|
+
await this.fillIdInput(processId);
|
|
401
|
+
await (0, sleep_1.sleep)(1000);
|
|
402
|
+
await this.clickStartEventElement();
|
|
403
|
+
try {
|
|
404
|
+
await (0, test_1.expect)(this.appendElementButton).toBeVisible({ timeout: 1000 });
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
await this.clickStartEventElement();
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
await this.clickAppendElementButton();
|
|
411
|
+
await this.clickAppendTaskButton();
|
|
412
|
+
await this.clickChangeTypeButton();
|
|
413
|
+
await this.clickUserTaskOption();
|
|
414
|
+
await this.assertImplementationOption('zeebeUserTask');
|
|
415
|
+
if (formName.length > 0) {
|
|
416
|
+
await this.clickEmbedFormButton();
|
|
417
|
+
await this.clickForm(formName);
|
|
418
|
+
await this.clickEmbedButton();
|
|
419
|
+
}
|
|
420
|
+
await this.clickAppendElementButton();
|
|
421
|
+
await this.clickAppendEndEventButton();
|
|
422
|
+
await this.clickCanvas();
|
|
423
|
+
await this.assertThreeElementsVisible();
|
|
424
|
+
}
|
|
425
|
+
async modelJobWorkerDiagram(processName) {
|
|
426
|
+
await (0, test_1.expect)(this.generalPanel).toBeVisible({
|
|
427
|
+
timeout: 120000,
|
|
428
|
+
});
|
|
429
|
+
await this.enterDiagramName(processName);
|
|
430
|
+
await (0, sleep_1.sleep)(1000);
|
|
431
|
+
await this.clickStartEventElement();
|
|
432
|
+
await this.clickGeneralPropertiesPanel();
|
|
433
|
+
await this.clickIdInput();
|
|
434
|
+
await this.fillIdInput(processName);
|
|
435
|
+
await this.assertNameInput(processName);
|
|
436
|
+
await this.clickStartEventElement();
|
|
437
|
+
await this.clickAppendElementButton();
|
|
438
|
+
await this.clickAppendTaskButton();
|
|
439
|
+
await this.clickChangeTypeButton();
|
|
440
|
+
await this.clickUserTaskOption();
|
|
441
|
+
await this.chooseImplementationOption('jobWorker');
|
|
442
|
+
await this.clickAppendElementButton();
|
|
443
|
+
await this.clickAppendEndEventButton();
|
|
444
|
+
await this.clickCanvas();
|
|
445
|
+
await this.assertThreeElementsVisible();
|
|
446
|
+
}
|
|
447
|
+
async assertThreeElementsVisible() {
|
|
448
|
+
await (0, test_1.expect)(this.firstElement).toBeVisible({ timeout: 30000 });
|
|
449
|
+
await (0, test_1.expect)(this.secondElement).toBeVisible({ timeout: 30000 });
|
|
450
|
+
await (0, test_1.expect)(this.thirdElement).toBeVisible({ timeout: 30000 });
|
|
451
|
+
}
|
|
452
|
+
// The Carbon shell links a form via a plain list item click. Workspaces-enabled
|
|
453
|
+
// Web Modeler instead browses LinkTargetModal, whose rows are files keyed by id
|
|
454
|
+
// (not name) and merely *select* the row -- linking requires a separate confirm
|
|
455
|
+
// click (LinkTargetLevel/SelectableRow: `aria-label="Select ${name}"`, and the
|
|
456
|
+
// modal's own primary button, clicked by the caller via clickEmbedButton()).
|
|
457
|
+
async clickForm(name) {
|
|
458
|
+
const legacyItem = this.page
|
|
459
|
+
.locator(`[data-test="item-${name}"]`)
|
|
460
|
+
.getByText(name);
|
|
461
|
+
const selectableRow = this.page.getByRole('button', {
|
|
462
|
+
name: `Select ${name}`,
|
|
463
|
+
exact: true,
|
|
464
|
+
});
|
|
465
|
+
await legacyItem
|
|
466
|
+
.or(selectableRow)
|
|
467
|
+
.first()
|
|
468
|
+
.waitFor({ state: 'visible', timeout: 90000 });
|
|
469
|
+
if (await selectableRow.isVisible().catch(() => false)) {
|
|
470
|
+
await selectableRow.click({ timeout: 90000 });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
await legacyItem.click({ timeout: 90000 });
|
|
474
|
+
}
|
|
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();
|
|
479
|
+
await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
|
|
480
|
+
await this.waitForFileSavesToSettle();
|
|
481
|
+
const maxRetries = 3;
|
|
482
|
+
let executeSucceeded = false;
|
|
483
|
+
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
484
|
+
try {
|
|
485
|
+
await this.clickDeployAndRunMainButton();
|
|
486
|
+
await (0, test_1.expect)(this.dialog).toBeVisible({
|
|
487
|
+
timeout: 30000,
|
|
488
|
+
});
|
|
489
|
+
await (0, modelerCluster_1.selectModelerCluster)(this.dialog);
|
|
490
|
+
if (variables.length > 0) {
|
|
491
|
+
await (0, test_1.expect)(this.variableInput).toBeVisible({
|
|
492
|
+
timeout: 60000,
|
|
493
|
+
});
|
|
494
|
+
await this.clickVariableInput();
|
|
495
|
+
await this.fillVariableInput(variables);
|
|
496
|
+
}
|
|
497
|
+
if (tenant.length > 0) {
|
|
498
|
+
await this.clickTenantIdInput();
|
|
499
|
+
await this.fillTenantIdInput(tenant);
|
|
500
|
+
}
|
|
501
|
+
if (businessId.length > 0) {
|
|
502
|
+
await (0, test_1.expect)(this.businessIdInput).toBeVisible({
|
|
503
|
+
timeout: 60000,
|
|
504
|
+
});
|
|
505
|
+
await this.businessIdInput.fill(businessId);
|
|
506
|
+
}
|
|
507
|
+
const [executeResponse] = await Promise.all([
|
|
508
|
+
this.page.waitForResponse((response) => response.request().method() === 'POST' &&
|
|
509
|
+
// Hub's deploy & run posts to /v2/files/{id}/execute; older
|
|
510
|
+
// workspace builds used /v2/projects/{id} and
|
|
511
|
+
// /process-applications/{id}.
|
|
512
|
+
/\/(process-applications|projects|files)\/[^/]+\/execute$/.test(new URL(response.url()).pathname), { timeout: 30000 }),
|
|
513
|
+
this.clickDeployAndRunSubButton(),
|
|
514
|
+
]);
|
|
515
|
+
(0, test_1.expect)(executeResponse.ok(), await executeResponse.text()).toBeTruthy();
|
|
516
|
+
executeSucceeded = true;
|
|
517
|
+
break;
|
|
518
|
+
}
|
|
519
|
+
catch (error) {
|
|
520
|
+
if (attempt < maxRetries - 1) {
|
|
521
|
+
console.warn(`Attempt ${attempt + 1} failed for deploy & run running process: ${error instanceof Error ? error.message : error}. Retrying...`);
|
|
522
|
+
await this.page.reload();
|
|
523
|
+
await (0, sleep_1.sleep)(10000);
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
throw new Error(`runProcessInstance failed after ${maxRetries} attempts. Last error: ${error instanceof Error ? error.message : error}`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
if (executeSucceeded) {
|
|
531
|
+
await this.instanceStartedAssertion();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
// Test mode mounts a one-time "A new way to test your processes" modal
|
|
535
|
+
// (camunda-hub TestAnnouncementModal) that covers the setup steps behind it.
|
|
536
|
+
// It renders unless localStorage already carries the seen flag, which it
|
|
537
|
+
// reads on mount; a fresh Playwright context always starts empty, so set the
|
|
538
|
+
// flag before entering Test mode rather than clicking the modal away.
|
|
539
|
+
static isFileSave(request) {
|
|
540
|
+
return (request.method() === 'PATCH' &&
|
|
541
|
+
/\/api\/internal\/files\/[^/]+$/.test(new URL(request.url()).pathname));
|
|
542
|
+
}
|
|
543
|
+
// Deploy & run deploys the diagram the server holds, not what is on screen.
|
|
544
|
+
// Hub debounces edits for roughly a second before it sends
|
|
545
|
+
// PATCH /files/{id}, and the autosave label keeps showing the previous
|
|
546
|
+
// "Autosaved at ..." time during that window, so the label cannot prove the
|
|
547
|
+
// latest edits are stored. Wait until no file save is in flight and none
|
|
548
|
+
// has started for longer than the debounce, measured from this call, so a
|
|
549
|
+
// save queued by edits made just before it is observed and awaited.
|
|
550
|
+
async waitForFileSavesToSettle(quietMs = 2000, timeout = 60000) {
|
|
551
|
+
const start = Date.now();
|
|
552
|
+
await test_1.expect
|
|
553
|
+
.poll(() => this.pendingFileSaves === 0 &&
|
|
554
|
+
Date.now() - Math.max(start, this.lastFileSaveActivity) >= quietMs, { timeout, intervals: [250] })
|
|
555
|
+
.toBe(true);
|
|
556
|
+
}
|
|
557
|
+
async suppressTestModeAnnouncement() {
|
|
558
|
+
await this.page.evaluate(() => localStorage.setItem('play-testAnnouncementSeen', 'true'));
|
|
559
|
+
}
|
|
560
|
+
async switchToPlay() {
|
|
561
|
+
await (0, test_1.expect)(this.playTab).toBeVisible();
|
|
562
|
+
await this.suppressTestModeAnnouncement();
|
|
563
|
+
await this.playTab.click();
|
|
564
|
+
}
|
|
565
|
+
async clickGeneralPropertiesPanel() {
|
|
566
|
+
const isExpanded = (await this.generalPanel
|
|
567
|
+
.locator('.bio-properties-panel-group-header open')
|
|
568
|
+
.count()) > 0;
|
|
569
|
+
if (!isExpanded) {
|
|
570
|
+
await this.generalPanel.click({ timeout: 60000 });
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
async clickIdInput() {
|
|
574
|
+
try {
|
|
575
|
+
await this.processIdInput.click();
|
|
576
|
+
}
|
|
577
|
+
catch {
|
|
578
|
+
await this.processIdSecondLocator.click();
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async clickTaskDefinitionPropertiesPanel() {
|
|
582
|
+
await this.taskDefinitionPanel.click();
|
|
583
|
+
}
|
|
584
|
+
async clickProcessIdInput() {
|
|
585
|
+
await this.processIdInput.click();
|
|
586
|
+
}
|
|
587
|
+
async fillIdInput(id) {
|
|
588
|
+
try {
|
|
589
|
+
await this.processIdInput.fill(id);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
await this.processIdSecondLocator.fill(id);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
async clickElemendIdInput() {
|
|
596
|
+
await this.elemendIdInput.click();
|
|
597
|
+
}
|
|
598
|
+
async fillElementIdInput(id) {
|
|
599
|
+
await this.elemendIdInput.fill(id);
|
|
600
|
+
}
|
|
601
|
+
async clickStartEventElement() {
|
|
602
|
+
await this.startEventElement.click();
|
|
603
|
+
}
|
|
604
|
+
async selectStartEventElement() {
|
|
605
|
+
await this.startEventElement.click();
|
|
606
|
+
}
|
|
607
|
+
// The create pad's direct append icons no longer carry the bpmn-js `title`
|
|
608
|
+
// attributes ("Append task", "Append intermediate/boundary", "Append end
|
|
609
|
+
// event"), so append through the "append anything" entry instead: it opens
|
|
610
|
+
// the bpmn-append popup whose entries are addressed by the stable `data-id`
|
|
611
|
+
// ids bpmn-js declares in PopupEntries. Since camunda-bpmn-js 5.30.0 those
|
|
612
|
+
// entries sit in drill-in categories, so filter with the popup search first
|
|
613
|
+
// and fall back to walking the categories when search does not surface it.
|
|
614
|
+
// The append create pad renders its entries asynchronously once the source
|
|
615
|
+
// element is selected, so wait for the requested entry instead of probing for
|
|
616
|
+
// it. Entries are addressed by the `data-entry-id` bpmn-js declares on them
|
|
617
|
+
// ("append.end-event", "append.intermediate-event", "append.append-task").
|
|
618
|
+
async clickCreatePadEntry(entryId) {
|
|
619
|
+
const entry = this.page.locator(`.djs-create-pad-entries [data-entry-id="${entryId}"]`);
|
|
620
|
+
try {
|
|
621
|
+
await entry.waitFor({ state: 'visible', timeout: 30000 });
|
|
622
|
+
}
|
|
623
|
+
catch {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
await entry.click({ timeout: 30000 });
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
async appendViaPopup(entryId, categoryIds, searchTerm) {
|
|
630
|
+
await this.hoverOnLocator(this.appendPreButton);
|
|
631
|
+
await this.appendAnythingEntry.click({ timeout: 30000 });
|
|
632
|
+
await (0, test_1.expect)(this.appendPopup).toBeVisible({ timeout: 30000 });
|
|
633
|
+
const entry = this.appendPopup.locator(`[data-id="${entryId}"]`).first();
|
|
634
|
+
const search = this.appendPopup.locator('.djs-popup-search input');
|
|
635
|
+
const hasSearch = await search
|
|
636
|
+
.isVisible({ timeout: 5000 })
|
|
637
|
+
.catch(() => false);
|
|
638
|
+
if (hasSearch) {
|
|
639
|
+
await search.fill(searchTerm);
|
|
640
|
+
}
|
|
641
|
+
const surfaced = await entry.isVisible({ timeout: 5000 }).catch(() => false);
|
|
642
|
+
if (!surfaced) {
|
|
643
|
+
if (hasSearch) {
|
|
644
|
+
await search.fill('');
|
|
645
|
+
}
|
|
646
|
+
// The popup drills into a category based on its own hover-tracked
|
|
647
|
+
// selection, so hover each category before clicking it.
|
|
648
|
+
for (const categoryId of categoryIds) {
|
|
649
|
+
const category = this.appendPopup
|
|
650
|
+
.locator(`[data-id="${categoryId}"]`)
|
|
651
|
+
.first();
|
|
652
|
+
await category.scrollIntoViewIfNeeded({ timeout: 15000 });
|
|
653
|
+
await category.hover({ timeout: 15000 });
|
|
654
|
+
await category.click({ timeout: 15000 });
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
await entry.scrollIntoViewIfNeeded({ timeout: 30000 });
|
|
658
|
+
await entry.click({ timeout: 30000 });
|
|
659
|
+
await (0, test_1.expect)(this.appendPopup).toBeHidden({ timeout: 30000 });
|
|
660
|
+
}
|
|
661
|
+
async clickAppendTaskButton() {
|
|
662
|
+
await this.hoverOnLocator(this.appendPreButton);
|
|
663
|
+
if (await this.clickCreatePadEntry('append.append-task')) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
await this.appendViaPopup('append-task', ['append-tasks'], 'Task');
|
|
667
|
+
}
|
|
668
|
+
// The create pad is only mounted while the append affordance is engaged. An
|
|
669
|
+
// element without an outgoing flow gets the pad on selection, but one that
|
|
670
|
+
// already has an outgoing flow (the first gateway of a parallel branch) only
|
|
671
|
+
// gets the overlay indicator ("+" beside the shape), and the pad appears once
|
|
672
|
+
// that indicator is hovered. The indicator carries its element id in
|
|
673
|
+
// `data-element`, so key the hover on the current selection: hovering another
|
|
674
|
+
// element's indicator would open its pad and append to the wrong shape.
|
|
675
|
+
async padHoverTargets(locator, selector) {
|
|
676
|
+
if (!selector.includes('create-pad')) {
|
|
677
|
+
return [locator];
|
|
678
|
+
}
|
|
679
|
+
const selectedElementId = await this.page
|
|
680
|
+
.locator('.djs-element.selected')
|
|
681
|
+
.first()
|
|
682
|
+
.getAttribute('data-element-id', { timeout: 5000 })
|
|
683
|
+
.catch(() => null);
|
|
684
|
+
if (!selectedElementId) {
|
|
685
|
+
return [locator];
|
|
686
|
+
}
|
|
687
|
+
return [
|
|
688
|
+
this.page
|
|
689
|
+
.locator(`.djs-append-indicator[data-element="${selectedElementId}"]`)
|
|
690
|
+
.first(),
|
|
691
|
+
locator,
|
|
692
|
+
];
|
|
693
|
+
}
|
|
694
|
+
async hoverOnLocator(locator, selector = '.djs-create-pad-entry') {
|
|
695
|
+
// Only visible entries count: both `page.waitForSelector` and `.first()`
|
|
696
|
+
// resolve a single DOM match, so an entry belonging to another element's
|
|
697
|
+
// closed pad would never satisfy `visible`.
|
|
698
|
+
const padEntry = this.page.locator(`${selector}:visible`).first();
|
|
699
|
+
let lastError = null;
|
|
700
|
+
// Promise.race leaves the losing branch pending, so when that branch later
|
|
701
|
+
// rejects nobody awaits it: the unhandled rejection fails the whole test
|
|
702
|
+
// with the loser's message ("Canvas not visible") before the callers'
|
|
703
|
+
// retries can recover. Poll the two states sequentially instead.
|
|
704
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
705
|
+
if (await padEntry.isVisible()) {
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
// bpmn-js opens the pad on mouseover, which does not re-fire while the
|
|
709
|
+
// pointer already rests on the trigger, so park it off-element on retries.
|
|
710
|
+
if (attempt > 1) {
|
|
711
|
+
await this.page.mouse.move(0, 0);
|
|
712
|
+
}
|
|
713
|
+
for (const target of await this.padHoverTargets(locator, selector)) {
|
|
714
|
+
const targetVisible = await target
|
|
715
|
+
.waitFor({ state: 'visible', timeout: 5000 })
|
|
716
|
+
.then(() => true)
|
|
717
|
+
.catch(() => false);
|
|
718
|
+
if (!targetVisible) {
|
|
719
|
+
continue;
|
|
720
|
+
}
|
|
721
|
+
// force: the append affordance of an element that already has an
|
|
722
|
+
// outgoing flow is drawn on top of that sequence flow, so the
|
|
723
|
+
// hit-target check resolves to the connection instead of the trigger.
|
|
724
|
+
await target.hover({ timeout: 10000, force: true }).catch((error) => {
|
|
725
|
+
lastError = error;
|
|
726
|
+
});
|
|
727
|
+
const padOpen = await padEntry
|
|
728
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
729
|
+
.then(() => true)
|
|
730
|
+
.catch(() => false);
|
|
731
|
+
if (padOpen) {
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
throw new Error(`Canvas pad entries "${selector}" did not open${lastError ? `: ${lastError}` : ''}`);
|
|
737
|
+
}
|
|
738
|
+
async clickAppendGatewayButton() {
|
|
739
|
+
await this.appendGatewayButton.click({ timeout: 90000 });
|
|
740
|
+
}
|
|
741
|
+
async setEngineVersion(version) {
|
|
742
|
+
const escapedVersion = version.replace(/\./g, '\\.');
|
|
743
|
+
const versionSelector = this.page.locator('[data-test="version-selector"]');
|
|
744
|
+
// The dropdown renders each version as an outer <label> (Styled.Label)
|
|
745
|
+
// wrapping a radio input and a separate text span ("Camunda X.Y").
|
|
746
|
+
// The radio's accessible name is empty (no label prop passed), so
|
|
747
|
+
// getByRole('radio') won't resolve to the right item by text.
|
|
748
|
+
// Clicking the outer <label> containing the version string triggers
|
|
749
|
+
// the enclosed radio input's onChange via event propagation.
|
|
750
|
+
const versionOption = this.page
|
|
751
|
+
.getByRole('radiogroup', { name: 'Camunda versions' })
|
|
752
|
+
.locator('label')
|
|
753
|
+
.filter({ hasText: new RegExp(`Camunda\\s+${escapedVersion}`) })
|
|
754
|
+
.first();
|
|
755
|
+
// Retry the open-select cycle and confirm via the selector label ("Check
|
|
756
|
+
// problems against: Camunda X.Y"). A label click dropped while the dropdown
|
|
757
|
+
// is still animating silently leaves the diagram on the default (older)
|
|
758
|
+
// version, which greys out the version-gated REST connector downstream.
|
|
759
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
760
|
+
if (await versionSelector.filter({ hasText: version }).isVisible()) {
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
await versionSelector.click({ timeout: 10000 });
|
|
764
|
+
await versionOption.click({ timeout: 5000 });
|
|
765
|
+
const committed = await versionSelector
|
|
766
|
+
.filter({ hasText: version })
|
|
767
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
768
|
+
.then(() => true)
|
|
769
|
+
.catch(() => false);
|
|
770
|
+
if (committed) {
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
async clickChangeTypeButton() {
|
|
776
|
+
try {
|
|
777
|
+
await this.changeTypeButton.click({ timeout: 180000, force: true });
|
|
778
|
+
}
|
|
779
|
+
catch {
|
|
780
|
+
await this.page.reload();
|
|
781
|
+
if (await this.secondElement.isVisible({ timeout: 6000 })) {
|
|
782
|
+
await this.secondElement.click({ timeout: 90000, force: true });
|
|
783
|
+
await this.changeTypeButton.click({ timeout: 90000, force: true });
|
|
784
|
+
}
|
|
785
|
+
else {
|
|
786
|
+
await this.firstElement.click({ timeout: 90000, force: true });
|
|
787
|
+
await this.changeTypeButton.click({ timeout: 90000, force: true });
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
async clickUserTaskOption() {
|
|
792
|
+
const maxRetries = 3;
|
|
793
|
+
for (let retries = 0; retries < maxRetries; retries++) {
|
|
794
|
+
try {
|
|
795
|
+
if (retries === 0) {
|
|
796
|
+
await this.userTaskOption.click({ timeout: 30000, force: true });
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
await this.changeTypeButton.click({ force: true, timeout: 30000 });
|
|
800
|
+
await this.userTaskOption.click({ timeout: 30000, force: true });
|
|
801
|
+
}
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
catch (error) {
|
|
805
|
+
console.error(`Click attempt ${retries + 1} failed: ${error}`);
|
|
806
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
|
|
810
|
+
}
|
|
811
|
+
async clickServiceTaskOption() {
|
|
812
|
+
const maxRetries = 3;
|
|
813
|
+
for (let retries = 0; retries < maxRetries; retries++) {
|
|
814
|
+
try {
|
|
815
|
+
if (retries === 0) {
|
|
816
|
+
await this.serviceTaskOption.click({ timeout: 30000, force: true });
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
await this.changeTypeButton.click({ force: true, timeout: 30000 });
|
|
820
|
+
await this.serviceTaskOption.click({ timeout: 30000, force: true });
|
|
821
|
+
}
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
console.error(`Click attempt ${retries + 1} failed: ${error}`);
|
|
826
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
|
|
830
|
+
}
|
|
831
|
+
async appendEndEvent() {
|
|
832
|
+
if (await this.clickCreatePadEntry('append.end-event')) {
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
await this.appendViaPopup('append-none-end-event', ['append-events', 'append-end-events'], 'End event');
|
|
836
|
+
}
|
|
837
|
+
async clickAppendEndEventButton(parentElement) {
|
|
838
|
+
try {
|
|
839
|
+
await this.appendEndEvent();
|
|
840
|
+
}
|
|
841
|
+
catch (error) {
|
|
842
|
+
await this.page.reload();
|
|
843
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
844
|
+
await sleep(10000);
|
|
845
|
+
if (parentElement == undefined) {
|
|
846
|
+
await this.clickSecondPlacedElement();
|
|
847
|
+
}
|
|
848
|
+
else if (parentElement == 'parallelGateway') {
|
|
849
|
+
await this.clickSecondPlacedGateway();
|
|
850
|
+
}
|
|
851
|
+
await this.hoverOnLocator(this.appendElementButton);
|
|
852
|
+
await this.appendEndEvent();
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
async clickFirstPlacedElement() {
|
|
856
|
+
await this.firstElement.click({ timeout: 90000 });
|
|
857
|
+
}
|
|
858
|
+
async clickSecondPlacedElement() {
|
|
859
|
+
await this.secondPlacedElement.click({ timeout: 60000 });
|
|
860
|
+
}
|
|
861
|
+
async clickFirstPlacedGateway() {
|
|
862
|
+
try {
|
|
863
|
+
await this.firstPlacedGateway.click({ timeout: 3000 });
|
|
864
|
+
await this.appendElementButton.isVisible({ timeout: 3000 });
|
|
865
|
+
}
|
|
866
|
+
catch (error) {
|
|
867
|
+
await this.page.reload();
|
|
868
|
+
await this.firstPlacedGateway.click({ timeout: 3000 });
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
async clickSecondPlacedGateway() {
|
|
872
|
+
try {
|
|
873
|
+
await (0, test_1.expect)(this.secondPlacedGateway).toBeVisible({ timeout: 30000 });
|
|
874
|
+
await this.secondPlacedGateway.click({ timeout: 3000 });
|
|
875
|
+
await this.appendElementButton.isVisible({ timeout: 3000 });
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
await this.page.reload();
|
|
879
|
+
await (0, test_1.expect)(this.secondPlacedGateway).toBeVisible({ timeout: 30000 });
|
|
880
|
+
await this.secondPlacedGateway.click({ timeout: 3000 });
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
async clickDeployAndRunMainButton() {
|
|
884
|
+
await this.deployAndRunMainButton.click({ timeout: 30000 });
|
|
885
|
+
}
|
|
886
|
+
async clickDeployAndRunSubButton() {
|
|
887
|
+
await this.deployAndRunSubButton.click({ timeout: 30000 });
|
|
888
|
+
}
|
|
889
|
+
async clickViewProcessInstanceLink() {
|
|
890
|
+
await this.viewProcessInstanceLink.click({ timeout: 90000 });
|
|
891
|
+
}
|
|
892
|
+
async clickNameInput() {
|
|
893
|
+
await this.nameInput.click();
|
|
894
|
+
}
|
|
895
|
+
async clickUserTask(id) {
|
|
896
|
+
const priorityUserTask = this.page.locator(`[data-element-id="${id}"]`);
|
|
897
|
+
await priorityUserTask.click({ timeout: 60000 });
|
|
898
|
+
}
|
|
899
|
+
async clickCanvasElement(elementId) {
|
|
900
|
+
await this.page
|
|
901
|
+
.locator(`[data-element-id="${elementId}"]`)
|
|
902
|
+
.first()
|
|
903
|
+
.click({ timeout: 30000 });
|
|
904
|
+
}
|
|
905
|
+
async fillNameInput(name) {
|
|
906
|
+
await this.nameInput.fill(name);
|
|
907
|
+
}
|
|
908
|
+
async clickJobTypeInput() {
|
|
909
|
+
await this.jobTypeInput.click();
|
|
910
|
+
}
|
|
911
|
+
async fillJobTypeInput(name) {
|
|
912
|
+
await this.jobTypeInput.fill(name);
|
|
913
|
+
}
|
|
914
|
+
// Carbon renamed a diagram in three steps: click the diagram crumb, click
|
|
915
|
+
// "Rename" in the menu it opens, then type into the inline input. The Design
|
|
916
|
+
// System shell has no breadcrumb-diagram hook and drives the whole rename
|
|
917
|
+
// from enterDiagramName()'s dialog path, so these two steps have nothing to
|
|
918
|
+
// do there -- skip them instead of timing out on a hook that is gone.
|
|
919
|
+
async clickDiagramBreadcrumb() {
|
|
920
|
+
if (!(await this.legacyBreadcrumbPresent())) {
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
await this.diagramBreadcrumb.click({ timeout: 60000 });
|
|
924
|
+
}
|
|
925
|
+
async clickEditDiagramNameButton() {
|
|
926
|
+
if (!(await this.legacyBreadcrumbPresent())) {
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
await this.renameDiagramNameButton.click();
|
|
930
|
+
}
|
|
931
|
+
async legacyBreadcrumbPresent() {
|
|
932
|
+
return this.diagramBreadcrumb
|
|
933
|
+
.waitFor({ state: 'visible', timeout: 5000 })
|
|
934
|
+
.then(() => true)
|
|
935
|
+
.catch(() => false);
|
|
936
|
+
}
|
|
937
|
+
// The Carbon editor renames the diagram through an inline editable input. The
|
|
938
|
+
// Design System editor has none: renaming moved to the breadcrumb's "More
|
|
939
|
+
// actions" > "Rename" dialog. Use whichever the deployed shell offers.
|
|
940
|
+
async enterDiagramName(name) {
|
|
941
|
+
// Same hazard as ModelerHomePage.enterFormName: the project page also has a
|
|
942
|
+
// "More actions" button, so the or() gate can be satisfied before the
|
|
943
|
+
// editor has loaded and the rename would target the project.
|
|
944
|
+
await this.page.waitForURL(/\/diagrams\//, { timeout: 60000 });
|
|
945
|
+
await this.diagramNameInput
|
|
946
|
+
.or(this.moreActionsButton)
|
|
947
|
+
.first()
|
|
948
|
+
.waitFor({ state: 'visible', timeout: 60000 });
|
|
949
|
+
if (await this.diagramNameInput.isVisible()) {
|
|
950
|
+
await this.diagramNameInput.fill(name, { timeout: 60000 });
|
|
951
|
+
await this.diagramNameInput.press('Enter', { timeout: 60000 });
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
// Today the shell renders one kebab for the whole breadcrumb; camunda-hub
|
|
955
|
+
// #28045 gives every segment its own, so take the LAST -- the diagram is
|
|
956
|
+
// the deepest crumb. With one kebab last() is that same button.
|
|
957
|
+
//
|
|
958
|
+
// The header's license tags sit in an ml-auto flex sibling that overlaps
|
|
959
|
+
// the kebab once they render, so a mouse click lands on the tag instead.
|
|
960
|
+
// Open the menu from the keyboard -- same control, reached the way a
|
|
961
|
+
// keyboard user reaches it, and unaffected by what paints over it.
|
|
962
|
+
//
|
|
963
|
+
// The header also keeps settling after the editor paints -- license tags
|
|
964
|
+
// mount into that same ml-auto sibling and the breadcrumb label swaps to
|
|
965
|
+
// the persisted diagram name -- and either re-render unmounts an already
|
|
966
|
+
// open Radix menu, so re-open it on each attempt instead of clicking into
|
|
967
|
+
// a menu that may already be gone.
|
|
968
|
+
const diagramActions = this.moreActionsButton.last();
|
|
969
|
+
await (0, test_1.expect)(async () => {
|
|
970
|
+
if (await this.renameMenuItem.isVisible()) {
|
|
971
|
+
await this.page.keyboard.press('Escape');
|
|
972
|
+
await (0, test_1.expect)(this.renameMenuItem).toBeHidden({ timeout: 5000 });
|
|
973
|
+
}
|
|
974
|
+
await diagramActions.focus();
|
|
975
|
+
await diagramActions.press('Enter');
|
|
976
|
+
await this.renameMenuItem.click({ timeout: 15000 });
|
|
977
|
+
await (0, test_1.expect)(this.renameInput).toBeVisible({ timeout: 10000 });
|
|
978
|
+
}).toPass({ timeout: 120000, intervals: [1000, 2000, 5000] });
|
|
979
|
+
await this.renameInput.fill(name);
|
|
980
|
+
await this.renameSubmitButton.click();
|
|
981
|
+
await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 10000 });
|
|
982
|
+
}
|
|
983
|
+
async clickVariableInput() {
|
|
984
|
+
await this.variableInput.click();
|
|
985
|
+
}
|
|
986
|
+
async fillVariableInput(variable) {
|
|
987
|
+
await this.variableInput.fill(variable);
|
|
988
|
+
}
|
|
989
|
+
async clickEmbedFormButton() {
|
|
990
|
+
await this.embedFormButton.click({ timeout: 90000 });
|
|
991
|
+
}
|
|
992
|
+
async clickEmbedButton() {
|
|
993
|
+
await this.embedButton.click();
|
|
994
|
+
}
|
|
995
|
+
async clickAdditionalActionsButton() {
|
|
996
|
+
await (0, test_1.expect)(this.additionalActionsButton).toBeVisible({ timeout: 20000 });
|
|
997
|
+
await this.additionalActionsButton.click({ timeout: 60000 });
|
|
998
|
+
}
|
|
999
|
+
async clickDeployMenuItem() {
|
|
1000
|
+
await (0, test_1.expect)(this.deployMenuItem).toBeVisible({ timeout: 20000 });
|
|
1001
|
+
await this.deployMenuItem.click({ timeout: 60000 });
|
|
1002
|
+
}
|
|
1003
|
+
async clickDeployButton() {
|
|
1004
|
+
await this.clickAdditionalActionsButton();
|
|
1005
|
+
await this.clickDeployMenuItem();
|
|
1006
|
+
}
|
|
1007
|
+
async clickDeploySubButton() {
|
|
1008
|
+
await this.deploySubButton.click({ timeout: 90000 });
|
|
1009
|
+
}
|
|
1010
|
+
async clickCancelButton() {
|
|
1011
|
+
await this.cancelButton.click();
|
|
1012
|
+
}
|
|
1013
|
+
async recoverModelerSession(recoverSession) {
|
|
1014
|
+
const rawUrl = this.page.url();
|
|
1015
|
+
let url;
|
|
1016
|
+
if (rawUrl.includes('client_id=web-modeler')) {
|
|
1017
|
+
const match = rawUrl.match(/redirect_uri=([^&]+)/);
|
|
1018
|
+
url = match
|
|
1019
|
+
? decodeURIComponent(match[1]).split('?')[0]
|
|
1020
|
+
: rawUrl.split('?')[0];
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
url = rawUrl.split('?')[0];
|
|
1024
|
+
}
|
|
1025
|
+
if (recoverSession) {
|
|
1026
|
+
// Caller-provided function handles auth + navigation (e.g. navigationPage.goToModeler).
|
|
1027
|
+
// It retries internally; after it returns the session is valid on Modeler home.
|
|
1028
|
+
await recoverSession();
|
|
1029
|
+
}
|
|
1030
|
+
// Navigate to the diagram URL directly — goToModeler lands on Modeler home, and
|
|
1031
|
+
// after a pod restart Modeler also loses OIDC state and redirects home, so we go
|
|
1032
|
+
// to the diagram explicitly while the session is valid.
|
|
1033
|
+
await this.page.goto(url, {
|
|
1034
|
+
timeout: 30000,
|
|
1035
|
+
waitUntil: 'domcontentloaded',
|
|
1036
|
+
});
|
|
1037
|
+
await (0, test_1.expect)(this.secondElement).toBeVisible({ timeout: 60000 });
|
|
1038
|
+
}
|
|
1039
|
+
// The change-element popup virtualizes its list: connector templates render
|
|
1040
|
+
// below the built-in BPMN tasks and are not in the queryable DOM slice until
|
|
1041
|
+
// the list is filtered, so every attempt has to re-type the filter -- a freshly
|
|
1042
|
+
// (re)opened popup is always unfiltered, and asserting straight on the entry
|
|
1043
|
+
// then times out against an off-screen node. Waiting for the search input also
|
|
1044
|
+
// confirms the popup actually opened: the "Change element" click reports
|
|
1045
|
+
// success even when the popup does not render. Use pressSequentially (not
|
|
1046
|
+
// fill): fill sets the value in one shot and the single input event can land
|
|
1047
|
+
// before bpmn-js attaches its search handler during the popup re-render.
|
|
1048
|
+
async searchAndClickRestConnector() {
|
|
1049
|
+
await (0, test_1.expect)(this.changeElementSearchInput).toBeVisible({ timeout: 30000 });
|
|
1050
|
+
// 'Send REST Request' is the template's current name -- connectors #7917
|
|
1051
|
+
// renamed it from 'REST Outbound Connector' on 2026-07-15, and "Outbound"
|
|
1052
|
+
// now appears in no searchable field, so the old filter matched nothing.
|
|
1053
|
+
// Templates live under the popup's "Reusable assets" tab, so searching the
|
|
1054
|
+
// BPMN tab alone never lists them.
|
|
1055
|
+
await this.revealChangeElementTemplate(this.restConnectorOption, 'Send REST Request');
|
|
1056
|
+
await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout: 30000 });
|
|
1057
|
+
await this.restConnectorOption.click({ timeout: 90000 });
|
|
1058
|
+
}
|
|
1059
|
+
async clickRestConnectorOption(recoverSession) {
|
|
1060
|
+
const maxRetries = 4;
|
|
1061
|
+
for (let retries = 0; retries < maxRetries; retries++) {
|
|
1062
|
+
try {
|
|
1063
|
+
if (retries === 0) {
|
|
1064
|
+
// Panel already open from the caller (modelRestConnector in UtilitiesPage)
|
|
1065
|
+
await this.searchAndClickRestConnector();
|
|
1066
|
+
}
|
|
1067
|
+
else if (retries === 1 || retries === 2) {
|
|
1068
|
+
// Recover session cleanly instead of page.reload()
|
|
1069
|
+
await this.recoverModelerSession(recoverSession);
|
|
1070
|
+
await this.secondElement.click({ timeout: 60000 });
|
|
1071
|
+
await this.changeTypeButton.click({ force: true, timeout: 60000 });
|
|
1072
|
+
await this.searchAndClickRestConnector();
|
|
1073
|
+
}
|
|
1074
|
+
else {
|
|
1075
|
+
// Fourth attempt: install via marketplace (real product flow)
|
|
1076
|
+
await this.recoverModelerSession(recoverSession);
|
|
1077
|
+
await this.secondElement.click({ timeout: 60000 });
|
|
1078
|
+
await this.changeTypeButton.click({ force: true, timeout: 60000 });
|
|
1079
|
+
await this.clickMarketPlaceButton();
|
|
1080
|
+
const marketplace = new ConnectorMarketplacePage_1.ConnectorMarketplacePage(this.page);
|
|
1081
|
+
await marketplace.clickSearchForConnectorTextbox();
|
|
1082
|
+
await marketplace.fillSearchForConnectorTextbox('REST Connector');
|
|
1083
|
+
await marketplace.waitForConnectorSearchResults();
|
|
1084
|
+
await marketplace.downloadConnectorToProject();
|
|
1085
|
+
// Marketplace closes the change-type panel; reopen it so the newly
|
|
1086
|
+
// installed connector appears in the list.
|
|
1087
|
+
await this.changeTypeButton.click({ force: true, timeout: 60000 });
|
|
1088
|
+
await this.searchAndClickRestConnector();
|
|
1089
|
+
}
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
console.error(`Click attempt ${retries + 1} failed: ${error}`);
|
|
1094
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
|
|
1098
|
+
}
|
|
1099
|
+
async clickMarketPlaceButton() {
|
|
1100
|
+
const maxRetries = 3;
|
|
1101
|
+
for (let retries = 0; retries < maxRetries; retries++) {
|
|
1102
|
+
try {
|
|
1103
|
+
if (retries === 0) {
|
|
1104
|
+
await this.marketPlaceButton.click({ timeout: 30000 });
|
|
1105
|
+
}
|
|
1106
|
+
else {
|
|
1107
|
+
await this.changeTypeButton.click({ force: true, timeout: 30000 });
|
|
1108
|
+
await this.marketPlaceButton.click({ timeout: 30000 });
|
|
1109
|
+
}
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
catch (error) {
|
|
1113
|
+
console.error(`Click attempt ${retries + 1} failed: ${error}`);
|
|
1114
|
+
await new Promise((resolve) => setTimeout(resolve, 10000));
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
|
|
1118
|
+
}
|
|
1119
|
+
async completeDeploymentEndpointConfiguration() {
|
|
1120
|
+
await (0, test_1.expect)(this.clientIdTextbox).toBeVisible({ timeout: 60000 });
|
|
1121
|
+
await this.clientIdTextbox.click({ timeout: 30000 });
|
|
1122
|
+
await this.clientIdTextbox.fill('test', { timeout: 30000 });
|
|
1123
|
+
await this.clientSecretTextbox.click({ timeout: 30000 });
|
|
1124
|
+
await this.clientSecretTextbox.fill(process.env.DISTRO_QA_E2E_TESTS_KEYCLOAK_CLIENTS_SECRET);
|
|
1125
|
+
await this.rememberCredentialsCheckbox.click({ timeout: 30000 });
|
|
1126
|
+
}
|
|
1127
|
+
async completePlayConfiguration() {
|
|
1128
|
+
const timeout = 30000;
|
|
1129
|
+
// New flow (8.10+): "Set up environment" panel with three steps:
|
|
1130
|
+
// 1. Connect cluster → "Configure environment" modal → select cluster → Save
|
|
1131
|
+
// 2. Deploy process → wait for success banner
|
|
1132
|
+
// 3. Configure test case
|
|
1133
|
+
// The "Test Studio" rebrand gave this button the stable test id
|
|
1134
|
+
// `test-configuration-deploy-button` (confirmed in the failing run's trace
|
|
1135
|
+
// DOM). Prefer it, and keep the label-scoped locator as a fallback for
|
|
1136
|
+
// older modeler builds that render the button without a test id.
|
|
1137
|
+
const setupDeployButton = this.page
|
|
1138
|
+
.getByTestId('test-configuration-deploy-button')
|
|
1139
|
+
.or(this.page
|
|
1140
|
+
.getByText('Deploy process')
|
|
1141
|
+
.locator('..')
|
|
1142
|
+
.getByRole('button', { name: 'Deploy' }))
|
|
1143
|
+
.first();
|
|
1144
|
+
// Both attributes on this button have been renamed by camunda-hub, in
|
|
1145
|
+
// separate commits: a90fd06 changed the label from "Configure scenario" to
|
|
1146
|
+
// "Configure test case", and b3958a1649 (#27259, 2026-08-07) then changed
|
|
1147
|
+
// the test id from `play-configuration-configure-scenario-button` to
|
|
1148
|
+
// `configure-test-case-button`. Match current test id, then pre-rename test
|
|
1149
|
+
// id, then either label, so a further rename of any single attribute cannot
|
|
1150
|
+
// break the step.
|
|
1151
|
+
const configureScenarioButton = this.page
|
|
1152
|
+
.getByTestId('configure-test-case-button')
|
|
1153
|
+
.or(this.page.getByTestId('play-configuration-configure-scenario-button'))
|
|
1154
|
+
.or(this.page.getByRole('button', {
|
|
1155
|
+
name: /^Configure (test case|scenario)$/,
|
|
1156
|
+
}))
|
|
1157
|
+
.first();
|
|
1158
|
+
const configurationOverlay = this.page
|
|
1159
|
+
.locator('[data-test="test-studio-configuration-overlay"]')
|
|
1160
|
+
.or(this.page.locator('[data-test="play-configuration-overlay"]'))
|
|
1161
|
+
.or(this.page.getByText(/set up test run/i))
|
|
1162
|
+
.first();
|
|
1163
|
+
const configureTestPanel = this.page.getByTestId('configure-test-panel');
|
|
1164
|
+
await (0, test_1.expect)(configurationOverlay
|
|
1165
|
+
.or(this.continueToPlayButton)
|
|
1166
|
+
.or(configureTestPanel)
|
|
1167
|
+
.first()).toBeVisible({ timeout: 90000 });
|
|
1168
|
+
if (await configureTestPanel.isVisible()) {
|
|
1169
|
+
// Already past setup for this diagram -- nothing to drive.
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
// Keyed on the overlay itself, not on its Deploy button: when
|
|
1173
|
+
// checkExistingDeployment finds a deployment the overlay opens with the
|
|
1174
|
+
// deploy step already complete, so there is no Deploy button to see and
|
|
1175
|
+
// keying on it would fall through to the legacy Continue flow.
|
|
1176
|
+
const isNewFlow = await configurationOverlay.isVisible();
|
|
1177
|
+
if (isNewFlow) {
|
|
1178
|
+
// Step 1: connect cluster if not yet connected (modal with cluster dropdown)
|
|
1179
|
+
const connectClusterButton = this.page.getByRole('button', {
|
|
1180
|
+
name: 'Select environment',
|
|
1181
|
+
});
|
|
1182
|
+
const needsConnect = await connectClusterButton
|
|
1183
|
+
.isVisible({ timeout: 3000 })
|
|
1184
|
+
.catch(() => false);
|
|
1185
|
+
if (needsConnect) {
|
|
1186
|
+
await connectClusterButton.click({ timeout });
|
|
1187
|
+
// camunda-hub's TestModeDeployEndpointConfigModal replaced the
|
|
1188
|
+
// "Configure environment" combobox dialog with a "Choose where to
|
|
1189
|
+
// run" dialog: environments now render as a radio group (one radio
|
|
1190
|
+
// per environment), not a dropdown. The first environment is
|
|
1191
|
+
// auto-selected once the list loads, but click it explicitly rather
|
|
1192
|
+
// than relying on that default.
|
|
1193
|
+
const configureEnvDialog = this.page
|
|
1194
|
+
.getByRole('dialog')
|
|
1195
|
+
.filter({ hasText: 'Choose where to run' });
|
|
1196
|
+
await configureEnvDialog.getByRole('radio').first().click({ timeout });
|
|
1197
|
+
await configureEnvDialog
|
|
1198
|
+
.getByRole('button', { name: 'Save' })
|
|
1199
|
+
.click({ timeout });
|
|
1200
|
+
}
|
|
1201
|
+
// Step 2: deploy, unless the overlay already opened with the
|
|
1202
|
+
// deploy step complete. checkExistingDeployment only runs when
|
|
1203
|
+
// processApplicationId is defined, which it now is, so re-entering
|
|
1204
|
+
// Play on an already-deployed process application legitimately
|
|
1205
|
+
// has nothing to deploy and renders no Deploy button at all.
|
|
1206
|
+
// The "Configure test case" accordion header is always rendered once
|
|
1207
|
+
// the overlay is up -- it's only enabled once deploy has completed --
|
|
1208
|
+
// so isVisible() alone is always true here and this check must gate on
|
|
1209
|
+
// isEnabled() instead, or Step 2 is skipped even when deploy is still
|
|
1210
|
+
// pending and Step 3 spins forever on a button that never unlocks.
|
|
1211
|
+
const alreadyDeployed = await configureScenarioButton
|
|
1212
|
+
.isEnabled({ timeout: 3000 })
|
|
1213
|
+
.catch(() => false);
|
|
1214
|
+
if (!alreadyDeployed) {
|
|
1215
|
+
// Step 2: deploy
|
|
1216
|
+
await setupDeployButton.click({ timeout });
|
|
1217
|
+
// Deploy is done when the "Configure test case" button unlocks. There
|
|
1218
|
+
// is no `deploy-success-badge` in the Test Studio DOM -- the overlay
|
|
1219
|
+
// stays open with the deploy step showing a green check and that button
|
|
1220
|
+
// enabled (this is how hub's own `Definition.setupEnvironment` e2e page
|
|
1221
|
+
// object drives it: click Deploy, then click Configure, with no badge
|
|
1222
|
+
// in between). On a build that auto-collapses the overlay the
|
|
1223
|
+
// configure-test panel renders instead, so accept either. A failed
|
|
1224
|
+
// deploy leaves the button disabled and the panel absent, so this still
|
|
1225
|
+
// times out on a real deploy failure rather than masking it.
|
|
1226
|
+
await (0, test_1.expect)(async () => {
|
|
1227
|
+
const deployComplete = (await configureScenarioButton
|
|
1228
|
+
.isEnabled({ timeout: 3000 })
|
|
1229
|
+
.catch(() => false)) || (await configureTestPanel.isVisible());
|
|
1230
|
+
(0, test_1.expect)(deployComplete).toBe(true);
|
|
1231
|
+
}).toPass({ timeout: 90000 });
|
|
1232
|
+
}
|
|
1233
|
+
// Step 3: reveal the "Configure test case" section. In the current Test
|
|
1234
|
+
// Studio the "Set up environment" panel is an inline accordion, not a
|
|
1235
|
+
// blocking overlay -- its heading is always present and step 3 auto-opens
|
|
1236
|
+
// once deploy completes, rendering ConfigureTestPanel with the Start
|
|
1237
|
+
// button. Clicking the "Configure test case" accordion heading while it
|
|
1238
|
+
// is already open calls showConfigurationOverlay() and collapses the
|
|
1239
|
+
// panel again, so only click it to OPEN the section when the panel is not
|
|
1240
|
+
// yet visible; keying on the always-present overlay heading here would
|
|
1241
|
+
// re-collapse the panel on every pass.
|
|
1242
|
+
await (0, test_1.expect)(async () => {
|
|
1243
|
+
if (!(await configureTestPanel.isVisible())) {
|
|
1244
|
+
await configureScenarioButton.click({ timeout });
|
|
1245
|
+
}
|
|
1246
|
+
await (0, test_1.expect)(configureTestPanel).toBeVisible({ timeout: 15000 });
|
|
1247
|
+
}).toPass({ timeout: 90000 });
|
|
1248
|
+
return;
|
|
1249
|
+
}
|
|
1250
|
+
// Legacy flow: "Continue" button → "Play environment is ready" confirmation.
|
|
1251
|
+
let attempts = 0;
|
|
1252
|
+
const maxRetries = 2;
|
|
1253
|
+
while (attempts < maxRetries) {
|
|
1254
|
+
try {
|
|
1255
|
+
if (attempts > 0)
|
|
1256
|
+
await this.switchToPlay(); // Call switchToPlay on subsequent attempts
|
|
1257
|
+
await this.continueToPlayButton.click({ timeout });
|
|
1258
|
+
await (0, test_1.expect)(this.page.getByText('Play environment is ready')).toBeVisible({ timeout: 90000 });
|
|
1259
|
+
return; // Exit if successful
|
|
1260
|
+
}
|
|
1261
|
+
catch (error) {
|
|
1262
|
+
if (attempts >= maxRetries - 1)
|
|
1263
|
+
throw error;
|
|
1264
|
+
await this.page.reload();
|
|
1265
|
+
attempts++;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
// Two independent paths put a connector template into the popup's queryable
|
|
1270
|
+
// DOM slice, and neither is reliable alone: selecting the "Reusable assets"
|
|
1271
|
+
// tab renders the template entries directly, while searching flattens the
|
|
1272
|
+
// result list. Probe the option after each step and stop at the first one
|
|
1273
|
+
// that worked, then keep re-issuing the filter while the popup stays open:
|
|
1274
|
+
// the project's connector-template list is fetched asynchronously, so a
|
|
1275
|
+
// filter typed before it lands renders "0 results found" and a single typed
|
|
1276
|
+
// filter never picks the template up afterwards.
|
|
1277
|
+
// Use pressSequentially (not fill): the popup syncs its search state from a
|
|
1278
|
+
// keyup listener only, and fill() dispatches just an input event, so the
|
|
1279
|
+
// filter never applies and the unfiltered BPMN tab keeps rendering.
|
|
1280
|
+
async revealChangeElementTemplate(option, searchTerm) {
|
|
1281
|
+
if (await this.isVisibleWithin(option, 5000)) {
|
|
1282
|
+
return true;
|
|
1283
|
+
}
|
|
1284
|
+
// The tab strip's container class changed across Hub builds; probe the
|
|
1285
|
+
// "Reusable assets" tab itself instead of its wrapper.
|
|
1286
|
+
if (await this.isVisibleWithin(this.reusableAssetsTab, 5000)) {
|
|
1287
|
+
// A failed tab click leaves the search path intact, so do not throw.
|
|
1288
|
+
await this.reusableAssetsTab.click({ timeout: 30000 }).catch(() => { });
|
|
1289
|
+
if (await this.isVisibleWithin(option, 15000)) {
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
if (!(await this.isVisibleWithin(this.changeElementSearchInput, 5000))) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
const deadline = Date.now() + 90000;
|
|
1297
|
+
do {
|
|
1298
|
+
await this.changeElementSearchInput.click();
|
|
1299
|
+
await this.changeElementSearchInput.fill('');
|
|
1300
|
+
await this.changeElementSearchInput.pressSequentially(searchTerm, {
|
|
1301
|
+
delay: 50,
|
|
1302
|
+
});
|
|
1303
|
+
if (await this.isVisibleWithin(option, 15000)) {
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1306
|
+
} while (Date.now() < deadline);
|
|
1307
|
+
return false;
|
|
1308
|
+
}
|
|
1309
|
+
async isVisibleWithin(locator, timeout) {
|
|
1310
|
+
return locator
|
|
1311
|
+
.waitFor({ state: 'visible', timeout })
|
|
1312
|
+
.then(() => true)
|
|
1313
|
+
.catch(() => false);
|
|
1314
|
+
}
|
|
1315
|
+
// The specs reach the Marketplace through this popup's own empty-state link,
|
|
1316
|
+
// and the import modal closes the popup behind it, so the popup is routinely
|
|
1317
|
+
// gone by the time a template is clicked. Reopen it against the element being
|
|
1318
|
+
// replaced first: filtering a closed popup finds no search input and the
|
|
1319
|
+
// option assertion then times out against a popup that was never on screen.
|
|
1320
|
+
async openChangeElementPopup(element) {
|
|
1321
|
+
if (await this.isVisibleWithin(this.changeElementSearchInput, 5000)) {
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
// The context pad only renders for a selected element, so "Change element"
|
|
1325
|
+
// would otherwise never appear. Select with a plain (non-force) click: the
|
|
1326
|
+
// hover/selection that mounts a stable context pad only fires on a real
|
|
1327
|
+
// click, and a forced click skips it so the "Change element" entry never
|
|
1328
|
+
// settles and the click below times out. Reuse clickChangeTypeButton for the
|
|
1329
|
+
// open itself -- its 180s wait plus reload/re-select recovery is the proven
|
|
1330
|
+
// path the first change-element click in each flow already uses, whereas a
|
|
1331
|
+
// bare 30s force-click here has no recovery and loses the reopen race on a
|
|
1332
|
+
// freshly installed cluster.
|
|
1333
|
+
await (0, test_1.expect)(element).toBeVisible({ timeout: 60000 });
|
|
1334
|
+
await element.click({ timeout: 30000 });
|
|
1335
|
+
await this.clickChangeTypeButton();
|
|
1336
|
+
}
|
|
1337
|
+
async clickChangeElementTemplateOption(option, element, searchTerm) {
|
|
1338
|
+
const maxRetries = 3;
|
|
1339
|
+
let lastError;
|
|
1340
|
+
for (let retries = 0; retries < maxRetries; retries++) {
|
|
1341
|
+
try {
|
|
1342
|
+
if (retries > 0) {
|
|
1343
|
+
await this.page.reload();
|
|
1344
|
+
// domcontentloaded is more reliable than networkidle for Modeler,
|
|
1345
|
+
// which maintains persistent background connections (SSE/WebSocket).
|
|
1346
|
+
await this.page.waitForLoadState('domcontentloaded');
|
|
1347
|
+
}
|
|
1348
|
+
await this.openChangeElementPopup(element);
|
|
1349
|
+
if (!(await this.revealChangeElementTemplate(option, searchTerm))) {
|
|
1350
|
+
throw new Error('The connector template never appeared in the change-element popup.');
|
|
1351
|
+
}
|
|
1352
|
+
await option.click({ timeout: 30000 });
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
catch (error) {
|
|
1356
|
+
lastError = error;
|
|
1357
|
+
console.error(`Click attempt ${retries + 1} failed: ${error}`);
|
|
1358
|
+
await (0, sleep_1.sleep)(10000);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
throw new Error(`Failed to click the connector template after ${maxRetries} attempts: ${lastError}`);
|
|
1362
|
+
}
|
|
1363
|
+
async clickWebhookMessageStartEventConnectorOption() {
|
|
1364
|
+
await this.clickChangeElementTemplateOption(this.webhookMessageStartEventConnectorOption, this.startEventElement, 'Webhook');
|
|
1365
|
+
}
|
|
1366
|
+
async clickWebhookIdInput() {
|
|
1367
|
+
await this.webhookIdInput.click();
|
|
1368
|
+
}
|
|
1369
|
+
async clearWebhookIdInput() {
|
|
1370
|
+
await this.webhookIdInput.clear();
|
|
1371
|
+
}
|
|
1372
|
+
async fillWebhookIdInput(webhookId) {
|
|
1373
|
+
await this.webhookIdInput.fill(webhookId);
|
|
1374
|
+
}
|
|
1375
|
+
async hoverOnStartEvent() {
|
|
1376
|
+
await this.hoverOnLocator(this.startEventElement, '.djs-context-pad');
|
|
1377
|
+
}
|
|
1378
|
+
async instanceStartedAssertion() {
|
|
1379
|
+
await (0, test_1.expect)(this.page.getByText('Instance started!')).toBeVisible({
|
|
1380
|
+
timeout: 10000,
|
|
1381
|
+
});
|
|
1382
|
+
}
|
|
1383
|
+
async assertImplementationOption(implementationType) {
|
|
1384
|
+
await this.expandImplementationOptionIfNecessary();
|
|
1385
|
+
await (0, test_1.expect)(this.implementationOptions).toHaveValue(implementationType, {
|
|
1386
|
+
timeout: 30000,
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
async chooseImplementationOption(implementationType) {
|
|
1390
|
+
// Wait for implementation section to be attached and stable after task type change
|
|
1391
|
+
await this.implementationSection.waitFor({
|
|
1392
|
+
state: 'attached',
|
|
1393
|
+
timeout: 10000,
|
|
1394
|
+
});
|
|
1395
|
+
await this.implementationSection.click({ timeout: 30000 });
|
|
1396
|
+
if ((await this.implementationOptions.inputValue()) == implementationType) {
|
|
1397
|
+
console.log(`${implementationType} is already selected.`);
|
|
1398
|
+
}
|
|
1399
|
+
else {
|
|
1400
|
+
await this.implementationOptions.selectOption(implementationType);
|
|
1401
|
+
}
|
|
1402
|
+
await this.assertImplementationOption(implementationType);
|
|
1403
|
+
}
|
|
1404
|
+
async expandImplementationOptionIfNecessary() {
|
|
1405
|
+
// `.bio-properties-panel-group-header empty open` is a descendant
|
|
1406
|
+
// combinator over non-existent `empty`/`open` element types, so it always
|
|
1407
|
+
// counted 0 and every call clicked blindly. Read the entry the caller is
|
|
1408
|
+
// about to assert on instead, and click the header rather than the group
|
|
1409
|
+
// container: the container re-renders while the panel rebuilds after an
|
|
1410
|
+
// id/name edit and detaches mid-click.
|
|
1411
|
+
if (await this.implementationOptions.isVisible().catch(() => false)) {
|
|
1412
|
+
return;
|
|
1413
|
+
}
|
|
1414
|
+
await this.implementationSection.waitFor({
|
|
1415
|
+
state: 'visible',
|
|
1416
|
+
timeout: 30000,
|
|
1417
|
+
});
|
|
1418
|
+
await this.implementationSection
|
|
1419
|
+
.locator('.bio-properties-panel-group-header')
|
|
1420
|
+
.click({ timeout: 60000 });
|
|
1421
|
+
await (0, test_1.expect)(this.implementationOptions).toBeVisible({ timeout: 30000 });
|
|
1422
|
+
}
|
|
1423
|
+
async expandAssignmentSection() {
|
|
1424
|
+
// Scoped to assignmentSection already, so the group-id must not be repeated
|
|
1425
|
+
// here: a self-nested selector never matches, the guard always reads
|
|
1426
|
+
// "collapsed" and the click then toggles an open section shut.
|
|
1427
|
+
const isExpanded = (await this.assignmentSection
|
|
1428
|
+
.locator('.bio-properties-panel-group-header.open')
|
|
1429
|
+
.count()) > 0;
|
|
1430
|
+
if (!isExpanded) {
|
|
1431
|
+
await this.assignmentSection
|
|
1432
|
+
.locator('.bio-properties-panel-group-header')
|
|
1433
|
+
.click({ timeout: 60000 });
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
async clickAppendElementButton() {
|
|
1437
|
+
// hoverOnLocator already retries the hover; a second retry layer here only
|
|
1438
|
+
// multiplied the worst-case wait.
|
|
1439
|
+
await this.hoverOnLocator(this.appendPreButton);
|
|
1440
|
+
}
|
|
1441
|
+
// Owns the whole replace step, popup included: the append leaves the new
|
|
1442
|
+
// (exclusive) gateway selected while the canvas is still panning it into
|
|
1443
|
+
// view, and a forced "Change element" click issued mid-pan lands on empty
|
|
1444
|
+
// canvas -- that clears the selection, closes the context pad, and the
|
|
1445
|
+
// replace popup never opens, so a bare wait on the entry burns its full
|
|
1446
|
+
// timeout with nothing on screen. Pin the shape by id up front so the popup
|
|
1447
|
+
// can be reopened against the same element instead of a positional one.
|
|
1448
|
+
async clickParallelGatewayOption() {
|
|
1449
|
+
const element = await this.pinAppendedGateway();
|
|
1450
|
+
const maxRetries = 3;
|
|
1451
|
+
let lastError;
|
|
1452
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
1453
|
+
try {
|
|
1454
|
+
if (attempt > 1) {
|
|
1455
|
+
// Drop a popup left open on a stale search filter (or on the wrong
|
|
1456
|
+
// element) so the retry reopens a fresh one for `element`.
|
|
1457
|
+
await this.page.keyboard.press('Escape');
|
|
1458
|
+
}
|
|
1459
|
+
await this.openReplacePopup(element, attempt > 1);
|
|
1460
|
+
await this.revealReplaceEntry(this.parallelGatewayOption, 'Parallel gateway');
|
|
1461
|
+
await this.parallelGatewayOption.click({ timeout: 30000 });
|
|
1462
|
+
// bpmn-js closes the popup once an entry is applied, so a popup still
|
|
1463
|
+
// on screen means the click was swallowed and the element was not
|
|
1464
|
+
// replaced.
|
|
1465
|
+
await (0, test_1.expect)(this.changeElementResults).toBeHidden({ timeout: 30000 });
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
catch (error) {
|
|
1469
|
+
lastError = error;
|
|
1470
|
+
const listed = await this.changeElementResults
|
|
1471
|
+
.locator('[data-id]')
|
|
1472
|
+
.evaluateAll((entries) => entries.map((entry) => entry.getAttribute('data-id')).join(', '))
|
|
1473
|
+
.catch(() => '');
|
|
1474
|
+
console.error(`Parallel gateway attempt ${attempt} failed: ${error}; popup entries: [${listed}]`);
|
|
1475
|
+
await (0, sleep_1.sleep)(5000);
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
throw new Error(`Failed to replace the element with a parallel gateway after ${maxRetries} attempts: ${lastError}`);
|
|
1479
|
+
}
|
|
1480
|
+
// The replace popup lists the "replace-with-parallel-gateway" entry only for
|
|
1481
|
+
// a gateway, so it must be opened on the gateway "Append gateway" just added.
|
|
1482
|
+
// bpmn-js appends that shape last, so fall back to the last gateway when the
|
|
1483
|
+
// selection is not a gateway.
|
|
1484
|
+
async pinAppendedGateway() {
|
|
1485
|
+
const selectedId = await this.page
|
|
1486
|
+
.locator('.djs-element.selected')
|
|
1487
|
+
.first()
|
|
1488
|
+
.getAttribute('data-element-id', { timeout: 10000 })
|
|
1489
|
+
.catch(() => null);
|
|
1490
|
+
if (selectedId && selectedId.includes('Gateway')) {
|
|
1491
|
+
return this.page
|
|
1492
|
+
.locator(`.djs-element[data-element-id="${selectedId}"]`)
|
|
1493
|
+
.first();
|
|
1494
|
+
}
|
|
1495
|
+
const lastGatewayId = await this.secondPlacedGateway
|
|
1496
|
+
.getAttribute('data-element-id', { timeout: 30000 })
|
|
1497
|
+
.catch(() => null);
|
|
1498
|
+
return lastGatewayId
|
|
1499
|
+
? this.page
|
|
1500
|
+
.locator(`.djs-element[data-element-id="${lastGatewayId}"]`)
|
|
1501
|
+
.first()
|
|
1502
|
+
: this.secondPlacedGateway;
|
|
1503
|
+
}
|
|
1504
|
+
// diagram-js closes the popup on `canvas.viewbox.changing`, so wait for the
|
|
1505
|
+
// post-append auto-scroll to stop moving the shape before opening it.
|
|
1506
|
+
async waitForShapeToSettle(element) {
|
|
1507
|
+
let previous = await element.boundingBox().catch(() => null);
|
|
1508
|
+
for (let check = 0; check < 20; check++) {
|
|
1509
|
+
await (0, sleep_1.sleep)(250);
|
|
1510
|
+
const current = await element.boundingBox().catch(() => null);
|
|
1511
|
+
if (previous &&
|
|
1512
|
+
current &&
|
|
1513
|
+
previous.x === current.x &&
|
|
1514
|
+
previous.y === current.y) {
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
previous = current;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async openReplacePopup(element, force) {
|
|
1521
|
+
await (0, test_1.expect)(element).toBeVisible({ timeout: 30000 });
|
|
1522
|
+
await this.waitForShapeToSettle(element);
|
|
1523
|
+
if (await this.isVisibleWithin(this.changeElementResults, 2000)) {
|
|
1524
|
+
return;
|
|
1525
|
+
}
|
|
1526
|
+
// "Change element" acts on the selected element, so select the pinned
|
|
1527
|
+
// gateway first: a context pad left open for another shape would open
|
|
1528
|
+
// that shape's replace menu, which never lists a parallel gateway.
|
|
1529
|
+
const isSelected = await element
|
|
1530
|
+
.evaluate((node) => node.classList.contains('selected'))
|
|
1531
|
+
.catch(() => false);
|
|
1532
|
+
if (!isSelected ||
|
|
1533
|
+
!(await this.isVisibleWithin(this.changeTypeButton, 5000))) {
|
|
1534
|
+
await element.click({ force: true, timeout: 30000 });
|
|
1535
|
+
}
|
|
1536
|
+
await this.changeTypeButton.click({ timeout: 60000, force });
|
|
1537
|
+
await (0, test_1.expect)(this.changeElementResults).toBeVisible({ timeout: 30000 });
|
|
1538
|
+
}
|
|
1539
|
+
async revealReplaceEntry(option, searchTerm) {
|
|
1540
|
+
if (await this.isVisibleWithin(option, 5000)) {
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
// The popup lists the project's deployed resources alongside the built-in
|
|
1544
|
+
// replace entries, so the built-in entry can sit below the fold of the
|
|
1545
|
+
// scrolling results list; the popup's own search filters it back into view.
|
|
1546
|
+
if (await this.isVisibleWithin(this.changeElementSearchInput, 5000)) {
|
|
1547
|
+
await this.changeElementSearchInput.click();
|
|
1548
|
+
await this.changeElementSearchInput.fill('');
|
|
1549
|
+
await this.changeElementSearchInput.pressSequentially(searchTerm, {
|
|
1550
|
+
delay: 50,
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
await (0, test_1.expect)(option).toBeVisible({ timeout: 30000 });
|
|
1554
|
+
}
|
|
1555
|
+
async clickConnectToOtherElementButton() {
|
|
1556
|
+
await (0, test_1.expect)(this.connectToOtherElementButton).toBeVisible({
|
|
1557
|
+
timeout: 60000,
|
|
1558
|
+
});
|
|
1559
|
+
await this.connectToOtherElementButton.click({ timeout: 60000 });
|
|
1560
|
+
}
|
|
1561
|
+
async clickPriorityInput() {
|
|
1562
|
+
try {
|
|
1563
|
+
await this.priorityInput.click();
|
|
1564
|
+
}
|
|
1565
|
+
catch (error) {
|
|
1566
|
+
// Re-expand through the guard rather than clicking the section header
|
|
1567
|
+
// directly — a bare click toggles an already-open section shut and takes
|
|
1568
|
+
// the priority entry out of the DOM for the retry below.
|
|
1569
|
+
await this.expandAssignmentSection();
|
|
1570
|
+
await this.priorityInput.click({ timeout: 60000 });
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
async fillPriorityInput(priority) {
|
|
1574
|
+
await this.priorityInput.fill(priority, { timeout: 60000 });
|
|
1575
|
+
await (0, sleep_1.sleep)(1000);
|
|
1576
|
+
}
|
|
1577
|
+
async clickCanvas() {
|
|
1578
|
+
await this.canvas.click({ timeout: 60000 });
|
|
1579
|
+
}
|
|
1580
|
+
async addParallelUserTasks(numberOfTasks, taskName) {
|
|
1581
|
+
try {
|
|
1582
|
+
await this.clickAppendElementButton();
|
|
1583
|
+
}
|
|
1584
|
+
catch (error) {
|
|
1585
|
+
await this.page.reload();
|
|
1586
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
1587
|
+
await sleep(10000);
|
|
1588
|
+
await this.clickFirstPlacedElement();
|
|
1589
|
+
await this.hoverOnLocator(this.appendElementButton);
|
|
1590
|
+
}
|
|
1591
|
+
await this.clickAppendGatewayButton();
|
|
1592
|
+
await this.clickParallelGatewayOption();
|
|
1593
|
+
await this.clickAppendElementButton();
|
|
1594
|
+
await this.clickAppendTaskButton();
|
|
1595
|
+
await this.clickChangeTypeButton();
|
|
1596
|
+
await this.clickUserTaskOption();
|
|
1597
|
+
await this.clickGeneralPropertiesPanel();
|
|
1598
|
+
await this.clickNameInput();
|
|
1599
|
+
await this.fillNameInput(taskName + '1');
|
|
1600
|
+
await this.clickIdInput();
|
|
1601
|
+
await this.fillIdInput(taskName + '1');
|
|
1602
|
+
await this.assertImplementationOption('zeebeUserTask');
|
|
1603
|
+
await this.clickAppendElementButton();
|
|
1604
|
+
await this.clickAppendGatewayButton();
|
|
1605
|
+
await this.clickParallelGatewayOption();
|
|
1606
|
+
for (let i = 2; i <= numberOfTasks; i++) {
|
|
1607
|
+
await this.clickFirstPlacedGateway();
|
|
1608
|
+
await this.clickAppendElementButton();
|
|
1609
|
+
await this.clickAppendTaskButton();
|
|
1610
|
+
await this.clickChangeTypeButton();
|
|
1611
|
+
await this.clickUserTaskOption();
|
|
1612
|
+
await this.clickNameInput();
|
|
1613
|
+
await this.fillNameInput(taskName + i);
|
|
1614
|
+
await this.clickIdInput();
|
|
1615
|
+
await this.fillIdInput(taskName + i);
|
|
1616
|
+
await this.assertImplementationOption('zeebeUserTask');
|
|
1617
|
+
await this.clickConnectToOtherElementButton();
|
|
1618
|
+
await this.clickSecondPlacedGateway();
|
|
1619
|
+
await this.clickCanvas();
|
|
1620
|
+
}
|
|
1621
|
+
await this.clickSecondPlacedGateway();
|
|
1622
|
+
}
|
|
1623
|
+
async setPriority(taskName, priority, numberOfTasks) {
|
|
1624
|
+
for (let i = 1; i <= numberOfTasks; i++) {
|
|
1625
|
+
const userTaskId = taskName + i;
|
|
1626
|
+
const priorityUserTask = this.page.locator(`g[data-element-id="${userTaskId}"]`);
|
|
1627
|
+
await priorityUserTask.click();
|
|
1628
|
+
await this.expandAssignmentSection();
|
|
1629
|
+
await this.clickPriorityInput();
|
|
1630
|
+
await this.fillPriorityInput(priority.toString());
|
|
1631
|
+
await (0, sleep_1.sleep)(5000);
|
|
1632
|
+
await this.clickCanvas();
|
|
1633
|
+
priority += 25;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
async clickIntermediateBoundaryEvent() {
|
|
1637
|
+
if (await this.clickCreatePadEntry('append.intermediate-event')) {
|
|
1638
|
+
return;
|
|
1639
|
+
}
|
|
1640
|
+
await this.appendViaPopup('append-none-intermediate-throwing', ['append-events', 'append-intermediate-throw-events'], 'Intermediate throw event');
|
|
1641
|
+
}
|
|
1642
|
+
async clickIntermediateWebhookConnectorOption() {
|
|
1643
|
+
await this.clickChangeElementTemplateOption(this.intermediateWebhookConnectorOption, this.secondElement, 'Webhook');
|
|
1644
|
+
}
|
|
1645
|
+
async clickCorrelationKeyProcessInput() {
|
|
1646
|
+
await this.correlationKeyProcessInput.click({ timeout: 60000 });
|
|
1647
|
+
}
|
|
1648
|
+
async fillCorrelationKeyProcessInput(correlationKeyProcess) {
|
|
1649
|
+
await this.correlationKeyProcessInput.fill(correlationKeyProcess);
|
|
1650
|
+
await (0, test_1.expect)(this.page.getByText('Correlation key (process) must not be empty.')).not.toBeVisible({ timeout: 30000 });
|
|
1651
|
+
}
|
|
1652
|
+
async clickCorrelationKeyPayloadInput() {
|
|
1653
|
+
await this.correlationKeyPayloadInput.click({ timeout: 60000 });
|
|
1654
|
+
}
|
|
1655
|
+
async fillCorrelationKeyPayloadInput(correlationKeyPayload) {
|
|
1656
|
+
await this.correlationKeyPayloadInput.fill(correlationKeyPayload);
|
|
1657
|
+
await (0, test_1.expect)(this.page.getByText('Correlation key (payload) must not be empty.')).not.toBeVisible({ timeout: 30000 });
|
|
1658
|
+
}
|
|
1659
|
+
async selectUserTask(userTask) {
|
|
1660
|
+
await this.page.locator(`[data-element-id="${userTask}"]`).click();
|
|
1661
|
+
}
|
|
1662
|
+
async clickCandidateUsersInput() {
|
|
1663
|
+
await this.candidateUsersInput.click({ timeout: 60000 });
|
|
1664
|
+
}
|
|
1665
|
+
async fillCandidateUsersInput(candidateUser) {
|
|
1666
|
+
await this.candidateUsersInput.fill(candidateUser, { timeout: 60000 });
|
|
1667
|
+
await this.page.waitForTimeout(1000);
|
|
1668
|
+
}
|
|
1669
|
+
async clickCandidateGroupsInput() {
|
|
1670
|
+
await this.candidateGroupsInput.click({ timeout: 60000 });
|
|
1671
|
+
}
|
|
1672
|
+
async fillCandidateGroupsInput(candidateGroup) {
|
|
1673
|
+
await this.candidateGroupsInput.fill(candidateGroup, { timeout: 60000 });
|
|
1674
|
+
await (0, sleep_1.sleep)(1000);
|
|
1675
|
+
}
|
|
1676
|
+
async clickAssigneeInput() {
|
|
1677
|
+
await this.assigneeInput.click({ timeout: 60000 });
|
|
1678
|
+
}
|
|
1679
|
+
async fillAssigneeInput(name) {
|
|
1680
|
+
await this.assigneeInput.fill(name, { timeout: 60000 });
|
|
1681
|
+
await this.page.waitForTimeout(1000);
|
|
1682
|
+
}
|
|
1683
|
+
async clickTenantIdInput() {
|
|
1684
|
+
await (0, test_1.expect)(this.tenantIdInput).toBeVisible({ timeout: 60000 });
|
|
1685
|
+
await this.tenantIdInput.click({ timeout: 60000 });
|
|
1686
|
+
}
|
|
1687
|
+
async fillTenantIdInput(tenant) {
|
|
1688
|
+
await this.tenantIdInput.fill(tenant, { timeout: 60000 });
|
|
1689
|
+
await (0, sleep_1.sleep)(1000);
|
|
1690
|
+
}
|
|
1691
|
+
async clickTimerEventOption() {
|
|
1692
|
+
// The change-element popup renders its results list asynchronously, so wait
|
|
1693
|
+
// past the 10s default action timeout before clicking.
|
|
1694
|
+
await (0, test_1.expect)(this.timerEventOption).toBeVisible({ timeout: 60000 });
|
|
1695
|
+
await this.timerEventOption.click({ timeout: 30000 });
|
|
1696
|
+
}
|
|
1697
|
+
async clickTimerEventSettings() {
|
|
1698
|
+
await this.timerEventSettings.click();
|
|
1699
|
+
}
|
|
1700
|
+
async selectTimerType(type) {
|
|
1701
|
+
await this.timerType.selectOption(type);
|
|
1702
|
+
await (0, test_1.expect)(this.page.getByText('Type must be defined.')).not.toBeVisible({
|
|
1703
|
+
timeout: 30000,
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
async clickTimerValue() {
|
|
1707
|
+
await this.timerValue.click();
|
|
1708
|
+
}
|
|
1709
|
+
async fillTimerValue(value) {
|
|
1710
|
+
await this.timerValue.fill(value);
|
|
1711
|
+
await (0, test_1.expect)(this.page.getByText('Value must be defined.')).not.toBeVisible({ timeout: 30000 });
|
|
1712
|
+
await (0, sleep_1.sleep)(10000);
|
|
1713
|
+
}
|
|
1714
|
+
async clickEighthPlacedElement() {
|
|
1715
|
+
await this.eighthPlacedElement.click();
|
|
1716
|
+
}
|
|
1717
|
+
async assertNameInput(processName) {
|
|
1718
|
+
try {
|
|
1719
|
+
await (0, test_1.expect)(this.nameInput).toHaveValue(processName, { timeout: 2000 });
|
|
1720
|
+
}
|
|
1721
|
+
catch {
|
|
1722
|
+
await this.nameInput.click();
|
|
1723
|
+
await this.nameInput.fill(processName);
|
|
1724
|
+
await (0, test_1.expect)(this.nameInput).toHaveValue(processName);
|
|
1725
|
+
}
|
|
1726
|
+
}
|
|
1727
|
+
async switchToImplementTab() {
|
|
1728
|
+
await this.implementTab.click({ timeout: 30000 });
|
|
1729
|
+
}
|
|
1730
|
+
async clickStartEvent() {
|
|
1731
|
+
await this.startEventElement.click({ timeout: 30000 });
|
|
1732
|
+
}
|
|
1733
|
+
async clickContinueToPlay() {
|
|
1734
|
+
await this.continueToPlayButton.click({ timeout: 30000 });
|
|
1735
|
+
}
|
|
1736
|
+
async assertScenarioNameVisible(scenarioName) {
|
|
1737
|
+
await (0, test_1.expect)(this.page
|
|
1738
|
+
.getByTestId('instance-header')
|
|
1739
|
+
.getByText(scenarioName)
|
|
1740
|
+
.or(this.page
|
|
1741
|
+
.getByRole('region', { name: /^(Scenarios|Test cases)$/i })
|
|
1742
|
+
.getByText(scenarioName, { exact: false }))).toBeVisible();
|
|
1743
|
+
}
|
|
1744
|
+
async fillStartEventVariablesForDocHandling(zeebeUrl, zeebeClientId, zeebeClientSecret) {
|
|
1745
|
+
console.log(`URL: ${zeebeUrl}, ClientId: ${zeebeClientId}, Secret: ${zeebeClientSecret}`);
|
|
1746
|
+
(0, test_1.expect)(this.connectorsDocHandlingStartEventElement).toBeVisible({
|
|
1747
|
+
timeout: 60000,
|
|
1748
|
+
});
|
|
1749
|
+
await this.connectorsDocHandlingStartEventElement.click();
|
|
1750
|
+
(0, test_1.expect)(this.startEventVariableInputWithThreeVariables).toBeVisible({
|
|
1751
|
+
timeout: 60000,
|
|
1752
|
+
});
|
|
1753
|
+
await this.startEventVariableInputWithThreeVariables.click();
|
|
1754
|
+
await this.fillZeebeRestValue(zeebeUrl);
|
|
1755
|
+
await this.fillZeebeSecretValue(zeebeClientSecret);
|
|
1756
|
+
await this.fillZeebeClientId(zeebeClientId);
|
|
1757
|
+
await (0, sleep_1.sleep)(1000);
|
|
1758
|
+
}
|
|
1759
|
+
async fillZeebeClientId(clientId) {
|
|
1760
|
+
await this.zeebeClientIdVariable.click();
|
|
1761
|
+
await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toBeVisible({
|
|
1762
|
+
timeout: 60000,
|
|
1763
|
+
});
|
|
1764
|
+
await this.variableAssignmentValueTextboxThirdVariable.click();
|
|
1765
|
+
await (0, sleep_1.sleep)(1000);
|
|
1766
|
+
await this.variableAssignmentValueTextboxThirdVariable.fill('');
|
|
1767
|
+
await this.variableAssignmentValueTextboxThirdVariable.fill(clientId);
|
|
1768
|
+
await (0, test_1.expect)(this.variableAssignmentValueTextboxThirdVariable).toHaveText(clientId);
|
|
1769
|
+
}
|
|
1770
|
+
async fillZeebeSecretValue(zeebeSecret) {
|
|
1771
|
+
await this.zeebeSecretVariable.click();
|
|
1772
|
+
await (0, test_1.expect)(this.variableAssignmentValueTextboxSecondVariable).toBeVisible({
|
|
1773
|
+
timeout: 60000,
|
|
1774
|
+
});
|
|
1775
|
+
await this.variableAssignmentValueTextboxSecondVariable.click();
|
|
1776
|
+
await (0, sleep_1.sleep)(1000);
|
|
1777
|
+
await this.variableAssignmentValueTextboxSecondVariable.fill('');
|
|
1778
|
+
await this.variableAssignmentValueTextboxSecondVariable.fill(zeebeSecret);
|
|
1779
|
+
await (0, test_1.expect)(this.variableAssignmentValueTextboxSecondVariable).toHaveText(zeebeSecret);
|
|
1780
|
+
}
|
|
1781
|
+
async fillZeebeRestValue(zeebeUrl) {
|
|
1782
|
+
await this.zeebeRestAddressVariable.click();
|
|
1783
|
+
await (0, test_1.expect)(this.variableAssignmentValueTextbox).toBeVisible({
|
|
1784
|
+
timeout: 60000,
|
|
1785
|
+
});
|
|
1786
|
+
await this.variableAssignmentValueTextbox.click();
|
|
1787
|
+
await (0, sleep_1.sleep)(1000);
|
|
1788
|
+
await this.variableAssignmentValueTextbox.fill('');
|
|
1789
|
+
await this.variableAssignmentValueTextbox.fill(zeebeUrl);
|
|
1790
|
+
await (0, test_1.expect)(this.variableAssignmentValueTextbox).toHaveText(zeebeUrl);
|
|
1791
|
+
}
|
|
1792
|
+
async embedForm(formName) {
|
|
1793
|
+
await this.clickEmbedFormButton();
|
|
1794
|
+
await this.clickForm(formName);
|
|
1795
|
+
await this.clickEmbedButton();
|
|
1796
|
+
}
|
|
1797
|
+
async clickFirstPlaceRESTConnector() {
|
|
1798
|
+
await this.firstPlaceRESTConnector.click({ timeout: 60000 });
|
|
1799
|
+
}
|
|
1800
|
+
async dismissCreatePad() {
|
|
1801
|
+
// The append create-pad stays open for the currently selected element, so
|
|
1802
|
+
// its "Append task" entry keeps intercepting the pointer event when the
|
|
1803
|
+
// next task to click sits under it. Moving the pointer away does not close
|
|
1804
|
+
// it (the pad is anchored to the selection, not the cursor). Clear the
|
|
1805
|
+
// selection by clicking an empty area of the canvas — that detaches the
|
|
1806
|
+
// pad — then wait for it to disappear before the next element click.
|
|
1807
|
+
const createPad = this.page.locator('.djs-create-pad.open').first();
|
|
1808
|
+
if (!(await createPad.isVisible().catch(() => false))) {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
const canvasBox = await this.page
|
|
1812
|
+
.locator('.djs-container')
|
|
1813
|
+
.first()
|
|
1814
|
+
.boundingBox();
|
|
1815
|
+
if (canvasBox) {
|
|
1816
|
+
// A point below the element row (30% across, 65% down) is clear of both
|
|
1817
|
+
// the diagram elements and the bottom zoom toolbar.
|
|
1818
|
+
await this.page.mouse.click(canvasBox.x + canvasBox.width * 0.3, canvasBox.y + canvasBox.height * 0.65);
|
|
1819
|
+
}
|
|
1820
|
+
// Also press Escape and move the pointer off-canvas: the pad can reopen on
|
|
1821
|
+
// hover so a single canvas-click is not always sufficient.
|
|
1822
|
+
await this.page.keyboard.press('Escape');
|
|
1823
|
+
await this.page.mouse.move(0, 0);
|
|
1824
|
+
await createPad
|
|
1825
|
+
.waitFor({ state: 'hidden', timeout: 10000 })
|
|
1826
|
+
.catch(() => false);
|
|
1827
|
+
}
|
|
1828
|
+
async isAuthenticationPanelOpen() {
|
|
1829
|
+
const classes = await this.authenticationPanel
|
|
1830
|
+
.getAttribute('class', { timeout: 5000 })
|
|
1831
|
+
.catch(() => null);
|
|
1832
|
+
return (classes ?? '').split(/\s+/).includes('open');
|
|
1833
|
+
}
|
|
1834
|
+
// The header is a toggle, not an "open" button, and the properties panel
|
|
1835
|
+
// keeps a group's expanded state across element selections — so clicking it
|
|
1836
|
+
// unconditionally collapses a group that a previous element already opened.
|
|
1837
|
+
// Click only while `open` is absent, then wait for it to appear.
|
|
1838
|
+
//
|
|
1839
|
+
// The Authentication group is contributed by the REST connector element
|
|
1840
|
+
// template, so it renders only while the version the diagram pins in
|
|
1841
|
+
// zeebe:modelerTemplateVersion is among the element templates Web Modeler
|
|
1842
|
+
// offers this project. When it is not, the properties panel shows
|
|
1843
|
+
// "Template: Not found" and the group can never appear, so report that as
|
|
1844
|
+
// its own cause instead of a bare 60s click timeout.
|
|
1845
|
+
async openAuthenticationPanel() {
|
|
1846
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1847
|
+
if (await this.isAuthenticationPanelOpen()) {
|
|
1848
|
+
return;
|
|
1849
|
+
}
|
|
1850
|
+
try {
|
|
1851
|
+
await this.authenticationPanel.click({ timeout: 60000 });
|
|
1852
|
+
}
|
|
1853
|
+
catch (err) {
|
|
1854
|
+
const templateMissing = await this.templateNotFoundTag
|
|
1855
|
+
.isVisible()
|
|
1856
|
+
.catch(() => false);
|
|
1857
|
+
if (templateMissing) {
|
|
1858
|
+
throw new Error('REST connector element template did not resolve for this element: ' +
|
|
1859
|
+
'the properties panel shows "Template: Not found", so its ' +
|
|
1860
|
+
'Authentication group never renders. The zeebe:modelerTemplateVersion ' +
|
|
1861
|
+
'pinned in the uploaded fixture is no longer one of the element ' +
|
|
1862
|
+
'template versions this Web Modeler offers — re-pin the fixture to ' +
|
|
1863
|
+
'the offered version.');
|
|
1864
|
+
}
|
|
1865
|
+
throw err;
|
|
1866
|
+
}
|
|
1867
|
+
await (0, test_1.expect)(this.authenticationPanel)
|
|
1868
|
+
.toHaveClass(/(?:^|\s)open(?:\s|$)/, { timeout: 10000 })
|
|
1869
|
+
.catch(() => false);
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
async setOAuthTokenEndpoint(elementToClick, endpoint, options) {
|
|
1873
|
+
const retries = options?.retries ?? 2;
|
|
1874
|
+
const settleMs = options?.settleMs ?? 250;
|
|
1875
|
+
const openPanel = options?.openPanel ?? false;
|
|
1876
|
+
// The create-pad can reappear when Playwright hovers the target to click
|
|
1877
|
+
// it, so dismiss it and retry rather than relying on a single dismissal.
|
|
1878
|
+
let clickError;
|
|
1879
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1880
|
+
await this.dismissCreatePad();
|
|
1881
|
+
try {
|
|
1882
|
+
await elementToClick.click({ timeout: 20000 });
|
|
1883
|
+
clickError = undefined;
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
catch (err) {
|
|
1887
|
+
clickError = err;
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
if (clickError) {
|
|
1891
|
+
throw clickError;
|
|
1892
|
+
}
|
|
1893
|
+
if (openPanel) {
|
|
1894
|
+
await this.clickOpenDetailsPanel();
|
|
1895
|
+
}
|
|
1896
|
+
if (options?.elementId) {
|
|
1897
|
+
await this.ensureElementSelected(elementToClick, options.elementId);
|
|
1898
|
+
}
|
|
1899
|
+
await this.openAuthenticationPanel();
|
|
1900
|
+
let lastError;
|
|
1901
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
1902
|
+
try {
|
|
1903
|
+
await (0, test_1.expect)(this.oauthTokenEndpointInput).toBeVisible({
|
|
1904
|
+
timeout: 10000,
|
|
1905
|
+
});
|
|
1906
|
+
await this.oauthTokenEndpointInput.click({ timeout: 10000 });
|
|
1907
|
+
await this.oauthTokenEndpointInput.fill(endpoint, { timeout: 30000 });
|
|
1908
|
+
return;
|
|
1909
|
+
}
|
|
1910
|
+
catch (err) {
|
|
1911
|
+
lastError = err;
|
|
1912
|
+
// The stale-selection restore can land after ensureElementSelected
|
|
1913
|
+
// has already confirmed the target, detaching the field mid-fill.
|
|
1914
|
+
if (options?.elementId) {
|
|
1915
|
+
await this.ensureElementSelected(elementToClick, options.elementId);
|
|
1916
|
+
}
|
|
1917
|
+
await this.openAuthenticationPanel();
|
|
1918
|
+
await (0, sleep_1.sleep)(settleMs);
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
throw new Error(`Failed to set OAuth 2.0 token endpoint after ${retries} attempt(s). Last error: ${String(lastError)}`);
|
|
1922
|
+
}
|
|
1923
|
+
// After a page reload, reopening the Details panel can restore the
|
|
1924
|
+
// selection from before the reload (and scroll the canvas to it), so the
|
|
1925
|
+
// panel shows a different element than the one just clicked. The target may
|
|
1926
|
+
// then sit under the panel, so re-select it with a DOM click that the panel
|
|
1927
|
+
// cannot intercept.
|
|
1928
|
+
async ensureElementSelected(element, elementId) {
|
|
1929
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
1930
|
+
await (0, sleep_1.sleep)(1000);
|
|
1931
|
+
const selectedId = await this.elemendIdInput
|
|
1932
|
+
.inputValue({ timeout: 10000 })
|
|
1933
|
+
.catch(() => '');
|
|
1934
|
+
if (selectedId === elementId) {
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
await element.dispatchEvent('click');
|
|
1938
|
+
}
|
|
1939
|
+
await (0, test_1.expect)(this.elemendIdInput).toHaveValue(elementId, {
|
|
1940
|
+
timeout: 10000,
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
async clickCloseDetailsPanel() {
|
|
1944
|
+
await this.closeDetailsPanel.click({ timeout: 30000 });
|
|
1945
|
+
}
|
|
1946
|
+
async clickOpenDetailsPanel() {
|
|
1947
|
+
await this.openDetailsPanel.click({ timeout: 30000 });
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
exports.ModelerCreatePage = ModelerCreatePage;
|