@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.
Files changed (122) hide show
  1. package/.github/c8_versions.json +1 -0
  2. package/README.md +2 -0
  3. package/dist/fixtures/SM-8.11.d.ts +77 -0
  4. package/dist/fixtures/SM-8.11.js +176 -0
  5. package/dist/pages/SM-8.11/Authorization.d.ts +39 -0
  6. package/dist/pages/SM-8.11/Authorization.js +349 -0
  7. package/dist/pages/SM-8.11/ClusterDetailsPage.d.ts +8 -0
  8. package/dist/pages/SM-8.11/ClusterDetailsPage.js +65 -0
  9. package/dist/pages/SM-8.11/ClusterPage.d.ts +11 -0
  10. package/dist/pages/SM-8.11/ClusterPage.js +37 -0
  11. package/dist/pages/SM-8.11/ConnectorMarketplacePage.d.ts +27 -0
  12. package/dist/pages/SM-8.11/ConnectorMarketplacePage.js +187 -0
  13. package/dist/pages/SM-8.11/ConnectorSettingsPage.d.ts +41 -0
  14. package/dist/pages/SM-8.11/ConnectorSettingsPage.js +153 -0
  15. package/dist/pages/SM-8.11/ConnectorTemplatePage.d.ts +11 -0
  16. package/dist/pages/SM-8.11/ConnectorTemplatePage.js +34 -0
  17. package/dist/pages/SM-8.11/FormJsPage.d.ts +33 -0
  18. package/dist/pages/SM-8.11/FormJsPage.js +109 -0
  19. package/dist/pages/SM-8.11/HomePage.d.ts +20 -0
  20. package/dist/pages/SM-8.11/HomePage.js +49 -0
  21. package/dist/pages/SM-8.11/IdentityTenantPage.d.ts +53 -0
  22. package/dist/pages/SM-8.11/IdentityTenantPage.js +264 -0
  23. package/dist/pages/SM-8.11/IdentityUsersPage.d.ts +92 -0
  24. package/dist/pages/SM-8.11/IdentityUsersPage.js +485 -0
  25. package/dist/pages/SM-8.11/KeycloakAdminPage.d.ts +17 -0
  26. package/dist/pages/SM-8.11/KeycloakAdminPage.js +78 -0
  27. package/dist/pages/SM-8.11/KeycloakLoginPage.d.ts +12 -0
  28. package/dist/pages/SM-8.11/KeycloakLoginPage.js +32 -0
  29. package/dist/pages/SM-8.11/KeycloakUtils.d.ts +17 -0
  30. package/dist/pages/SM-8.11/KeycloakUtils.js +94 -0
  31. package/dist/pages/SM-8.11/LoginPage.d.ts +21 -0
  32. package/dist/pages/SM-8.11/LoginPage.js +112 -0
  33. package/dist/pages/SM-8.11/ManagementIdentityPage.d.ts +44 -0
  34. package/dist/pages/SM-8.11/ManagementIdentityPage.js +176 -0
  35. package/dist/pages/SM-8.11/ModelerCreatePage.d.ts +238 -0
  36. package/dist/pages/SM-8.11/ModelerCreatePage.js +1950 -0
  37. package/dist/pages/SM-8.11/ModelerHomePage.d.ts +97 -0
  38. package/dist/pages/SM-8.11/ModelerHomePage.js +956 -0
  39. package/dist/pages/SM-8.11/NavigationPage.d.ts +58 -0
  40. package/dist/pages/SM-8.11/NavigationPage.js +389 -0
  41. package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.d.ts +10 -0
  42. package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.js +22 -0
  43. package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.d.ts +22 -0
  44. package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.js +120 -0
  45. package/dist/pages/SM-8.11/OCIdentityGroupsPage.d.ts +39 -0
  46. package/dist/pages/SM-8.11/OCIdentityGroupsPage.js +154 -0
  47. package/dist/pages/SM-8.11/OCIdentityHomePage.d.ts +23 -0
  48. package/dist/pages/SM-8.11/OCIdentityHomePage.js +115 -0
  49. package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.d.ts +23 -0
  50. package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.js +116 -0
  51. package/dist/pages/SM-8.11/OCIdentityRolesPage.d.ts +61 -0
  52. package/dist/pages/SM-8.11/OCIdentityRolesPage.js +433 -0
  53. package/dist/pages/SM-8.11/OCTenantPage.d.ts +73 -0
  54. package/dist/pages/SM-8.11/OCTenantPage.js +399 -0
  55. package/dist/pages/SM-8.11/OperateHomePage.d.ts +26 -0
  56. package/dist/pages/SM-8.11/OperateHomePage.js +139 -0
  57. package/dist/pages/SM-8.11/OperateProcessInstancePage.d.ts +30 -0
  58. package/dist/pages/SM-8.11/OperateProcessInstancePage.js +276 -0
  59. package/dist/pages/SM-8.11/OperateProcessesPage.d.ts +38 -0
  60. package/dist/pages/SM-8.11/OperateProcessesPage.js +383 -0
  61. package/dist/pages/SM-8.11/OptimizeCollectionsPage.d.ts +17 -0
  62. package/dist/pages/SM-8.11/OptimizeCollectionsPage.js +62 -0
  63. package/dist/pages/SM-8.11/OptimizeDashboardPage.d.ts +14 -0
  64. package/dist/pages/SM-8.11/OptimizeDashboardPage.js +63 -0
  65. package/dist/pages/SM-8.11/OptimizeHomePage.d.ts +19 -0
  66. package/dist/pages/SM-8.11/OptimizeHomePage.js +76 -0
  67. package/dist/pages/SM-8.11/OptimizeReportPage.d.ts +50 -0
  68. package/dist/pages/SM-8.11/OptimizeReportPage.js +209 -0
  69. package/dist/pages/SM-8.11/PlayPage.d.ts +47 -0
  70. package/dist/pages/SM-8.11/PlayPage.js +389 -0
  71. package/dist/pages/SM-8.11/ProcessApplicationPage.d.ts +35 -0
  72. package/dist/pages/SM-8.11/ProcessApplicationPage.js +340 -0
  73. package/dist/pages/SM-8.11/SettingsPage.d.ts +12 -0
  74. package/dist/pages/SM-8.11/SettingsPage.js +34 -0
  75. package/dist/pages/SM-8.11/TaskDetailsPage.d.ts +70 -0
  76. package/dist/pages/SM-8.11/TaskDetailsPage.js +273 -0
  77. package/dist/pages/SM-8.11/TaskPanelPage.d.ts +29 -0
  78. package/dist/pages/SM-8.11/TaskPanelPage.js +320 -0
  79. package/dist/pages/SM-8.11/TaskProcessesPage.d.ts +15 -0
  80. package/dist/pages/SM-8.11/TaskProcessesPage.js +77 -0
  81. package/dist/pages/SM-8.11/UtilitiesPage.d.ts +38 -0
  82. package/dist/pages/SM-8.11/UtilitiesPage.js +573 -0
  83. package/dist/pages/SM-8.11/optimizeReportUtils.d.ts +6 -0
  84. package/dist/pages/SM-8.11/optimizeReportUtils.js +77 -0
  85. package/dist/tests/SM-8.11/cluster-management-user-flows.spec.d.ts +1 -0
  86. package/dist/tests/SM-8.11/cluster-management-user-flows.spec.js +48 -0
  87. package/dist/tests/SM-8.11/cluster-variables.spec.d.ts +1 -0
  88. package/dist/tests/SM-8.11/cluster-variables.spec.js +226 -0
  89. package/dist/tests/SM-8.11/connectors-user-flows.spec.d.ts +1 -0
  90. package/dist/tests/SM-8.11/connectors-user-flows.spec.js +307 -0
  91. package/dist/tests/SM-8.11/document-handling-user-flows.spec.d.ts +1 -0
  92. package/dist/tests/SM-8.11/document-handling-user-flows.spec.js +174 -0
  93. package/dist/tests/SM-8.11/hto-user-flows.spec.d.ts +1 -0
  94. package/dist/tests/SM-8.11/hto-user-flows.spec.js +410 -0
  95. package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.d.ts +1 -0
  96. package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.js +24 -0
  97. package/dist/tests/SM-8.11/identity-user-flows.spec.d.ts +1 -0
  98. package/dist/tests/SM-8.11/identity-user-flows.spec.js +220 -0
  99. package/dist/tests/SM-8.11/license-key-user-flows.spec.d.ts +1 -0
  100. package/dist/tests/SM-8.11/license-key-user-flows.spec.js +461 -0
  101. package/dist/tests/SM-8.11/login.spec.d.ts +1 -0
  102. package/dist/tests/SM-8.11/login.spec.js +24 -0
  103. package/dist/tests/SM-8.11/migration-path-user-flows.spec.d.ts +1 -0
  104. package/dist/tests/SM-8.11/migration-path-user-flows.spec.js +263 -0
  105. package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.d.ts +1 -0
  106. package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.js +607 -0
  107. package/dist/tests/SM-8.11/optimize-user-flows.spec.d.ts +1 -0
  108. package/dist/tests/SM-8.11/optimize-user-flows.spec.js +505 -0
  109. package/dist/tests/SM-8.11/play.spec.d.ts +1 -0
  110. package/dist/tests/SM-8.11/play.spec.js +144 -0
  111. package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.d.ts +1 -0
  112. package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.js +712 -0
  113. package/dist/tests/SM-8.11/smoke-tests.spec.d.ts +1 -0
  114. package/dist/tests/SM-8.11/smoke-tests.spec.js +167 -0
  115. package/dist/tests/SM-8.11/test-setup.spec.d.ts +1 -0
  116. package/dist/tests/SM-8.11/test-setup.spec.js +24 -0
  117. package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.d.ts +1 -0
  118. package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.js +58 -0
  119. package/dist/tests/SM-8.11/web-modeler-user-flows.spec.d.ts +1 -0
  120. package/dist/tests/SM-8.11/web-modeler-user-flows.spec.js +223 -0
  121. package/package.json +1 -1
  122. 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;