@camunda/e2e-test-suite 0.0.1443 → 0.0.1444

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,956 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ModelerHomePage = void 0;
4
+ const test_1 = require("@playwright/test");
5
+ const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
6
+ const randomName_1 = require("../../utils/randomName");
7
+ const NavigationPage_1 = require("./NavigationPage");
8
+ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
9
+ class ModelerHomePage {
10
+ page;
11
+ defaultFolderName = 'Cross Component Test Project';
12
+ // Since camunda/camunda-hub#25831 files and folders cannot sit at a
13
+ // workspace ("project" in today's UI) root -- only process applications and
14
+ // IDP applications can. The container is therefore entered lazily, at the
15
+ // point a resource is created, and remembered for the lifetime of this page
16
+ // object (one instance per test, see fixtures/SM-8.11.ts). Specs that only
17
+ // need the workspace itself never trigger the hop.
18
+ processApplicationPage;
19
+ // Load-bearing: hub renders no process-application breadcrumb on a diagram
20
+ // page at container root, so the remembered URL -- not a breadcrumb click --
21
+ // is how we get back. Hub's own Playwright suite does the same.
22
+ processApplicationUrl;
23
+ processApplicationName;
24
+ // Flag-on (workspaces-enabled) state. Under camunda-hub#27903 the c8-e2e
25
+ // "project folder" maps to a Workspace and the "process application" to a
26
+ // Project: `/` redirects to the `/workspaces` overview, a Workspace holds a
27
+ // projects listing at `/workspaces/{wsId}/projects`, and a Project (the
28
+ // resource container) lives one level deeper. These remember the shared
29
+ // Workspace's projects-listing URL and the overview URL so returns don't
30
+ // re-walk the hierarchy from the root each time.
31
+ workspaceProjectsUrl;
32
+ workspacesOverviewUrl;
33
+ modelerPageBanner;
34
+ createNewProjectButton;
35
+ // /workspaces overview controls (flag-on). The "Create workspace" CTA is
36
+ // always present there (empty-state button, or the toolbar's own once a
37
+ // workspace exists); each existing workspace renders an "Open workspace {name}"
38
+ // tile. See the hub team's HomePage/CreateWorkspacePage page objects.
39
+ createWorkspaceButton;
40
+ workspaceOpenButton;
41
+ noWorkspacesHeading;
42
+ // Router 404: a flag-off deployment has no `/workspaces` route.
43
+ noRouteMatchMessage;
44
+ workspaceWizardHeading;
45
+ workspaceNameInput;
46
+ workspaceDescriptionInput;
47
+ workspaceWizardSubmit;
48
+ workspaceMembersHeading;
49
+ workspaceEnvironmentsHeading;
50
+ projectNameInput;
51
+ workspaceWizardNameInput;
52
+ workspaceWizardSubmitButton;
53
+ workspaceWizardMembersStep;
54
+ workspaceWizardEnvironmentsStep;
55
+ moreActionsButton;
56
+ renameMenuItem;
57
+ renameInput;
58
+ renameSubmitButton;
59
+ chooseBpmnTemplateButton;
60
+ diagramTypeDropdown;
61
+ bpmnTemplateOption;
62
+ formTemplateOption;
63
+ projectBreadcrumb;
64
+ workspaceBreadcrumb;
65
+ openOrganizationsButton;
66
+ manageButton;
67
+ crossComponentProjectFolder;
68
+ homeBreadcrumb;
69
+ formNameInput;
70
+ formEditorCanvas;
71
+ rows;
72
+ licenseKeyTagNonProduction;
73
+ licenseKeyTagNonCommercial;
74
+ licenseKeyTagProduction;
75
+ licenseKeyTagCommercial;
76
+ uploadFilesButton;
77
+ messageBanner;
78
+ closeButton;
79
+ modalCloseButton;
80
+ constructor(page) {
81
+ this.page = page;
82
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
83
+ // Matches every Modeler shell -- see NavigationPage.modelerPageBanner for
84
+ // why the Design System branch excludes the /login href and why both the
85
+ // pre-rebrand "Camunda Modeler home" and post-rebrand "Camunda Hub home"
86
+ // aria-labels are matched.
87
+ this.modelerPageBanner = page
88
+ .getByRole('link', { name: 'Camunda Hub home' })
89
+ .or(page.getByRole('link', { name: 'Camunda Modeler home' }))
90
+ .and(page.locator(':not([href$="/login"])'))
91
+ .or(page.locator('a').filter({ hasText: 'Camunda logoModeler' }))
92
+ .first();
93
+ this.createNewProjectButton = page
94
+ .getByRole('button', {
95
+ name: 'Create new project',
96
+ })
97
+ .or(page.getByRole('button', {
98
+ name: 'New project',
99
+ }))
100
+ .or(page.getByRole('button', {
101
+ name: 'Create project',
102
+ exact: true,
103
+ }))
104
+ .or(page.getByRole('button', {
105
+ name: 'Create workspace',
106
+ exact: true,
107
+ }));
108
+ this.projectNameInput = page.locator('[data-test="editable-input"]');
109
+ // The Design System shell no longer creates an empty project and renames it
110
+ // inline; "Create workspace" opens a two-step wizard (General -> Members)
111
+ // that names the workspace up front. The name lives on the General step, and
112
+ // the submit button keeps this test id across both steps ("Next" on General,
113
+ // "Create workspace" on Members).
114
+ this.workspaceWizardNameInput = page.locator('#workspace-name');
115
+ this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
116
+ this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
117
+ this.workspaceWizardEnvironmentsStep = page.locator('[data-test="step-environments"]');
118
+ // The rename kebab sits beside the breadcrumb in the app header. The
119
+ // editor's action bar is a <header> too, so it also carries the banner
120
+ // role -- getByRole('banner') alone matches three headers in the editor
121
+ // and pulls in "Additional actions" and "Browse more actions". Only the
122
+ // app header holds the breadcrumb nav, so filter on that. The kebab's own
123
+ // name is shell-dependent ("More actions" today, "<segment label> actions"
124
+ // once the design system derives it from the label), so match either.
125
+ this.moreActionsButton = page
126
+ .getByRole('banner')
127
+ .filter({ has: page.getByRole('navigation') })
128
+ .getByRole('button', { name: /actions$/i });
129
+ this.renameMenuItem = page.getByRole('menuitem', { name: 'Rename' });
130
+ this.renameInput = page.locator('[data-test="rename-input"]');
131
+ this.renameSubmitButton = page.locator('[data-test="rename-submit"]');
132
+ this.chooseBpmnTemplateButton = page.getByRole('button', {
133
+ name: 'Choose BPMN template',
134
+ });
135
+ this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
136
+ // Matched on the test id alone: hub's design-system variant of the create
137
+ // menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
138
+ // day that variant ships.
139
+ this.bpmnTemplateOption = page.locator('[data-test="create-bpmn-diagram"]');
140
+ this.formTemplateOption = page.locator('[data-test="create-form"]');
141
+ // The Design System shell drops the breadcrumb-project hook. Its context
142
+ // breadcrumb is org / project / entity, so the project crumb is the second
143
+ // item -- scoped to the breadcrumb nav, never a bare nth() on the page.
144
+ this.projectBreadcrumb = page
145
+ .locator('[data-test="breadcrumb-project"]')
146
+ .or(page
147
+ .locator('nav[aria-label="Context navigation"] [data-slot="nav-breadcrumb-item"]')
148
+ .nth(1)
149
+ .getByRole('button')
150
+ .first())
151
+ .first();
152
+ // Kept separate from projectBreadcrumb, which now means "the container my
153
+ // resources live in". When hub's workspaces terminology flag flips,
154
+ // breadcrumb-project comes to denote the process application, so the
155
+ // workspace level needs its own name from the start. Carries the same
156
+ // Design System fallback as projectBreadcrumb: under that shell neither
157
+ // test-id hook exists, and the workspace level is the project crumb.
158
+ this.workspaceBreadcrumb = page
159
+ .locator('[data-test="breadcrumb-project"]')
160
+ .or(page.locator('[data-test="breadcrumb-workspace"]'))
161
+ .or(page
162
+ .locator('nav[aria-label="Context navigation"] [data-slot="nav-breadcrumb-item"]')
163
+ .nth(1)
164
+ .getByRole('button')
165
+ .first())
166
+ .first();
167
+ this.openOrganizationsButton = page.getByLabel('Open Organizations');
168
+ this.manageButton = page.getByRole('button', { name: 'Manage' });
169
+ // A grid tile opens the workspace through a button, a list-view row
170
+ // through a link.
171
+ this.crossComponentProjectFolder = page
172
+ .getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
173
+ .or(page.getByRole('link', {
174
+ name: `Open workspace ${this.defaultFolderName}`,
175
+ }))
176
+ .or(page.getByTitle(this.defaultFolderName, { exact: true }))
177
+ .first();
178
+ // The Design System shell drops the breadcrumb-home hook and labels the
179
+ // same crumb "My organisation".
180
+ this.homeBreadcrumb = page
181
+ .locator('[data-test="breadcrumb-home"]')
182
+ .or(page.getByRole('button', { name: 'My organisation' }))
183
+ .first();
184
+ this.formNameInput = page.locator('[data-test="editable-input"]');
185
+ // form-js labels its editor region "Form Definition" on both shells (see
186
+ // FormJsPage.formEditor). Unlike the app header, it mounts only once the
187
+ // editor itself has rendered, which makes it the ready signal enterFormName
188
+ // gates on.
189
+ this.formEditorCanvas = page.getByLabel('Form Definition').first();
190
+ this.rows = page.getByRole('row');
191
+ this.licenseKeyTagNonProduction = page
192
+ .getByText('Non-production license')
193
+ .first();
194
+ this.licenseKeyTagNonCommercial = page.getByText('Non-commercial license');
195
+ this.licenseKeyTagProduction = page.getByText('Production license').first();
196
+ this.licenseKeyTagCommercial = page.getByText('Commercial license');
197
+ this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
198
+ this.messageBanner = page.locator('[data-test="close-top-banner"]');
199
+ // The "New features" release modal is a Carbon ComposedModal. When it opens
200
+ // over another always-rendered modal its wrapper carries aria-hidden="true",
201
+ // which drops its buttons from the accessibility tree -- getByRole then never
202
+ // matches and the modal is never dismissed, deadlocking waitForModelerReady.
203
+ // Fall back to a CSS `button` locator (which ignores the accessibility tree)
204
+ // so the visible-but-aria-hidden dismiss control is still found and clicked.
205
+ this.closeButton = page
206
+ .getByRole('button', { name: 'Got it - Dismiss' })
207
+ .or(page.locator('button').filter({ hasText: 'Got it - Dismiss' }))
208
+ .first();
209
+ this.modalCloseButton = page.locator('.cds--modal.is-visible .cds--modal-close');
210
+ // exact:true — the Workspaces overview "Create workspace" split button has a
211
+ // sibling options chevron whose name would otherwise substring-match.
212
+ this.createWorkspaceButton = page.getByRole('button', {
213
+ name: 'Create workspace',
214
+ });
215
+ this.workspaceOpenButton = page
216
+ .getByRole('button', { name: /^open workspace /i })
217
+ .first();
218
+ this.noWorkspacesHeading = page.getByRole('heading', {
219
+ name: /no workspaces/i,
220
+ });
221
+ this.noRouteMatchMessage = page.getByText(/no match for the current location/i);
222
+ // Create-workspace wizard (`/workspaces/create`). Its single primary button
223
+ // relabels itself "Next" -> "Create workspace" as the step changes, so it is
224
+ // located by the stable data-test hook rather than its changing name.
225
+ this.workspaceWizardHeading = page.getByRole('heading', {
226
+ name: 'New workspace',
227
+ });
228
+ this.workspaceNameInput = page.getByLabel('Workspace name');
229
+ this.workspaceDescriptionInput = page.getByLabel('Description');
230
+ this.workspaceWizardSubmit = page.locator('[data-test="workspace-wizard-submit"]');
231
+ this.workspaceMembersHeading = page.getByRole('heading', {
232
+ name: /workspace members/i,
233
+ });
234
+ this.workspaceEnvironmentsHeading = page.getByRole('heading', {
235
+ name: /assign environments/i,
236
+ });
237
+ }
238
+ async clickCreateNewProjectButton() {
239
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewProjectButton, {
240
+ totalTimeout: 30000,
241
+ });
242
+ // Modals (WhatsNew, "BPMN conditional events", etc.) render lazily after
243
+ // page load and intercept clicks on the button underneath. Dismiss them.
244
+ await this.closeButton
245
+ .waitFor({ state: 'visible', timeout: 5000 })
246
+ .then(() => this.closeButton.click())
247
+ .catch(() => { });
248
+ await this.modalCloseButton
249
+ .waitFor({ state: 'visible', timeout: 2000 })
250
+ .then(() => this.modalCloseButton.click())
251
+ .catch(() => { });
252
+ await this.createNewProjectButton.click();
253
+ }
254
+ // The Carbon shell renames a project through an inline editable input on the
255
+ // project page. The Design System shell renders the name as a plain span and
256
+ // moved renaming behind "More actions" > "Rename", which opens a dialog. Wait
257
+ // for whichever affordance the deployed shell offers, then use that one --
258
+ // both images are still in circulation.
259
+ async enterNewProjectName(name) {
260
+ await this.projectNameInput
261
+ .or(this.moreActionsButton)
262
+ .or(this.workspaceWizardNameInput)
263
+ .first()
264
+ .waitFor({ state: 'visible', timeout: 30000 });
265
+ // Design System shell: "Create workspace" opens the creation wizard, which
266
+ // names the workspace on its first step, so there is no post-creation rename
267
+ // to perform. Check this before the inline branch — the wizard replaces it.
268
+ if (await this.workspaceWizardNameInput.isVisible()) {
269
+ await this.createWorkspaceViaWizard(name);
270
+ return;
271
+ }
272
+ // Inline first: when the Carbon shell renders it, it is the whole flow.
273
+ if (await this.projectNameInput.isVisible()) {
274
+ await this.projectNameInput.click({ timeout: 60000 });
275
+ await this.projectNameInput.fill(name);
276
+ await this.projectNameInput.press('Enter');
277
+ return;
278
+ }
279
+ await this.renameViaMoreActions(name);
280
+ }
281
+ // Drives hub's workspace-creation wizard. Environments-enabled builds add an
282
+ // optional third step after Members; older builds create on the second submit.
283
+ async createWorkspaceViaWizard(name) {
284
+ await this.workspaceWizardNameInput.fill(name);
285
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
286
+ await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
287
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
288
+ await this.finishWorkspaceWizard();
289
+ }
290
+ async finishWorkspaceWizard() {
291
+ const needsEnvironmentSubmit = await Promise.race([
292
+ this.page
293
+ .waitForURL(/\/workspaces?\/[^/]+\/projects/, { timeout: 120000 })
294
+ .then(() => false),
295
+ (0, test_1.expect)(this.workspaceWizardEnvironmentsStep)
296
+ .toHaveAttribute('data-state', 'active', { timeout: 120000 })
297
+ .then(() => true),
298
+ ]);
299
+ if (needsEnvironmentSubmit) {
300
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
301
+ await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
302
+ timeout: 120000,
303
+ });
304
+ }
305
+ }
306
+ // Wait for the Modeler project workspace to become interactive. The caller
307
+ // arrives here via goToModeler(), which has already loaded and authenticated
308
+ // the app shell (banner visible). The nav banner mounts before the project
309
+ // list, which renders after an API round-trip, so gate on an actionable
310
+ // element — the "Create new project" button or an existing project folder —
311
+ // rather than the banner, which otherwise lets the caller proceed against a
312
+ // half-loaded shell and time out clicking "Create new project".
313
+ //
314
+ // Deliberately does NOT reload: a raw page.reload() drops the authenticated
315
+ // SPA state (goToModeler handles the OAuth redirect flow; reload does not),
316
+ // which lands on a blank page and makes the workspace element unreachable for
317
+ // the rest of the timeout. Transient upstream 50x is already absorbed by
318
+ // goToModeler's gateway-error re-navigation before we get here.
319
+ async waitForModelerReady(timeoutMs = 120000) {
320
+ // Flag-on: `/` redirects to the `/workspaces` overview, which renders the
321
+ // "Create workspace" CTA (always present) plus an "Open workspace ..." tile
322
+ // per existing workspace — the legacy "Create new project" button never
323
+ // renders there, which is what used to time out. Gate on those overview
324
+ // markers, the projects-listing "Create project" CTA (landed on directly
325
+ // after creating a workspace, or after opening one that has no projects
326
+ // yet -- neither overview marker renders there), and the legacy flag-off
327
+ // home markers, so the wait resolves whichever shell the deployment
328
+ // renders. .first(): several can be present at once (e.g. the CTA and a
329
+ // tile), and waitFor() requires a single match.
330
+ const workspaceElement = this.createWorkspaceButton
331
+ .or(this.workspaceOpenButton)
332
+ .or(this.createNewProjectButton)
333
+ .or(this.crossComponentProjectFolder)
334
+ .or(this.processApplicationPage.createProcessApplicationButton)
335
+ .first();
336
+ // The "New features" release modal renders lazily over the Modeler home,
337
+ // and under the Design System shell the project list -- the "Create new
338
+ // project" button and any existing folder included -- does not mount until
339
+ // the modal is dismissed. Gating on the workspace element alone therefore
340
+ // deadlocks: it never appears while the modal is up, and the dismissal in
341
+ // clickMessageBanner/clickCreateNewProjectButton only runs after this wait.
342
+ // Dismiss the modal on each poll until the workspace element mounts.
343
+ try {
344
+ await (0, test_1.expect)(async () => {
345
+ await this.clickMessageBanner();
346
+ await (0, test_1.expect)(workspaceElement).toBeVisible({ timeout: 5000 });
347
+ }).toPass({ timeout: timeoutMs, intervals: [1000, 2000, 5000] });
348
+ }
349
+ catch (error) {
350
+ // Re-label the timeout with the real cause; "element(s) not found" reads
351
+ // as a slow-loading project list. Outside the poll because toPass()
352
+ // retries thrown errors.
353
+ if (await this.noRouteMatchMessage.isVisible().catch(() => false)) {
354
+ throw new Error(`Web Modeler served a router 404 ("No match for the current location") for ${this.page.url()}. ` +
355
+ 'The route does not exist in this deployment -- e.g. /workspaces on a ' +
356
+ 'build where the Workspaces feature flag (workspacesEnabled) is off.');
357
+ }
358
+ throw error;
359
+ }
360
+ }
361
+ // `credentials`: the identity already logged in on this page (e.g. the RBA/
362
+ // multi-tenancy test user a preceding goToModeler(undefined, credentials)
363
+ // authenticated as). Only needed if that identity is not the default
364
+ // Identity first user -- pass it through so a flag-off fallback re-navigation
365
+ // doesn't silently switch principals mid-test.
366
+ async createCrossComponentProjectFolder(credentials) {
367
+ // Kept name: opens (creating if absent) the shared Workspace this suite puts
368
+ // every test's Project inside, leaving the browser on the Workspace's
369
+ // projects listing. The flag-off era created a top-level project folder here;
370
+ // under flag-on that folder maps to a Workspace.
371
+ await this.ensureWorkspaceOpen(credentials);
372
+ }
373
+ // Only an org admin can assign Environments to a workspace (Hub 8.10), so a
374
+ // workspace created by a plain Member has nothing to deploy to until one does.
375
+ // Call while this page is inside that workspace: assigns an Environment as the
376
+ // default Identity first user (an org admin) in a separate browser context,
377
+ // leaving this page's session untouched.
378
+ async assignWorkspaceEnvironmentAsOrgAdmin(browser) {
379
+ const workspaceUrl = this.page.url();
380
+ const context = await browser.newContext({
381
+ baseURL: new URL(workspaceUrl).origin,
382
+ });
383
+ try {
384
+ const adminPage = await context.newPage();
385
+ await new NavigationPage_1.NavigationPage(adminPage).goToModeler();
386
+ await new ProcessApplicationPage_1.ProcessApplicationPage(adminPage).assignEnvironmentToWorkspace(workspaceUrl, process.env.MODELER_CLUSTER_NAME);
387
+ }
388
+ finally {
389
+ await context.close();
390
+ }
391
+ // This page loaded the workspace before the admin assigned the
392
+ // Environment in the other session, so Hub still holds the empty
393
+ // environment list and Deploy & run has no target until it refetches.
394
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
395
+ this.processApplicationPage.markWorkspaceEnvironmentAssignedByAdmin(workspaceUrl);
396
+ }
397
+ async clickCrossComponentProjectFolder(credentials) {
398
+ // Kept name: callers use this to (re-)enter the shared Workspace's projects
399
+ // listing after leaving Modeler. Creates the Workspace if it is somehow
400
+ // absent (e.g. a fresh cluster where the setup spec did not run first).
401
+ await this.ensureWorkspaceOpen(credentials);
402
+ }
403
+ // Ensures the shared "Cross Component Test Project" Workspace is open and the
404
+ // browser is on its projects listing (`/workspaces/{wsId}/projects`), where the
405
+ // per-test Project is later created. Retries the whole navigation+login on a
406
+ // transient web-modeler 500 rather than a bare reload, which would drop the
407
+ // authenticated SPA state and land on the empty login form.
408
+ //
409
+ // `credentials` must be the identity already active on `page` (the caller's
410
+ // own goToModeler login), not a new one to log in as: the flag-off branch
411
+ // re-navigates mid-flow, and NavigationPage.goToModeler() defaults to the
412
+ // first Identity user when credentials are omitted, which would silently
413
+ // swap principals for RBA/multi-tenancy callers that authenticated as a
414
+ // specific test user before calling this.
415
+ async ensureWorkspaceOpen(credentials) {
416
+ const maxAttempts = 3;
417
+ const retryWaitMs = 5000;
418
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
419
+ try {
420
+ // Go straight to the Workspaces overview rather than waiting on the
421
+ // post-login landing: after the OIDC callback the app briefly sits on
422
+ // Home (or /login-callback), which is not the workspace listing and on
423
+ // some builds does not auto-redirect to /workspaces. Navigating here
424
+ // explicitly is robust whether or not the root redirect fires.
425
+ await this.gotoWorkspacesOverview();
426
+ // Flag-off serves a router 404 here, not Home as this fallback once
427
+ // assumed. Neither the overview nor the legacy markers can render on a
428
+ // no-match page, so checking readiness first burned the full 120s and
429
+ // threw, leaving the flag-off branch below unreachable.
430
+ const routed404 = await this.becameVisible(this.noRouteMatchMessage, 5000);
431
+ // Still confirm an overview marker: waitForModelerReady() also accepts
432
+ // the legacy Home markers, so "no 404" alone is not proof of flag-on.
433
+ let flagOn = false;
434
+ if (!routed404) {
435
+ await this.waitForModelerReady();
436
+ await this.clickMessageBanner();
437
+ flagOn = await this.becameVisible(this.createWorkspaceButton.or(this.workspaceOpenButton).first(), 30000);
438
+ }
439
+ if (!flagOn) {
440
+ // Legacy flag-off deployment (e.g. a pre-upgrade version in a
441
+ // migration flow): the shared folder IS the top-level project. Must
442
+ // re-navigate — the legacy markers never appear on the 404 page.
443
+ // Pass through the caller's credentials so this doesn't fall back to
444
+ // the default Identity user mid-test (see the credentials note above).
445
+ await new NavigationPage_1.NavigationPage(this.page).goToModeler(undefined, credentials);
446
+ await this.waitForModelerReady();
447
+ await this.clickMessageBanner();
448
+ await this.openOrCreateLegacyFolder();
449
+ return;
450
+ }
451
+ // On the flag-on overview. Remember it for clickHomeBreadcrumb().
452
+ this.workspacesOverviewUrl = this.page.url();
453
+ const workspaceExists = await this.crossComponentProjectFolder
454
+ .isVisible()
455
+ .catch(() => false);
456
+ if (!workspaceExists) {
457
+ // Creating the workspace lands the creator (a Workspace Admin) on its
458
+ // projects listing directly, so there is no tile to open afterwards.
459
+ await this.createWorkspace(this.defaultFolderName);
460
+ return;
461
+ }
462
+ await this.openWorkspaceTile();
463
+ return;
464
+ }
465
+ catch (error) {
466
+ if (attempt < maxAttempts - 1) {
467
+ console.log(`ensureWorkspaceOpen attempt ${attempt + 1} failed. Re-authenticating and retrying...`);
468
+ try {
469
+ await new NavigationPage_1.NavigationPage(this.page).goToModeler();
470
+ }
471
+ catch (navError) {
472
+ console.log(`Re-navigation to Modeler failed: ${navError}`);
473
+ }
474
+ await this.page.waitForTimeout(retryWaitMs);
475
+ }
476
+ else {
477
+ throw error;
478
+ }
479
+ }
480
+ }
481
+ }
482
+ // Navigates to the Workspaces overview by resolving it against the Modeler app
483
+ // root in the current URL, so it works whether Modeler is served under
484
+ // `/modeler` (Self-Managed combined ingress) or at the app root.
485
+ async gotoWorkspacesOverview() {
486
+ const current = new URL(this.page.url());
487
+ const appRootMatch = current.pathname.match(/^(.*\/modeler)(?:\/|$)/);
488
+ const appRoot = appRootMatch ? appRootMatch[1] : '';
489
+ await this.page.goto(`${current.origin}${appRoot}/workspaces`, {
490
+ waitUntil: 'domcontentloaded',
491
+ });
492
+ }
493
+ // Walks the create-workspace wizard (`/workspaces/create`): fill the general
494
+ // step, advance through any optional steps, and submit with the creator only
495
+ // (the creator is pre-added as Workspace Admin, satisfying the "at least one
496
+ // admin" guard). On success the hub routes the role-holding creator straight
497
+ // to the new workspace's projects listing, not back to the `/workspaces`
498
+ // overview, so gate on that landing (singular or plural route).
499
+ async createWorkspace(name) {
500
+ await (0, test_1.expect)(this.createWorkspaceButton).toBeVisible({ timeout: 60000 });
501
+ await this.createWorkspaceButton.click({ timeout: 60000 });
502
+ await (0, test_1.expect)(this.workspaceWizardHeading).toBeVisible({ timeout: 60000 });
503
+ await this.workspaceNameInput.fill(name);
504
+ await this.workspaceDescriptionInput
505
+ .fill('Created by the cross-component E2E suite.')
506
+ .catch(() => { });
507
+ await this.submitWorkspaceWizard();
508
+ await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
509
+ timeout: 120000,
510
+ });
511
+ await this.waitForModelerReady();
512
+ this.workspaceProjectsUrl = this.page.url();
513
+ await this.clickMessageBanner();
514
+ }
515
+ // Advances the create-workspace wizard from the (already filled) General step
516
+ // to workspace creation. The wizard renders a variable number of steps:
517
+ // General is always present, while Members and Environments each render only
518
+ // for an org admin with the feature enabled (see camunda-hub
519
+ // CreateWorkspace.tsx / buildSteps). Its single primary submit button
520
+ // (`data-test=workspace-wizard-submit`) reads "Next" on every step except the
521
+ // last, where it reads "Create workspace". Advance while it reads "Next" —
522
+ // gating each click on the currently visible step section being replaced so a
523
+ // fast re-read cannot click the same "Next" twice — then submit to create.
524
+ // Only one step section renders at a time, so the visible-section list doubles
525
+ // as a per-step marker.
526
+ async submitWorkspaceWizard() {
527
+ const submit = this.workspaceWizardSubmit.first();
528
+ const stepSections = [
529
+ this.workspaceNameInput,
530
+ this.workspaceMembersHeading,
531
+ this.workspaceEnvironmentsHeading,
532
+ ];
533
+ const createsWorkspace = async () => /create workspace/i.test(((await submit.textContent()) ?? '').trim());
534
+ for (let step = 0; step < stepSections.length; step++) {
535
+ await (0, test_1.expect)(submit).toBeEnabled({ timeout: 60000 });
536
+ if (await createsWorkspace()) {
537
+ break;
538
+ }
539
+ let current;
540
+ for (const section of stepSections) {
541
+ if (await section.isVisible().catch(() => false)) {
542
+ current = section;
543
+ break;
544
+ }
545
+ }
546
+ await submit.click({ timeout: 30000 });
547
+ if (current) {
548
+ await current.waitFor({ state: 'hidden', timeout: 60000 });
549
+ }
550
+ }
551
+ await (0, test_1.expect)(submit).toHaveText(/create workspace/i, { timeout: 60000 });
552
+ await submit.click({ timeout: 30000 });
553
+ }
554
+ // Opens the shared Workspace's tile and waits until its projects listing has
555
+ // loaded, remembering that URL for later returns via clickWorkspaceBreadcrumb.
556
+ async openWorkspaceTile() {
557
+ await (0, test_1.expect)(this.crossComponentProjectFolder).toBeVisible({
558
+ timeout: 90000,
559
+ });
560
+ await this.dismissModals();
561
+ // Click the tile's "Open workspace" button specifically: the card also
562
+ // carries a plain title element (and a "Manage workspace" button) that the
563
+ // crossComponentProjectFolder .or(title) locator can resolve to first, and
564
+ // clicking those does not navigate to the projects listing.
565
+ const openButton = this.page.getByRole('button', {
566
+ name: `Open workspace ${this.defaultFolderName}`,
567
+ });
568
+ const target = (await openButton
569
+ .first()
570
+ .isVisible()
571
+ .catch(() => false))
572
+ ? openButton.first()
573
+ : this.crossComponentProjectFolder;
574
+ await target.click({ timeout: 60000 });
575
+ // Optional `s`: camunda-hub#28928 renames the in-workspace projects listing
576
+ // from singular `/workspace/{id}/projects` to plural `/workspaces/{id}/projects`
577
+ // with no redirect, and that PR is unmerged — so this suite runs against both
578
+ // (released/main hub still serves singular, the #28928 branch serves plural).
579
+ // Gate on either so the wait resolves whichever route the hub under test serves.
580
+ await this.page.waitForURL(/\/workspaces?\/[^/]+\/projects/, {
581
+ timeout: 120000,
582
+ });
583
+ this.workspaceProjectsUrl = this.page.url();
584
+ await this.clickMessageBanner();
585
+ }
586
+ // Legacy flag-off flow, kept for pre-upgrade UIs in migration specs: the shared
587
+ // folder is the top-level project, created inline via "Create new project".
588
+ async openOrCreateLegacyFolder() {
589
+ if (await this.crossComponentProjectFolder.isVisible().catch(() => false)) {
590
+ await this.dismissModals();
591
+ await this.crossComponentProjectFolder.click({ timeout: 60000 });
592
+ await this.clickMessageBanner();
593
+ return;
594
+ }
595
+ await this.clickCreateNewProjectButton();
596
+ await this.enterNewProjectName(this.defaultFolderName);
597
+ await (0, test_1.expect)(this.page.getByRole('heading', { name: this.defaultFolderName })).toBeVisible({ timeout: 10000 });
598
+ }
599
+ // Dismisses the lazily-rendered "New features" / conditional-events modals that
600
+ // can intercept a click on the workspace tile or create controls.
601
+ async dismissModals() {
602
+ await this.closeButton
603
+ .waitFor({ state: 'visible', timeout: 5000 })
604
+ .then(() => this.closeButton.click())
605
+ .catch(() => { });
606
+ await this.modalCloseButton
607
+ .waitFor({ state: 'visible', timeout: 2000 })
608
+ .then(() => this.modalCloseButton.click())
609
+ .catch(() => { });
610
+ }
611
+ // Opens a resource where this test put it. Several specs leave Modeler
612
+ // entirely (to Optimize, to Operate) and come back via
613
+ // clickCrossComponentProjectFolder(), which lands at workspace root -- the
614
+ // resource is one level down, so re-enter the container first.
615
+ async clickProcessDiagram(name) {
616
+ await this.ensureInResourceContainer();
617
+ // A Design System table row opens through a link laid over the row, which
618
+ // sits on top of the title text and takes its clicks -- so click the link.
619
+ // The link comes before the title in DOM order, so `first()` picks it
620
+ // whenever it exists; the title stays as the fallback for the classic table.
621
+ const row = this.rows.filter({ hasText: name });
622
+ const process = row
623
+ .getByRole('link', { name })
624
+ .or(row.getByTitle(name))
625
+ .first();
626
+ await (0, test_1.expect)(process).toBeVisible({ timeout: 90000 });
627
+ await process.click();
628
+ }
629
+ async clickHomeBreadcrumb() {
630
+ // Flag-on: "home" for Modeler is the `/workspaces` overview (the top of the
631
+ // hierarchy), remembered when the Workspace was opened.
632
+ if (this.workspacesOverviewUrl) {
633
+ await this.page.goto(this.workspacesOverviewUrl, {
634
+ waitUntil: 'domcontentloaded',
635
+ });
636
+ await this.waitForModelerReady();
637
+ return;
638
+ }
639
+ try {
640
+ await this.homeBreadcrumb.click({ timeout: 60000 });
641
+ }
642
+ catch (error) {
643
+ await this.page
644
+ .getByText('Home', { exact: true })
645
+ .first()
646
+ .click({ timeout: 60000 });
647
+ }
648
+ }
649
+ async clickChooseBpmnTemplateButton() {
650
+ await this.chooseBpmnTemplateButton.click();
651
+ }
652
+ // Opens the create menu inside this test's process application. The menu at
653
+ // workspace root no longer offers BPMN, Form or Upload -- those resources
654
+ // cannot live there -- so the container is entered first. Callers are
655
+ // unchanged, which is why the ~150 existing spec call sites need no edit.
656
+ async clickDiagramTypeDropdown() {
657
+ await this.enterResourceContainer();
658
+ const dropdown = this.processApplicationPage.createNewDropdown;
659
+ // Recovery must re-navigate to the container's absolute URL, not blindly
660
+ // page.reload(). Flag-on Modeler runs a silent Keycloak token refresh that
661
+ // routes through /modeler/login -> /modeler/login-callback and, once the
662
+ // deep link is lost, lands on the /modeler/workspaces overview -- which has
663
+ // no "Create new" menu. A page.reload() there just reloads /workspaces, so
664
+ // the dropdown never reappears and every "create a diagram" flow times out.
665
+ // Re-entering via processApplicationUrl (goto) puts us back on the Project
666
+ // detail where the menu renders, matching the goto-based recovery used by
667
+ // returnToResourceContainer() elsewhere in this page object.
668
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, dropdown, {
669
+ totalTimeout: 180000,
670
+ maxRetries: 6,
671
+ postAction: async () => {
672
+ if (this.processApplicationUrl) {
673
+ await this.page.goto(this.processApplicationUrl, {
674
+ waitUntil: 'domcontentloaded',
675
+ });
676
+ await this.clickMessageBanner();
677
+ }
678
+ else {
679
+ await this.page.reload();
680
+ }
681
+ },
682
+ });
683
+ await dropdown.click();
684
+ }
685
+ async clickBpmnTemplateOption() {
686
+ await (0, test_1.expect)(this.bpmnTemplateOption).toBeVisible({ timeout: 60000 });
687
+ await this.bpmnTemplateOption.click({ timeout: 60000 });
688
+ }
689
+ async clickFormOption() {
690
+ await this.formTemplateOption.click();
691
+ }
692
+ // Same shell split as enterNewProjectName: the Design System form editor has
693
+ // no inline editable input and renames through the breadcrumb's "More
694
+ // actions" > "Rename" dialog.
695
+ async enterFormName(name) {
696
+ // Wait for the form editor itself before probing rename affordances. The
697
+ // project page also renders a "More actions" button, so the or() gate below
698
+ // is satisfied while the caller's navigation is still in flight -- which
699
+ // renamed the PROJECT to the form's name instead of the form. The Carbon
700
+ // shell could not hit this: editable-input existed only inside the editor,
701
+ // so the gate implicitly waited for navigation.
702
+ //
703
+ // waitForURL is not enough on its own: the URL flips to /forms/ before the
704
+ // editor mounts, and the app header -- kebab included -- is already painted
705
+ // by then, still showing the process application as the deepest crumb. The
706
+ // canvas mounts together with the entity crumb, so gate on it rather than
707
+ // on the header, which is present either way.
708
+ await this.page.waitForURL(/\/forms\//, { timeout: 60000 });
709
+ // The URL flips before the editor mounts, and the app header re-renders as
710
+ // it does. Renaming in that window finds the kebab but loses the menu it
711
+ // opens, so wait for the editor canvas first. Soft: on a shell that labels
712
+ // the canvas differently the retry in renameViaMoreActions still covers us.
713
+ await this.becameVisible(this.formEditorCanvas, 60000);
714
+ await this.formNameInput
715
+ .or(this.formEditorCanvas)
716
+ .first()
717
+ .waitFor({ state: 'visible', timeout: 30000 });
718
+ if (await this.formNameInput.isVisible()) {
719
+ await this.formNameInput.click({ timeout: 60000 });
720
+ await this.formNameInput.fill(name);
721
+ await this.formNameInput.press('Enter');
722
+ return;
723
+ }
724
+ await this.renameViaMoreActions(name);
725
+ }
726
+ // Today the shell renders one kebab for the whole breadcrumb; camunda-hub
727
+ // #28045 gives every segment its own, so take the LAST -- the entity being
728
+ // edited is the deepest crumb. With one kebab last() is that same button.
729
+ //
730
+ // The header's license tags sit in an ml-auto flex sibling that overlaps the
731
+ // kebab once they render, so a mouse click lands on the tag instead. Open the
732
+ // menu from the keyboard -- same control, reached the way a keyboard user
733
+ // reaches it, and unaffected by what paints over it.
734
+ //
735
+ // The header keeps re-rendering for a while after the editor mounts (the
736
+ // breadcrumb swap, then the license tags arriving in that ml-auto sibling),
737
+ // and each re-render unmounts the open menu: the nightly trace shows "Rename"
738
+ // resolving, reporting "element is not stable", then "element was detached
739
+ // from the DOM". Reopen the menu and try again rather than racing a single
740
+ // open; the last attempt throws so a genuinely missing menu still fails.
741
+ // A freshly created entity's file data lands after the editor has already
742
+ // painted, and the header re-renders when it does -- which unmounts the open
743
+ // Radix menu mid-click ("element was detached from the DOM, retrying") and
744
+ // leaves it closed for the rest of the click's budget. Open the menu and pick
745
+ // Rename as one retried unit so a remount costs an iteration instead of the
746
+ // test. Escape first: the trigger toggles, so an attempt that left the menu
747
+ // open would otherwise have the next Enter close it again.
748
+ async renameViaMoreActions(name) {
749
+ await this.waitForEntityCrumb();
750
+ const entityActions = this.moreActionsButton.last();
751
+ // The header keeps settling after the editor paints: the license tags
752
+ // mount into the ml-auto sibling and the breadcrumb label swaps from its
753
+ // placeholder to the persisted entity name. Either re-render shifts and
754
+ // then unmounts the open Radix menu ("element is not stable", then
755
+ // "element was detached from the DOM"), and the menu item never comes
756
+ // back because the menu itself closed. Re-open the menu on each attempt
757
+ // rather than clicking into one that may already be gone.
758
+ await (0, test_1.expect)(async () => {
759
+ if (await this.renameMenuItem.isVisible()) {
760
+ await this.page.keyboard.press('Escape');
761
+ await (0, test_1.expect)(this.renameMenuItem).toBeHidden({ timeout: 5000 });
762
+ }
763
+ await entityActions.focus();
764
+ await entityActions.press('Enter');
765
+ await this.renameMenuItem.click({ timeout: 15000 });
766
+ await (0, test_1.expect)(this.renameInput).toBeVisible({ timeout: 10000 });
767
+ }).toPass({ timeout: 120000, intervals: [1000, 2000, 5000] });
768
+ await this.renameInput.fill(name);
769
+ await this.renameSubmitButton.click();
770
+ await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 10000 });
771
+ }
772
+ // The breadcrumb is built up progressively: on a freshly created resource the
773
+ // container is briefly the deepest crumb and owns the only kebab, and the
774
+ // entity crumb is appended a render later. Reopening the menu is not enough
775
+ // on its own -- an attempt that lands inside that window opens the
776
+ // CONTAINER's menu, so "Rename" there renames the process application
777
+ // instead of the entity, and the entity crumb's arrival re-renders the
778
+ // breadcrumb and unmounts the menu mid-click. Wait for the deepest kebab to
779
+ // stop being the container's. Shells that label the kebab generically
780
+ // ("More actions") never match a container label, so this is a no-op there.
781
+ async waitForEntityCrumb() {
782
+ if (!this.processApplicationName) {
783
+ return;
784
+ }
785
+ const containerActions = `${this.processApplicationName} actions`;
786
+ await test_1.expect
787
+ .poll(async () => (await this.moreActionsButton
788
+ .last()
789
+ .getAttribute('aria-label')
790
+ .catch(() => null)) ?? '', { timeout: 30000 })
791
+ .not.toBe(containerActions);
792
+ }
793
+ // Kept under its original name because it is what ~10 call sites already
794
+ // mean: "go back to where my resources live". That is now the process
795
+ // application, not the workspace root.
796
+ async clickProjectBreadcrumb() {
797
+ await this.returnToResourceContainer();
798
+ }
799
+ // The workspace ("project") level itself, for the surfaces that genuinely
800
+ // live there -- collaborators, IDP applications, published element
801
+ // templates. Under flag-on this is the Workspace's projects listing.
802
+ async clickWorkspaceBreadcrumb() {
803
+ if (this.workspaceProjectsUrl) {
804
+ await this.page.goto(this.workspaceProjectsUrl, {
805
+ waitUntil: 'domcontentloaded',
806
+ });
807
+ await this.processApplicationPage.createProcessApplicationButton
808
+ .waitFor({ state: 'visible', timeout: 90000 })
809
+ .catch(() => { });
810
+ await this.clickMessageBanner();
811
+ return;
812
+ }
813
+ await this.workspaceBreadcrumb.click({ timeout: 60000 });
814
+ }
815
+ // Creates, once per test, the process application that holds this test's
816
+ // resources, and leaves the browser inside it. Idempotent: called again while
817
+ // already in the container it is a no-op, so a spec that creates several
818
+ // resources in a row pays for the container once.
819
+ //
820
+ // One container per test rather than one shared with the seeded workspace:
821
+ // deploying is process-application-wide, so a shared container would let one
822
+ // spec's deploy pick up every other spec's half-modelled diagram.
823
+ async enterResourceContainer(name) {
824
+ if (this.processApplicationUrl) {
825
+ const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
826
+ if (!stillInside) {
827
+ await this.returnToResourceContainer();
828
+ }
829
+ return;
830
+ }
831
+ this.processApplicationName =
832
+ name ??
833
+ (await (0, randomName_1.randomNameAgregator)('Cross Component Process Application'));
834
+ console.log(`Creating process application ${this.processApplicationName} inside ${this.defaultFolderName}`);
835
+ await this.clickCreateProcessApplication();
836
+ await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
837
+ this.processApplicationUrl = this.page.url();
838
+ this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
839
+ // Under workspaces-enabled, a freshly created process application has no
840
+ // cluster assigned to its development stage: Deploy (and Play's "Configure
841
+ // environment") render a "Clusters not configured for stages" warning and
842
+ // stay disabled until one is. Assign it once here, right after the
843
+ // container is created, so every later deploy/run/play/connector/form flow
844
+ // in this test finds a usable cluster instead of independently reproducing
845
+ // this connection step (or failing on the warning). ensureStageCluster()
846
+ // memoizes per test, so runProcessInstance()'s own call is a no-op after
847
+ // this.
848
+ await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
849
+ }
850
+ // Which control creates a process application depends on the hub version
851
+ // under test. Since camunda/camunda-hub#25831 it is a plain primary button on
852
+ // the workspace root, and the "Create new" menu explicitly hides the item
853
+ // (hideProcessApplication) because a process application is no longer just
854
+ // another resource. Before that change the menu item was the only way in.
855
+ // Race both entry points so neither version pays a full timeout, then use
856
+ // whichever one rendered.
857
+ async clickCreateProcessApplication() {
858
+ const button = this.processApplicationPage.createProcessApplicationButton;
859
+ const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
860
+ if (!rootLoaded) {
861
+ await this.page.reload();
862
+ }
863
+ // Short: the joint probe above already settled which branch rendered, and
864
+ // hub renders exactly one of them (EntityTable renders the toolbar's create
865
+ // menu OR the empty state, never both), so this only confirms which.
866
+ if (await this.becameVisible(button, 5000)) {
867
+ await this.processApplicationPage.clickCreateProcessApplicationButton();
868
+ return;
869
+ }
870
+ await this.diagramTypeDropdown.click({ timeout: 60000 });
871
+ await this.processApplicationPage.clickCreateProcessApplicationOption();
872
+ }
873
+ // Re-enters this test's container if it already has one, without creating
874
+ // one: opening an existing resource must not have the side effect of
875
+ // creating a container for a spec that never asked for it.
876
+ async ensureInResourceContainer() {
877
+ if (!this.processApplicationUrl) {
878
+ return;
879
+ }
880
+ await this.enterResourceContainer();
881
+ }
882
+ // Navigates back to this test's process application by the remembered URL
883
+ // rather than by a breadcrumb click: hub renders no process-application
884
+ // breadcrumb on a diagram page at container root, and the breadcrumb that
885
+ // does exist on the container page doubles as its rename control. Hub's own
886
+ // Playwright suite navigates back the same way.
887
+ async returnToResourceContainer() {
888
+ if (!this.processApplicationUrl) {
889
+ await this.clickWorkspaceBreadcrumb();
890
+ return;
891
+ }
892
+ await this.page.goto(this.processApplicationUrl, {
893
+ waitUntil: 'domcontentloaded',
894
+ });
895
+ await this.processApplicationPage.waitForProcessApplication();
896
+ await this.clickMessageBanner();
897
+ }
898
+ // Single seam for "give me an empty BPMN diagram in this test's container".
899
+ // A process application always ships an auto-created main process; this adds
900
+ // a new diagram rather than adopting that one, so every existing modelling
901
+ // assertion keeps its current subject.
902
+ async startBlankBpmnDiagram() {
903
+ await this.clickDiagramTypeDropdown();
904
+ await this.clickBpmnTemplateOption();
905
+ }
906
+ async clickOpenOrganizationsButton() {
907
+ await this.openOrganizationsButton.click({ timeout: 30000 });
908
+ }
909
+ async clickManageButton() {
910
+ await this.manageButton.click({ timeout: 30000 });
911
+ }
912
+ async clickUploadFilesButton() {
913
+ await this.uploadFilesButton.click({ timeout: 60000 });
914
+ }
915
+ // Dismisses both Modeler overlays that can sit over the project list.
916
+ async clickMessageBanner() {
917
+ // Maintenance/downtime banner
918
+ if (await this.overlayBecameVisible(this.messageBanner)) {
919
+ try {
920
+ await this.messageBanner.click();
921
+ }
922
+ catch {
923
+ // Banner dismissed itself between the probe and the click
924
+ }
925
+ }
926
+ // "New features" modal, dismissed after the banner rather than through an
927
+ // .or() chain: that chain stopped at whichever overlay matched first,
928
+ // leaving the other one on top to intercept the caller's next click.
929
+ if (await this.overlayBecameVisible(this.closeButton)) {
930
+ try {
931
+ await this.closeButton.click();
932
+ }
933
+ catch {
934
+ // Modal dismissed itself between the probe and the click
935
+ }
936
+ }
937
+ }
938
+ // Boolean visibility probe. waitFor rather than isVisible, which resolves
939
+ // immediately and would report a false negative on a still-rendering page.
940
+ async becameVisible(locator, timeout) {
941
+ return locator
942
+ .waitFor({ state: 'visible', timeout })
943
+ .then(() => true)
944
+ .catch(() => false);
945
+ }
946
+ // Probe with waitFor, not isVisible: the "New features" modal renders lazily
947
+ // after page load, and isVisible resolves immediately — Playwright ignores
948
+ // its timeout option — so it misses an overlay that has yet to paint.
949
+ async overlayBecameVisible(overlay) {
950
+ return overlay
951
+ .waitFor({ state: 'visible', timeout: 2000 })
952
+ .then(() => true)
953
+ .catch(() => false);
954
+ }
955
+ }
956
+ exports.ModelerHomePage = ModelerHomePage;