@camunda/e2e-test-suite 0.0.1443 → 0.0.1445
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/c8_versions.json +1 -0
- package/README.md +2 -0
- package/dist/fixtures/SM-8.11.d.ts +77 -0
- package/dist/fixtures/SM-8.11.js +176 -0
- package/dist/pages/SM-8.11/Authorization.d.ts +39 -0
- package/dist/pages/SM-8.11/Authorization.js +349 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.d.ts +8 -0
- package/dist/pages/SM-8.11/ClusterDetailsPage.js +65 -0
- package/dist/pages/SM-8.11/ClusterPage.d.ts +11 -0
- package/dist/pages/SM-8.11/ClusterPage.js +37 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.d.ts +27 -0
- package/dist/pages/SM-8.11/ConnectorMarketplacePage.js +187 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.d.ts +41 -0
- package/dist/pages/SM-8.11/ConnectorSettingsPage.js +153 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.d.ts +11 -0
- package/dist/pages/SM-8.11/ConnectorTemplatePage.js +34 -0
- package/dist/pages/SM-8.11/FormJsPage.d.ts +33 -0
- package/dist/pages/SM-8.11/FormJsPage.js +109 -0
- package/dist/pages/SM-8.11/HomePage.d.ts +20 -0
- package/dist/pages/SM-8.11/HomePage.js +49 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.d.ts +53 -0
- package/dist/pages/SM-8.11/IdentityTenantPage.js +264 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.d.ts +92 -0
- package/dist/pages/SM-8.11/IdentityUsersPage.js +485 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakAdminPage.js +78 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.d.ts +12 -0
- package/dist/pages/SM-8.11/KeycloakLoginPage.js +32 -0
- package/dist/pages/SM-8.11/KeycloakUtils.d.ts +17 -0
- package/dist/pages/SM-8.11/KeycloakUtils.js +94 -0
- package/dist/pages/SM-8.11/LoginPage.d.ts +21 -0
- package/dist/pages/SM-8.11/LoginPage.js +112 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.d.ts +44 -0
- package/dist/pages/SM-8.11/ManagementIdentityPage.js +176 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.d.ts +238 -0
- package/dist/pages/SM-8.11/ModelerCreatePage.js +1950 -0
- package/dist/pages/SM-8.11/ModelerHomePage.d.ts +97 -0
- package/dist/pages/SM-8.11/ModelerHomePage.js +956 -0
- package/dist/pages/SM-8.11/NavigationPage.d.ts +58 -0
- package/dist/pages/SM-8.11/NavigationPage.js +389 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.d.ts +10 -0
- package/dist/pages/SM-8.11/OCIdentityAuthorizationsPage.js +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.d.ts +22 -0
- package/dist/pages/SM-8.11/OCIdentityClusterVariablesPage.js +120 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.d.ts +39 -0
- package/dist/pages/SM-8.11/OCIdentityGroupsPage.js +154 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityHomePage.js +115 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.d.ts +23 -0
- package/dist/pages/SM-8.11/OCIdentityMappingRulesPage.js +116 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.d.ts +61 -0
- package/dist/pages/SM-8.11/OCIdentityRolesPage.js +433 -0
- package/dist/pages/SM-8.11/OCTenantPage.d.ts +73 -0
- package/dist/pages/SM-8.11/OCTenantPage.js +399 -0
- package/dist/pages/SM-8.11/OperateHomePage.d.ts +26 -0
- package/dist/pages/SM-8.11/OperateHomePage.js +139 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.d.ts +30 -0
- package/dist/pages/SM-8.11/OperateProcessInstancePage.js +276 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/OperateProcessesPage.js +383 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.d.ts +17 -0
- package/dist/pages/SM-8.11/OptimizeCollectionsPage.js +62 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.d.ts +14 -0
- package/dist/pages/SM-8.11/OptimizeDashboardPage.js +63 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.d.ts +19 -0
- package/dist/pages/SM-8.11/OptimizeHomePage.js +76 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.d.ts +50 -0
- package/dist/pages/SM-8.11/OptimizeReportPage.js +209 -0
- package/dist/pages/SM-8.11/PlayPage.d.ts +47 -0
- package/dist/pages/SM-8.11/PlayPage.js +389 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.d.ts +35 -0
- package/dist/pages/SM-8.11/ProcessApplicationPage.js +340 -0
- package/dist/pages/SM-8.11/SettingsPage.d.ts +12 -0
- package/dist/pages/SM-8.11/SettingsPage.js +34 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.d.ts +70 -0
- package/dist/pages/SM-8.11/TaskDetailsPage.js +273 -0
- package/dist/pages/SM-8.11/TaskPanelPage.d.ts +29 -0
- package/dist/pages/SM-8.11/TaskPanelPage.js +320 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.d.ts +15 -0
- package/dist/pages/SM-8.11/TaskProcessesPage.js +77 -0
- package/dist/pages/SM-8.11/UtilitiesPage.d.ts +38 -0
- package/dist/pages/SM-8.11/UtilitiesPage.js +573 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.d.ts +6 -0
- package/dist/pages/SM-8.11/optimizeReportUtils.js +77 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-management-user-flows.spec.js +48 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/cluster-variables.spec.js +226 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/connectors-user-flows.spec.js +307 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/document-handling-user-flows.spec.js +174 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hto-user-flows.spec.js +410 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/hub-web-modeler-smoke.spec.js +24 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/identity-user-flows.spec.js +220 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/license-key-user-flows.spec.js +461 -0
- package/dist/tests/SM-8.11/login.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/login.spec.js +24 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/migration-path-user-flows.spec.js +263 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/mt-enabled-user-flows.spec.js +607 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/optimize-user-flows.spec.js +505 -0
- package/dist/tests/SM-8.11/play.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/play.spec.js +144 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/rba-enabled-user-flows.spec.js +712 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/smoke-tests.spec.js +167 -0
- package/dist/tests/SM-8.11/test-setup.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/test-setup.spec.js +24 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/topology-orchestration-smoke.spec.js +58 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.d.ts +1 -0
- package/dist/tests/SM-8.11/web-modeler-user-flows.spec.js +223 -0
- package/package.json +1 -1
- package/playwright.config.ts +21 -0
|
@@ -0,0 +1,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;
|