@camunda/e2e-test-suite 0.0.1011 → 0.0.1013
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/pages/8.9/FormJsPage.d.ts +2 -0
- package/dist/pages/8.9/FormJsPage.js +74 -1
- package/dist/pages/8.9/ModelerCreatePage.d.ts +1 -0
- package/dist/pages/8.9/ModelerCreatePage.js +157 -20
- package/dist/pages/8.9/ModelerHomePage.d.ts +12 -0
- package/dist/pages/8.9/ModelerHomePage.js +152 -7
- package/dist/pages/8.9/ProcessApplicationPage.d.ts +30 -0
- package/dist/pages/8.9/ProcessApplicationPage.js +260 -0
- package/dist/pages/8.9/UtilitiesPage.js +8 -0
- package/dist/pages/SM-8.9/ModelerHomePage.d.ts +7 -0
- package/dist/pages/SM-8.9/ModelerHomePage.js +97 -2
- package/dist/pages/SM-8.9/ProcessApplicationPage.d.ts +22 -0
- package/dist/pages/SM-8.9/ProcessApplicationPage.js +121 -0
- package/dist/tests/8.9/access-user-role-management.spec.js +3 -3
- package/dist/tests/8.9/idp-user-flows.spec.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Locator, Page } from '@playwright/test';
|
|
2
|
+
declare class ProcessApplicationPage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly createProcessApplicationButton: Locator;
|
|
5
|
+
readonly createProcessApplicationOption: Locator;
|
|
6
|
+
readonly creationDialog: Locator;
|
|
7
|
+
readonly nameInput: Locator;
|
|
8
|
+
readonly submitButton: Locator;
|
|
9
|
+
readonly cancelButton: Locator;
|
|
10
|
+
readonly breadcrumb: Locator;
|
|
11
|
+
readonly folderExplorer: Locator;
|
|
12
|
+
readonly createNewDropdown: Locator;
|
|
13
|
+
readonly inlineNameInput: Locator;
|
|
14
|
+
readonly defineStagesDialog: Locator;
|
|
15
|
+
readonly developmentClusterSelect: Locator;
|
|
16
|
+
readonly defineStagesSaveButton: Locator;
|
|
17
|
+
constructor(page: Page);
|
|
18
|
+
private pageReady;
|
|
19
|
+
waitForProcessApplication(timeout?: number): Promise<void>;
|
|
20
|
+
clickCreateProcessApplicationButton(): Promise<void>;
|
|
21
|
+
clickCreateProcessApplicationOption(): Promise<void>;
|
|
22
|
+
nameNewProcessApplication(name: string): Promise<void>;
|
|
23
|
+
defineDevelopmentStage(clusterName?: string): Promise<void>;
|
|
24
|
+
rememberResourceContainer(url: string): void;
|
|
25
|
+
forgetStageCluster(): void;
|
|
26
|
+
ensureStageCluster(clusterName: string): Promise<boolean>;
|
|
27
|
+
private openStageDefinition;
|
|
28
|
+
openByName(name: string): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
export { ProcessApplicationPage };
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProcessApplicationPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
|
|
6
|
+
// Which process application holds the current test's resources, and which
|
|
7
|
+
// cluster its development stage currently points at. Keyed by the Playwright
|
|
8
|
+
// Page because the object that creates the container (ModelerHomePage) and the
|
|
9
|
+
// objects that later need a specific cluster inside it (ModelerCreatePage,
|
|
10
|
+
// FormJsPage) hold separate ProcessApplicationPage instances but share one page.
|
|
11
|
+
const resourceContainers = new WeakMap();
|
|
12
|
+
// Since camunda/camunda-hub#25831 a process application is a first-class
|
|
13
|
+
// container rather than a folder, and a workspace root ("project" in today's
|
|
14
|
+
// UI) holds nothing but process applications and IDP applications. Every file
|
|
15
|
+
// and folder this suite creates therefore has to live inside a process
|
|
16
|
+
// application. This page object owns that container's surface only; entering
|
|
17
|
+
// and leaving it is driven from ModelerHomePage so the ~150 existing spec call
|
|
18
|
+
// sites keep working unchanged.
|
|
19
|
+
class ProcessApplicationPage {
|
|
20
|
+
page;
|
|
21
|
+
// Empty workspace root: a plain primary button rendered next to the "Create
|
|
22
|
+
// new" menu (hub's EmptyStateActionButtons).
|
|
23
|
+
createProcessApplicationButton;
|
|
24
|
+
// Non-empty root: the item inside the "Create new" menu. Hub passes
|
|
25
|
+
// hideProcessApplication at workspace level, so this is the fallback rather
|
|
26
|
+
// than the primary path -- its own unit test asserts the item is absent
|
|
27
|
+
// there. Kept because the Hub PR is still moving this surface.
|
|
28
|
+
createProcessApplicationOption;
|
|
29
|
+
creationDialog;
|
|
30
|
+
nameInput;
|
|
31
|
+
submitButton;
|
|
32
|
+
cancelButton;
|
|
33
|
+
breadcrumb;
|
|
34
|
+
folderExplorer;
|
|
35
|
+
createNewDropdown;
|
|
36
|
+
inlineNameInput;
|
|
37
|
+
// "Define stages" dialog, reached from the process application's "Connected
|
|
38
|
+
// clusters" card (or its /connected-clusters deep link). Assigning a cluster
|
|
39
|
+
// to a stage here is what makes it deployable: DeployModal renders
|
|
40
|
+
// ProcessAppClusterSelection for a process application, and that filters the
|
|
41
|
+
// cluster list through getProcessAppClustersByStage -- so a process
|
|
42
|
+
// application with no stage assigned offers no cluster at all.
|
|
43
|
+
defineStagesDialog;
|
|
44
|
+
developmentClusterSelect;
|
|
45
|
+
defineStagesSaveButton;
|
|
46
|
+
constructor(page) {
|
|
47
|
+
this.page = page;
|
|
48
|
+
// .first() defensively only: hub renders exactly one of these today, in the
|
|
49
|
+
// workspace-root empty state. A future second placement should not turn
|
|
50
|
+
// every PA-creating test into a strict-mode violation.
|
|
51
|
+
this.createProcessApplicationButton = page
|
|
52
|
+
.getByRole('button', { name: 'Create process application' })
|
|
53
|
+
.first();
|
|
54
|
+
this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
|
|
55
|
+
this.creationDialog = page.getByRole('dialog', {
|
|
56
|
+
name: 'Create a process application',
|
|
57
|
+
});
|
|
58
|
+
// Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
|
|
59
|
+
// getByLabel('Name', {exact: true}) for the properties panel, which would
|
|
60
|
+
// resolve to two elements while the creation modal is open.
|
|
61
|
+
this.nameInput = this.creationDialog.getByLabel('Name', { exact: true });
|
|
62
|
+
this.submitButton = this.creationDialog.getByRole('button', {
|
|
63
|
+
name: 'Create',
|
|
64
|
+
exact: true,
|
|
65
|
+
});
|
|
66
|
+
this.cancelButton = this.creationDialog.getByRole('button', {
|
|
67
|
+
name: 'Cancel',
|
|
68
|
+
exact: true,
|
|
69
|
+
});
|
|
70
|
+
this.breadcrumb = page.locator('[data-test="breadcrumb-process-application-menu"]');
|
|
71
|
+
this.folderExplorer = page.getByRole('region', { name: 'Folder explorer' });
|
|
72
|
+
// The process application page renders more than one diagram-dropdown
|
|
73
|
+
// (hub's own unit tests reach for it with getAllByTestId), so the dropdown
|
|
74
|
+
// has to be resolved inside the folder explorer rather than page-wide.
|
|
75
|
+
this.createNewDropdown = this.folderExplorer
|
|
76
|
+
.locator('[data-test="diagram-dropdown"]')
|
|
77
|
+
.first();
|
|
78
|
+
this.inlineNameInput = page.locator('[data-test="editable-input"]');
|
|
79
|
+
this.defineStagesDialog = page
|
|
80
|
+
.getByRole('dialog')
|
|
81
|
+
.filter({ hasText: 'Define stages' });
|
|
82
|
+
// Carbon Dropdown / design-system Select both keep this id.
|
|
83
|
+
this.developmentClusterSelect = this.defineStagesDialog.locator('#dropdown-development-cluster');
|
|
84
|
+
this.defineStagesSaveButton = this.defineStagesDialog.getByRole('button', {
|
|
85
|
+
name: 'Save',
|
|
86
|
+
exact: true,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
// Either the folder explorer or the breadcrumb proves we are on the process
|
|
90
|
+
// application page; .first() because both are present once it has rendered.
|
|
91
|
+
pageReady() {
|
|
92
|
+
return this.folderExplorer.or(this.breadcrumb).first();
|
|
93
|
+
}
|
|
94
|
+
async waitForProcessApplication(timeout = 90000) {
|
|
95
|
+
await (0, test_1.expect)(this.pageReady()).toBeVisible({ timeout });
|
|
96
|
+
}
|
|
97
|
+
async clickCreateProcessApplicationButton() {
|
|
98
|
+
await (0, test_1.expect)(this.createProcessApplicationButton).toBeVisible({
|
|
99
|
+
timeout: 60000,
|
|
100
|
+
});
|
|
101
|
+
await this.createProcessApplicationButton.click({ timeout: 60000 });
|
|
102
|
+
}
|
|
103
|
+
async clickCreateProcessApplicationOption() {
|
|
104
|
+
await (0, test_1.expect)(this.createProcessApplicationOption).toBeVisible({
|
|
105
|
+
timeout: 60000,
|
|
106
|
+
});
|
|
107
|
+
await this.createProcessApplicationOption.click({ timeout: 60000 });
|
|
108
|
+
}
|
|
109
|
+
// Hub creates a process application through a modal today. It used an inline
|
|
110
|
+
// editable title before, and #27593 is still moving this surface, so the
|
|
111
|
+
// inline path is kept as a fallback — this is the one method that has to
|
|
112
|
+
// change if naming moves back inline.
|
|
113
|
+
async nameNewProcessApplication(name) {
|
|
114
|
+
const isModal = await this.creationDialog
|
|
115
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
116
|
+
.then(() => true)
|
|
117
|
+
.catch(() => false);
|
|
118
|
+
if (isModal) {
|
|
119
|
+
await this.nameInput.click({ timeout: 30000 });
|
|
120
|
+
await this.nameInput.fill(name);
|
|
121
|
+
await (0, test_1.expect)(this.nameInput).toHaveValue(name, { timeout: 10000 });
|
|
122
|
+
await (0, submitModalWithRetry_1.submitModalWithRetry)(this.submitButton, this.creationDialog);
|
|
123
|
+
await this.waitForProcessApplication();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await (0, test_1.expect)(this.inlineNameInput).toBeVisible({ timeout: 30000 });
|
|
127
|
+
await this.inlineNameInput.click({ timeout: 60000 });
|
|
128
|
+
await this.inlineNameInput.fill(name);
|
|
129
|
+
await this.inlineNameInput.press('Enter');
|
|
130
|
+
await this.waitForProcessApplication();
|
|
131
|
+
}
|
|
132
|
+
// Assigns a cluster to this process application's development stage.
|
|
133
|
+
//
|
|
134
|
+
// Deploying from inside a process application goes through
|
|
135
|
+
// ProcessAppClusterSelection, which only lists clusters the application has
|
|
136
|
+
// assigned to a stage, and does not auto-select anything when that list is
|
|
137
|
+
// empty. A freshly created process application has no stages, so the deploy
|
|
138
|
+
// dialog shows "Clusters not configured for stages" and offers nothing to
|
|
139
|
+
// pick -- which is why the old per-deploy cluster toggle was nowhere to be
|
|
140
|
+
// found. Assign the stage once, at creation, and the deploy dialog then
|
|
141
|
+
// auto-selects it.
|
|
142
|
+
//
|
|
143
|
+
// clusterName is matched as a prefix because the dropdown renders each
|
|
144
|
+
// cluster as "<name> <generation>". Without it, the first real cluster is
|
|
145
|
+
// taken -- callers that care assert the name in the deploy dialog anyway.
|
|
146
|
+
async defineDevelopmentStage(clusterName) {
|
|
147
|
+
await (0, test_1.expect)(this.defineStagesDialog).toBeVisible({ timeout: 60000 });
|
|
148
|
+
await this.developmentClusterSelect.click({ timeout: 30000 });
|
|
149
|
+
// Escape before interpolating: a cluster name can carry regex
|
|
150
|
+
// metacharacters -- "Test Cluster (v2)" is the common case -- which would
|
|
151
|
+
// otherwise mis-match or throw. Same treatment as
|
|
152
|
+
// utils/modelerCluster.selectModelerCluster.
|
|
153
|
+
const escapedClusterName = clusterName?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
154
|
+
const option = clusterName
|
|
155
|
+
? this.defineStagesDialog
|
|
156
|
+
.getByRole('option', { name: new RegExp(`^${escapedClusterName}`) })
|
|
157
|
+
.first()
|
|
158
|
+
: this.defineStagesDialog
|
|
159
|
+
.getByRole('option')
|
|
160
|
+
.filter({ hasNotText: 'Select a development cluster' })
|
|
161
|
+
.first();
|
|
162
|
+
await option.click({ timeout: 30000 });
|
|
163
|
+
await this.defineStagesSaveButton.click({ timeout: 30000 });
|
|
164
|
+
await (0, test_1.expect)(this.defineStagesDialog).toBeHidden({ timeout: 60000 });
|
|
165
|
+
}
|
|
166
|
+
rememberResourceContainer(url) {
|
|
167
|
+
resourceContainers.set(this.page, { url });
|
|
168
|
+
}
|
|
169
|
+
// Drops the recorded development-stage binding so the next ensureStageCluster
|
|
170
|
+
// call binds again. Called when the deploy dialog turns out not to offer the
|
|
171
|
+
// cluster we believe we bound: the dialog offers exactly what the
|
|
172
|
+
// application's stages point at, so if the cluster is missing the binding
|
|
173
|
+
// did not take, and reopening the dialog can never change that. Without
|
|
174
|
+
// this, a caller's retries all re-check a state nothing is changing.
|
|
175
|
+
forgetStageCluster() {
|
|
176
|
+
const container = resourceContainers.get(this.page);
|
|
177
|
+
if (container) {
|
|
178
|
+
container.developmentCluster = undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// A process application deploys to the clusters bound to its stages, and
|
|
182
|
+
// nothing else: inside a container DeployModal renders
|
|
183
|
+
// ProcessAppClusterSelection, which lists only those and auto-selects the
|
|
184
|
+
// first. Which cluster a test wants is known only once a deploy flow names
|
|
185
|
+
// it, so the binding happens here, on demand -- leave the diagram, bind the
|
|
186
|
+
// cluster to the development stage, come back. Repeat calls for the same
|
|
187
|
+
// cluster cost nothing, and a test deploying to a second cluster rebinds.
|
|
188
|
+
// Returns whether it navigated: the caller may have been on a view that a
|
|
189
|
+
// reload does not restore -- Play is a tab click, not a URL -- and has to
|
|
190
|
+
// re-enter it.
|
|
191
|
+
async ensureStageCluster(clusterName) {
|
|
192
|
+
const container = resourceContainers.get(this.page);
|
|
193
|
+
if (!container || container.developmentCluster === clusterName) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
const returnTo = this.page.url();
|
|
197
|
+
const paUrl = container.url.replace(/[?#].*$/, '').replace(/\/$/, '');
|
|
198
|
+
// Retried as a whole, re-entering the page on every attempt. This binding
|
|
199
|
+
// is a one-shot precondition for every deploy the test goes on to make,
|
|
200
|
+
// and unlike the deploy itself it sits outside runProcessInstance's retry
|
|
201
|
+
// loop -- so a single transient miss (the session bounced to /login, the
|
|
202
|
+
// cluster list not yet arrived when the dropdown opened) failed the test
|
|
203
|
+
// outright instead of being absorbed the way every other modal in this
|
|
204
|
+
// suite is. Re-navigating is the recovery; waiting longer on a page that
|
|
205
|
+
// has already gone wrong is not, which is why the inner timeouts stay
|
|
206
|
+
// short. A genuinely unbindable cluster still fails, with the same error.
|
|
207
|
+
await (0, test_1.expect)(async () => {
|
|
208
|
+
await this.page.goto(`${paUrl}/connected-clusters`, {
|
|
209
|
+
waitUntil: 'domcontentloaded',
|
|
210
|
+
});
|
|
211
|
+
await this.openStageDefinition();
|
|
212
|
+
await this.defineDevelopmentStage(clusterName);
|
|
213
|
+
}).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
|
|
214
|
+
container.developmentCluster = clusterName;
|
|
215
|
+
await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
// The /connected-clusters deep link opens the Define stages dialog by itself
|
|
219
|
+
// while the application has no stage bound. Once one is bound -- a test that
|
|
220
|
+
// deploys to a second cluster -- it has to be reopened from the page, so try
|
|
221
|
+
// the affordances hub offers before failing with exactly what was looked for.
|
|
222
|
+
// Worth the noise: an unbound cluster otherwise surfaces as a deploy dialog
|
|
223
|
+
// that silently offers nothing, which says nothing about its own cause.
|
|
224
|
+
async openStageDefinition() {
|
|
225
|
+
const opened = await this.defineStagesDialog
|
|
226
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
227
|
+
.then(() => true)
|
|
228
|
+
.catch(() => false);
|
|
229
|
+
if (opened) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Page-scoped .first(): hub renders one such control on this page, and a
|
|
233
|
+
// second placement must not turn every rebinding test into a strict-mode
|
|
234
|
+
// violation.
|
|
235
|
+
const reopen = this.page
|
|
236
|
+
.getByRole('button', { name: /define stages|edit stages|configure/i })
|
|
237
|
+
.first();
|
|
238
|
+
const canReopen = await reopen
|
|
239
|
+
.waitFor({ state: 'visible', timeout: 10000 })
|
|
240
|
+
.then(() => true)
|
|
241
|
+
.catch(() => false);
|
|
242
|
+
if (!canReopen) {
|
|
243
|
+
throw new Error('Could not open the "Define stages" dialog on /connected-clusters: it ' +
|
|
244
|
+
'did not open by itself and no "Define stages" / "Edit stages" / ' +
|
|
245
|
+
'"Configure" control was found.');
|
|
246
|
+
}
|
|
247
|
+
await reopen.click({ timeout: 30000 });
|
|
248
|
+
}
|
|
249
|
+
async openByName(name) {
|
|
250
|
+
const processApplication = this.page
|
|
251
|
+
.getByRole('row')
|
|
252
|
+
.filter({ hasText: name })
|
|
253
|
+
.getByTitle(name)
|
|
254
|
+
.first();
|
|
255
|
+
await (0, test_1.expect)(processApplication).toBeVisible({ timeout: 90000 });
|
|
256
|
+
await processApplication.click({ timeout: 60000 });
|
|
257
|
+
await this.waitForProcessApplication();
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
exports.ProcessApplicationPage = ProcessApplicationPage;
|
|
@@ -465,6 +465,14 @@ async function modelAndRunConnectorsTimerEventDiagram(modelerCreatePage, modeler
|
|
|
465
465
|
timeout: 60000,
|
|
466
466
|
});
|
|
467
467
|
await connectorMarketplacePage.downloadConnectorToProject();
|
|
468
|
+
// Both lookups are container-relative: clickProjectBreadcrumb() returns to
|
|
469
|
+
// this test's process application, which is where the uploaded diagram
|
|
470
|
+
// lives. The downloaded element template is the open question -- hub
|
|
471
|
+
// publishes element templates to the workspace, but the marketplace download
|
|
472
|
+
// is initiated from inside the container, so it should land there. If a
|
|
473
|
+
// trace shows the template at workspace root instead, this call becomes
|
|
474
|
+
// clickWorkspaceBreadcrumb() followed by returnToResourceContainer() before
|
|
475
|
+
// the diagram is reopened.
|
|
468
476
|
await modelerHomePage.clickProjectBreadcrumb();
|
|
469
477
|
await modelerHomePage.clickProcessDiagram('REST Outbound Connector');
|
|
470
478
|
await connectorTemplatePage.publishConnector();
|
|
@@ -2,11 +2,15 @@ import { Page, Locator } from '@playwright/test';
|
|
|
2
2
|
declare class ModelerHomePage {
|
|
3
3
|
private page;
|
|
4
4
|
private defaultFolderName;
|
|
5
|
+
private processApplicationPage;
|
|
6
|
+
private processApplicationUrl?;
|
|
7
|
+
private processApplicationName?;
|
|
5
8
|
readonly modelerPageBanner: Locator;
|
|
6
9
|
readonly createNewProjectButton: Locator;
|
|
7
10
|
readonly projectNameInput: Locator;
|
|
8
11
|
readonly chooseBpmnTemplateButton: Locator;
|
|
9
12
|
readonly diagramTypeDropdown: Locator;
|
|
13
|
+
readonly createIdpApplicationButton: Locator;
|
|
10
14
|
readonly bpmnTemplateOption: Locator;
|
|
11
15
|
readonly formTemplateOption: Locator;
|
|
12
16
|
readonly projectBreadcrumb: Locator;
|
|
@@ -34,6 +38,9 @@ declare class ModelerHomePage {
|
|
|
34
38
|
clickHomeBreadcrumb(): Promise<void>;
|
|
35
39
|
clickChooseBpmnTemplateButton(): Promise<void>;
|
|
36
40
|
clickDiagramTypeDropdown(): Promise<void>;
|
|
41
|
+
private enterResourceContainer;
|
|
42
|
+
private clickCreateProcessApplication;
|
|
43
|
+
private becameVisible;
|
|
37
44
|
clickBpmnTemplateOption(): Promise<void>;
|
|
38
45
|
clickFormOption(): Promise<void>;
|
|
39
46
|
enterFormName(name: string): Promise<void>;
|
|
@@ -4,14 +4,26 @@ exports.ModelerHomePage = void 0;
|
|
|
4
4
|
const test_1 = require("@playwright/test");
|
|
5
5
|
const sleep_1 = require("../../utils/sleep");
|
|
6
6
|
const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
|
|
7
|
+
const randomName_1 = require("../../utils/randomName");
|
|
8
|
+
const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
|
|
7
9
|
class ModelerHomePage {
|
|
8
10
|
page;
|
|
9
11
|
defaultFolderName = 'Cross Component Test Project';
|
|
12
|
+
// Only used on hub's own image, where content can live solely inside a
|
|
13
|
+
// process application (camunda/camunda-hub#27592). The released 8.9 image
|
|
14
|
+
// still creates resources at the project root and never touches this. The
|
|
15
|
+
// container is created lazily, the first time a resource is created, and
|
|
16
|
+
// remembered for the lifetime of this page object (one instance per test,
|
|
17
|
+
// see fixtures/SM-8.9.ts).
|
|
18
|
+
processApplicationPage;
|
|
19
|
+
processApplicationUrl;
|
|
20
|
+
processApplicationName;
|
|
10
21
|
modelerPageBanner;
|
|
11
22
|
createNewProjectButton;
|
|
12
23
|
projectNameInput;
|
|
13
24
|
chooseBpmnTemplateButton;
|
|
14
25
|
diagramTypeDropdown;
|
|
26
|
+
createIdpApplicationButton;
|
|
15
27
|
bpmnTemplateOption;
|
|
16
28
|
formTemplateOption;
|
|
17
29
|
projectBreadcrumb;
|
|
@@ -31,6 +43,7 @@ class ModelerHomePage {
|
|
|
31
43
|
modalCloseButton;
|
|
32
44
|
constructor(page) {
|
|
33
45
|
this.page = page;
|
|
46
|
+
this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
|
|
34
47
|
this.modelerPageBanner = page
|
|
35
48
|
.locator('a')
|
|
36
49
|
.filter({ hasText: 'Camunda logoModeler' });
|
|
@@ -46,6 +59,14 @@ class ModelerHomePage {
|
|
|
46
59
|
name: 'Choose BPMN template',
|
|
47
60
|
});
|
|
48
61
|
this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
|
|
62
|
+
// Positive marker of the process-application-only layout (hub's own image,
|
|
63
|
+
// camunda/camunda-hub#27592): that empty project root surfaces IDP-app
|
|
64
|
+
// creation as a top-level button. The released 8.9 root has no such button
|
|
65
|
+
// -- its IDP entry is a menu item inside the "Create new" dropdown -- so
|
|
66
|
+
// this reliably distinguishes the two layouts.
|
|
67
|
+
this.createIdpApplicationButton = page.getByRole('button', {
|
|
68
|
+
name: 'Create IDP application',
|
|
69
|
+
});
|
|
49
70
|
this.bpmnTemplateOption = page
|
|
50
71
|
.locator('[data-test="create-bpmn-diagram"]')
|
|
51
72
|
.getByText('BPMN Diagram');
|
|
@@ -212,7 +233,35 @@ class ModelerHomePage {
|
|
|
212
233
|
// -- the reload-and-wait retry never actually ran. Bound the recovery wait
|
|
213
234
|
// and widen the budget so all three attempts fit.
|
|
214
235
|
await this.waitForModelerReady();
|
|
215
|
-
|
|
236
|
+
// Two Web Modeler layouts run this suite. The released 8.9 image (the SM
|
|
237
|
+
// nightly) keeps the create-resource dropdown at the project root. Hub's
|
|
238
|
+
// own image (this pipeline) only lets content live inside a process
|
|
239
|
+
// application since camunda/camunda-hub#27592, so the root offers no
|
|
240
|
+
// dropdown and instead surfaces IDP-application creation as a top-level
|
|
241
|
+
// button.
|
|
242
|
+
//
|
|
243
|
+
// Route on a positive marker of EACH layout, never on the mere absence of
|
|
244
|
+
// the dropdown: wait for the root dropdown (old layout) OR the IDP button
|
|
245
|
+
// (new layout). If the released image's root dropdown were itself broken --
|
|
246
|
+
// the regression the old path exists to catch -- neither marker would
|
|
247
|
+
// render and this assertion throws, rather than silently taking the
|
|
248
|
+
// process-application path (whose "Create process application" button exists
|
|
249
|
+
// on both images).
|
|
250
|
+
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.diagramTypeDropdown.first().or(this.createIdpApplicationButton), {
|
|
251
|
+
visibilityTimeout: 15000,
|
|
252
|
+
totalTimeout: 180000,
|
|
253
|
+
postAction: async () => {
|
|
254
|
+
await this.page.reload();
|
|
255
|
+
await this.waitForModelerReady(45000);
|
|
256
|
+
},
|
|
257
|
+
});
|
|
258
|
+
if (await this.diagramTypeDropdown.first().isVisible()) {
|
|
259
|
+
await this.diagramTypeDropdown.first().click();
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
await this.enterResourceContainer();
|
|
263
|
+
const dropdown = this.processApplicationPage.createNewDropdown;
|
|
264
|
+
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, dropdown, {
|
|
216
265
|
visibilityTimeout: 15000,
|
|
217
266
|
totalTimeout: 180000,
|
|
218
267
|
postAction: async () => {
|
|
@@ -220,7 +269,53 @@ class ModelerHomePage {
|
|
|
220
269
|
await this.waitForModelerReady(45000);
|
|
221
270
|
},
|
|
222
271
|
});
|
|
223
|
-
await
|
|
272
|
+
await dropdown.click();
|
|
273
|
+
}
|
|
274
|
+
// Creates, once, the process application that holds this test's resources and
|
|
275
|
+
// leaves the browser inside it. Only reached on hub's process-application-only
|
|
276
|
+
// layout. Idempotent: re-entered by remembered URL if already created.
|
|
277
|
+
async enterResourceContainer() {
|
|
278
|
+
if (this.processApplicationUrl) {
|
|
279
|
+
const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
|
|
280
|
+
if (!stillInside) {
|
|
281
|
+
await this.page.goto(this.processApplicationUrl, {
|
|
282
|
+
waitUntil: 'domcontentloaded',
|
|
283
|
+
});
|
|
284
|
+
await this.processApplicationPage.waitForProcessApplication();
|
|
285
|
+
await this.clickMessageBanner();
|
|
286
|
+
}
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
this.processApplicationName = await (0, randomName_1.randomNameAgregator)('Cross Component Process Application');
|
|
290
|
+
await this.clickCreateProcessApplication();
|
|
291
|
+
await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
|
|
292
|
+
this.processApplicationUrl = this.page.url();
|
|
293
|
+
}
|
|
294
|
+
// Which control creates a process application depends on the hub version under
|
|
295
|
+
// test. Since camunda/camunda-hub#25831 it is a plain primary button on the
|
|
296
|
+
// workspace root, and the "Create new" menu explicitly hides the item because
|
|
297
|
+
// a process application is no longer just another resource. Before that change
|
|
298
|
+
// the menu item was the only way in. Race both entry points so neither version
|
|
299
|
+
// pays a full timeout, then use whichever one rendered.
|
|
300
|
+
async clickCreateProcessApplication() {
|
|
301
|
+
const button = this.processApplicationPage.createProcessApplicationButton;
|
|
302
|
+
const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
|
|
303
|
+
if (!rootLoaded) {
|
|
304
|
+
await this.page.reload();
|
|
305
|
+
await this.waitForModelerReady(45000);
|
|
306
|
+
}
|
|
307
|
+
if (await this.becameVisible(button, 5000)) {
|
|
308
|
+
await this.processApplicationPage.clickCreateProcessApplicationButton();
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
await this.diagramTypeDropdown.click({ timeout: 60000 });
|
|
312
|
+
await this.processApplicationPage.clickCreateProcessApplicationOption();
|
|
313
|
+
}
|
|
314
|
+
async becameVisible(locator, timeout) {
|
|
315
|
+
return locator
|
|
316
|
+
.waitFor({ state: 'visible', timeout })
|
|
317
|
+
.then(() => true)
|
|
318
|
+
.catch(() => false);
|
|
224
319
|
}
|
|
225
320
|
async clickBpmnTemplateOption() {
|
|
226
321
|
await (0, test_1.expect)(this.bpmnTemplateOption).toBeVisible({ timeout: 60000 });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Locator, Page } from '@playwright/test';
|
|
2
|
+
declare class ProcessApplicationPage {
|
|
3
|
+
private page;
|
|
4
|
+
readonly createProcessApplicationButton: Locator;
|
|
5
|
+
readonly createProcessApplicationOption: Locator;
|
|
6
|
+
readonly creationDialog: Locator;
|
|
7
|
+
readonly nameInput: Locator;
|
|
8
|
+
readonly submitButton: Locator;
|
|
9
|
+
readonly cancelButton: Locator;
|
|
10
|
+
readonly breadcrumb: Locator;
|
|
11
|
+
readonly folderExplorer: Locator;
|
|
12
|
+
readonly titleBar: Locator;
|
|
13
|
+
readonly createNewDropdown: Locator;
|
|
14
|
+
readonly inlineNameInput: Locator;
|
|
15
|
+
constructor(page: Page);
|
|
16
|
+
private pageReady;
|
|
17
|
+
waitForProcessApplication(timeout?: number): Promise<void>;
|
|
18
|
+
clickCreateProcessApplicationButton(): Promise<void>;
|
|
19
|
+
clickCreateProcessApplicationOption(): Promise<void>;
|
|
20
|
+
nameNewProcessApplication(name: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export { ProcessApplicationPage };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProcessApplicationPage = void 0;
|
|
4
|
+
const test_1 = require("@playwright/test");
|
|
5
|
+
const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
|
|
6
|
+
// Since camunda/camunda-hub#27592 a process application is the only container
|
|
7
|
+
// that can hold files and folders; a workspace root ("project" in today's UI)
|
|
8
|
+
// holds nothing but process applications and IDP applications. The released 8.9
|
|
9
|
+
// image still lets resources live at the project root, so this page object is
|
|
10
|
+
// only reached when the newer, process-application-only layout is detected --
|
|
11
|
+
// entering and leaving it is driven from ModelerHomePage so the existing spec
|
|
12
|
+
// call sites keep working unchanged.
|
|
13
|
+
class ProcessApplicationPage {
|
|
14
|
+
page;
|
|
15
|
+
// Empty workspace root: a plain primary button rendered next to the "Create
|
|
16
|
+
// new" menu (hub's EmptyStateActionButtons).
|
|
17
|
+
createProcessApplicationButton;
|
|
18
|
+
// Non-empty root: the item inside the "Create new" menu. Hub passes
|
|
19
|
+
// hideProcessApplication at workspace level, so this is the fallback rather
|
|
20
|
+
// than the primary path. Kept because the Hub PR is still moving this surface.
|
|
21
|
+
createProcessApplicationOption;
|
|
22
|
+
creationDialog;
|
|
23
|
+
nameInput;
|
|
24
|
+
submitButton;
|
|
25
|
+
cancelButton;
|
|
26
|
+
breadcrumb;
|
|
27
|
+
folderExplorer;
|
|
28
|
+
titleBar;
|
|
29
|
+
createNewDropdown;
|
|
30
|
+
inlineNameInput;
|
|
31
|
+
constructor(page) {
|
|
32
|
+
this.page = page;
|
|
33
|
+
// .first() defensively only: hub renders exactly one of these today, in the
|
|
34
|
+
// workspace-root empty state. A future second placement should not turn
|
|
35
|
+
// every PA-creating test into a strict-mode violation.
|
|
36
|
+
this.createProcessApplicationButton = page
|
|
37
|
+
.getByRole('button', { name: 'Create process application' })
|
|
38
|
+
.first();
|
|
39
|
+
this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
|
|
40
|
+
this.creationDialog = page.getByRole('dialog', {
|
|
41
|
+
name: 'Create a process application',
|
|
42
|
+
});
|
|
43
|
+
// Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
|
|
44
|
+
// getByLabel('Name', {exact: true}) for the properties panel, which would
|
|
45
|
+
// resolve to two elements while the creation modal is open.
|
|
46
|
+
this.nameInput = this.creationDialog.getByLabel('Name', { exact: true });
|
|
47
|
+
this.submitButton = this.creationDialog.getByRole('button', {
|
|
48
|
+
name: 'Create',
|
|
49
|
+
exact: true,
|
|
50
|
+
});
|
|
51
|
+
this.cancelButton = this.creationDialog.getByRole('button', {
|
|
52
|
+
name: 'Cancel',
|
|
53
|
+
exact: true,
|
|
54
|
+
});
|
|
55
|
+
this.breadcrumb = page.locator('[data-test="breadcrumb-process-application-menu"]');
|
|
56
|
+
// The Design System shell renames this region "Resources"; the Carbon shell
|
|
57
|
+
// calls it "Folder explorer". Both hold the resource table and its toolbar,
|
|
58
|
+
// so matching either keeps createNewDropdown below correctly scoped.
|
|
59
|
+
this.folderExplorer = page
|
|
60
|
+
.getByRole('region', { name: 'Folder explorer' })
|
|
61
|
+
.or(page.getByRole('region', { name: 'Resources' }))
|
|
62
|
+
.first();
|
|
63
|
+
// Design System proof that we are on a process application page: the
|
|
64
|
+
// Carbon-era breadcrumb hook is gone under that shell, the same way
|
|
65
|
+
// breadcrumb-project is.
|
|
66
|
+
this.titleBar = page.locator('[data-test="process-application-title-bar"]');
|
|
67
|
+
// The process application page renders more than one diagram-dropdown
|
|
68
|
+
// (hub's own unit tests reach for it with getAllByTestId), so the dropdown
|
|
69
|
+
// has to be resolved inside the folder explorer rather than page-wide.
|
|
70
|
+
this.createNewDropdown = this.folderExplorer
|
|
71
|
+
.locator('[data-test="diagram-dropdown"]')
|
|
72
|
+
.first();
|
|
73
|
+
this.inlineNameInput = page.locator('[data-test="editable-input"]');
|
|
74
|
+
}
|
|
75
|
+
// Any one of the title bar, the resource region or the breadcrumb proves we
|
|
76
|
+
// are on the process application page. Three branches because the Design
|
|
77
|
+
// System and Carbon shells expose different subsets: only the title bar is
|
|
78
|
+
// present under both. .first() because several match once it has rendered.
|
|
79
|
+
pageReady() {
|
|
80
|
+
return this.titleBar.or(this.folderExplorer).or(this.breadcrumb).first();
|
|
81
|
+
}
|
|
82
|
+
async waitForProcessApplication(timeout = 90000) {
|
|
83
|
+
await (0, test_1.expect)(this.pageReady()).toBeVisible({ timeout });
|
|
84
|
+
}
|
|
85
|
+
async clickCreateProcessApplicationButton() {
|
|
86
|
+
await (0, test_1.expect)(this.createProcessApplicationButton).toBeVisible({
|
|
87
|
+
timeout: 60000,
|
|
88
|
+
});
|
|
89
|
+
await this.createProcessApplicationButton.click({ timeout: 60000 });
|
|
90
|
+
}
|
|
91
|
+
async clickCreateProcessApplicationOption() {
|
|
92
|
+
await (0, test_1.expect)(this.createProcessApplicationOption).toBeVisible({
|
|
93
|
+
timeout: 60000,
|
|
94
|
+
});
|
|
95
|
+
await this.createProcessApplicationOption.click({ timeout: 60000 });
|
|
96
|
+
}
|
|
97
|
+
// Hub creates a process application through a modal today. It used an inline
|
|
98
|
+
// editable title before, and #27593 is still moving this surface, so the
|
|
99
|
+
// inline path is kept as a fallback -- this is the one method that has to
|
|
100
|
+
// change if naming moves back inline.
|
|
101
|
+
async nameNewProcessApplication(name) {
|
|
102
|
+
const isModal = await this.creationDialog
|
|
103
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
104
|
+
.then(() => true)
|
|
105
|
+
.catch(() => false);
|
|
106
|
+
if (isModal) {
|
|
107
|
+
await this.nameInput.click({ timeout: 30000 });
|
|
108
|
+
await this.nameInput.fill(name);
|
|
109
|
+
await (0, test_1.expect)(this.nameInput).toHaveValue(name, { timeout: 10000 });
|
|
110
|
+
await (0, submitModalWithRetry_1.submitModalWithRetry)(this.submitButton, this.creationDialog);
|
|
111
|
+
await this.waitForProcessApplication();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
await (0, test_1.expect)(this.inlineNameInput).toBeVisible({ timeout: 30000 });
|
|
115
|
+
await this.inlineNameInput.click({ timeout: 60000 });
|
|
116
|
+
await this.inlineNameInput.fill(name);
|
|
117
|
+
await this.inlineNameInput.press('Enter');
|
|
118
|
+
await this.waitForProcessApplication();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
exports.ProcessApplicationPage = ProcessApplicationPage;
|
|
@@ -125,7 +125,7 @@ _8_9_1.test.describe('User Roles User Flow', () => {
|
|
|
125
125
|
await modelerCreatePage.clickAppendEndEventButton();
|
|
126
126
|
await (0, sleep_1.sleep)(20000);
|
|
127
127
|
await modelerCreatePage.runProcessInstance(clusterName);
|
|
128
|
-
await (0, test_1.expect)(page.getByText(
|
|
128
|
+
await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).toBeVisible({ timeout: 60000 });
|
|
129
129
|
await modelerCreatePage.clickCloseButton();
|
|
130
130
|
});
|
|
131
131
|
});
|
|
@@ -216,7 +216,7 @@ _8_9_1.test.describe('User Roles User Flow', () => {
|
|
|
216
216
|
await modelerCreatePage.clickAppendEndEventButton();
|
|
217
217
|
await (0, sleep_1.sleep)(20000);
|
|
218
218
|
await modelerCreatePage.runProcessInstance(clusterName, '', {}, '', false);
|
|
219
|
-
await (0, test_1.expect)(page.getByText(
|
|
219
|
+
await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).toBeVisible({ timeout: 60000 });
|
|
220
220
|
await modelerCreatePage.clickCloseButton();
|
|
221
221
|
});
|
|
222
222
|
});
|
|
@@ -312,7 +312,7 @@ _8_9_1.test.describe('User Roles User Flow', () => {
|
|
|
312
312
|
await modelerCreatePage.clickAppendEndEventButton();
|
|
313
313
|
await (0, sleep_1.sleep)(20000);
|
|
314
314
|
await modelerCreatePage.runProcessInstance(clusterName);
|
|
315
|
-
await (0, test_1.expect)(page.getByText(
|
|
315
|
+
await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).not.toBeVisible({ timeout: 60000 });
|
|
316
316
|
await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeVisible({
|
|
317
317
|
timeout: 120000,
|
|
318
318
|
});
|