@camunda/e2e-test-suite 0.0.1126 → 0.0.1127
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.
|
@@ -187,7 +187,12 @@ class ModelerCreatePage {
|
|
|
187
187
|
this.variableInput = page.locator('[id="variables-json"]');
|
|
188
188
|
this.businessIdInput = page.locator('[id^="business-id-input"]');
|
|
189
189
|
this.embedFormButton = page.getByRole('button', { name: 'Link form' });
|
|
190
|
-
|
|
190
|
+
// Workspaces-enabled Web Modeler renders the "Link a form" picker via
|
|
191
|
+
// LinkTargetModal, whose primary action carries data-test="link-target-confirm"
|
|
192
|
+
// instead of the legacy move dialog's "confirm-move".
|
|
193
|
+
this.embedButton = page
|
|
194
|
+
.locator('[data-test="confirm-move"]')
|
|
195
|
+
.or(page.locator('[data-test="link-target-confirm"]'));
|
|
191
196
|
this.newForm = page.locator('[data-test="item-New form"]');
|
|
192
197
|
this.continueToPlayButton = page.getByRole('button', { name: 'Continue' });
|
|
193
198
|
this.deploySubButton = page
|
|
@@ -414,11 +419,28 @@ class ModelerCreatePage {
|
|
|
414
419
|
await (0, test_1.expect)(this.secondElement).toBeVisible({ timeout: 30000 });
|
|
415
420
|
await (0, test_1.expect)(this.thirdElement).toBeVisible({ timeout: 30000 });
|
|
416
421
|
}
|
|
422
|
+
// The Carbon shell links a form via a plain list item click. Workspaces-enabled
|
|
423
|
+
// Web Modeler instead browses LinkTargetModal, whose rows are files keyed by id
|
|
424
|
+
// (not name) and merely *select* the row -- linking requires a separate confirm
|
|
425
|
+
// click (LinkTargetLevel/SelectableRow: `aria-label="Select ${name}"`, and the
|
|
426
|
+
// modal's own primary button, clicked by the caller via clickEmbedButton()).
|
|
417
427
|
async clickForm(name) {
|
|
418
|
-
|
|
428
|
+
const legacyItem = this.page
|
|
419
429
|
.locator(`[data-test="item-${name}"]`)
|
|
420
|
-
.getByText(name)
|
|
421
|
-
|
|
430
|
+
.getByText(name);
|
|
431
|
+
const selectableRow = this.page.getByRole('button', {
|
|
432
|
+
name: `Select ${name}`,
|
|
433
|
+
exact: true,
|
|
434
|
+
});
|
|
435
|
+
await legacyItem
|
|
436
|
+
.or(selectableRow)
|
|
437
|
+
.first()
|
|
438
|
+
.waitFor({ state: 'visible', timeout: 90000 });
|
|
439
|
+
if (await selectableRow.isVisible().catch(() => false)) {
|
|
440
|
+
await selectableRow.click({ timeout: 90000 });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
await legacyItem.click({ timeout: 90000 });
|
|
422
444
|
}
|
|
423
445
|
async runProcessInstance(variables = '', tenant = '', businessId = '') {
|
|
424
446
|
await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
|
|
@@ -5,8 +5,18 @@ declare class ModelerHomePage {
|
|
|
5
5
|
private processApplicationPage;
|
|
6
6
|
private processApplicationUrl?;
|
|
7
7
|
private processApplicationName?;
|
|
8
|
+
private workspaceProjectsUrl?;
|
|
9
|
+
private workspacesOverviewUrl?;
|
|
8
10
|
readonly modelerPageBanner: Locator;
|
|
9
11
|
readonly createNewProjectButton: Locator;
|
|
12
|
+
readonly createWorkspaceButton: Locator;
|
|
13
|
+
readonly workspaceOpenButton: Locator;
|
|
14
|
+
readonly noWorkspacesHeading: Locator;
|
|
15
|
+
readonly workspaceWizardHeading: Locator;
|
|
16
|
+
readonly workspaceNameInput: Locator;
|
|
17
|
+
readonly workspaceDescriptionInput: Locator;
|
|
18
|
+
readonly workspaceWizardSubmit: Locator;
|
|
19
|
+
readonly workspaceMembersHeading: Locator;
|
|
10
20
|
readonly projectNameInput: Locator;
|
|
11
21
|
readonly moreActionsButton: Locator;
|
|
12
22
|
readonly renameMenuItem: Locator;
|
|
@@ -39,6 +49,12 @@ declare class ModelerHomePage {
|
|
|
39
49
|
waitForModelerReady(timeoutMs?: number): Promise<void>;
|
|
40
50
|
createCrossComponentProjectFolder(): Promise<void>;
|
|
41
51
|
clickCrossComponentProjectFolder(): Promise<void>;
|
|
52
|
+
private ensureWorkspaceOpen;
|
|
53
|
+
private gotoWorkspacesOverview;
|
|
54
|
+
private createWorkspace;
|
|
55
|
+
private openWorkspaceTile;
|
|
56
|
+
private openOrCreateLegacyFolder;
|
|
57
|
+
private dismissModals;
|
|
42
58
|
clickProcessDiagram(name: string): Promise<void>;
|
|
43
59
|
clickHomeBreadcrumb(): Promise<void>;
|
|
44
60
|
clickChooseBpmnTemplateButton(): Promise<void>;
|
|
@@ -21,8 +21,29 @@ class ModelerHomePage {
|
|
|
21
21
|
// is how we get back. Hub's own Playwright suite does the same.
|
|
22
22
|
processApplicationUrl;
|
|
23
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 `/workspace/{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;
|
|
24
33
|
modelerPageBanner;
|
|
25
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
|
+
workspaceWizardHeading;
|
|
43
|
+
workspaceNameInput;
|
|
44
|
+
workspaceDescriptionInput;
|
|
45
|
+
workspaceWizardSubmit;
|
|
46
|
+
workspaceMembersHeading;
|
|
26
47
|
projectNameInput;
|
|
27
48
|
moreActionsButton;
|
|
28
49
|
renameMenuItem;
|
|
@@ -127,9 +148,8 @@ class ModelerHomePage {
|
|
|
127
148
|
this.openOrganizationsButton = page.getByLabel('Open Organizations');
|
|
128
149
|
this.manageButton = page.getByRole('button', { name: 'Manage' });
|
|
129
150
|
this.crossComponentProjectFolder = page
|
|
130
|
-
.
|
|
131
|
-
exact: true
|
|
132
|
-
})
|
|
151
|
+
.getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
|
|
152
|
+
.or(page.getByTitle(this.defaultFolderName, { exact: true }))
|
|
133
153
|
.first();
|
|
134
154
|
// The Design System shell drops the breadcrumb-home hook and labels the
|
|
135
155
|
// same crumb "My organisation".
|
|
@@ -154,6 +174,29 @@ class ModelerHomePage {
|
|
|
154
174
|
this.messageBanner = page.locator('[data-test="close-top-banner"]');
|
|
155
175
|
this.closeButton = page.getByRole('button', { name: 'Got it - Dismiss' });
|
|
156
176
|
this.modalCloseButton = page.locator('.cds--modal.is-visible .cds--modal-close');
|
|
177
|
+
// exact:true — the Workspaces overview "Create workspace" split button has a
|
|
178
|
+
// sibling options chevron whose name would otherwise substring-match.
|
|
179
|
+
this.createWorkspaceButton = page.getByRole('button', {
|
|
180
|
+
name: 'Create workspace',
|
|
181
|
+
});
|
|
182
|
+
this.workspaceOpenButton = page
|
|
183
|
+
.getByRole('button', { name: /^open workspace /i })
|
|
184
|
+
.first();
|
|
185
|
+
this.noWorkspacesHeading = page.getByRole('heading', {
|
|
186
|
+
name: /no workspaces/i,
|
|
187
|
+
});
|
|
188
|
+
// Create-workspace wizard (`/workspaces/create`). Its single primary button
|
|
189
|
+
// relabels itself "Next" -> "Create workspace" as the step changes, so it is
|
|
190
|
+
// located by the stable data-test hook rather than its changing name.
|
|
191
|
+
this.workspaceWizardHeading = page.getByRole('heading', {
|
|
192
|
+
name: 'New workspace',
|
|
193
|
+
});
|
|
194
|
+
this.workspaceNameInput = page.getByLabel('Workspace name');
|
|
195
|
+
this.workspaceDescriptionInput = page.getByLabel('Description');
|
|
196
|
+
this.workspaceWizardSubmit = page.locator('[data-test="workspace-wizard-submit"]');
|
|
197
|
+
this.workspaceMembersHeading = page.getByRole('heading', {
|
|
198
|
+
name: /workspace members/i,
|
|
199
|
+
});
|
|
157
200
|
}
|
|
158
201
|
async clickCreateNewProjectButton() {
|
|
159
202
|
await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewProjectButton, {
|
|
@@ -204,44 +247,83 @@ class ModelerHomePage {
|
|
|
204
247
|
// the rest of the timeout. Transient upstream 50x is already absorbed by
|
|
205
248
|
// goToModeler's gateway-error re-navigation before we get here.
|
|
206
249
|
async waitForModelerReady(timeoutMs = 120000) {
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
250
|
+
// Flag-on: `/` redirects to the `/workspaces` overview, which renders the
|
|
251
|
+
// "Create workspace" CTA (always present) plus an "Open workspace ..." tile
|
|
252
|
+
// per existing workspace — the legacy "Create new project" button never
|
|
253
|
+
// renders there, which is what used to time out. Gate on those overview
|
|
254
|
+
// markers as well as the legacy flag-off home markers, so the wait resolves
|
|
255
|
+
// whichever shell the deployment renders. .first(): several can be present
|
|
256
|
+
// at once (e.g. the CTA and a tile), and waitFor() requires a single match.
|
|
257
|
+
const workspaceElement = this.createWorkspaceButton
|
|
258
|
+
.or(this.workspaceOpenButton)
|
|
259
|
+
.or(this.createNewProjectButton)
|
|
211
260
|
.or(this.crossComponentProjectFolder)
|
|
212
|
-
.first()
|
|
213
|
-
|
|
261
|
+
.first();
|
|
262
|
+
// The "New features" release modal renders lazily over the Modeler home,
|
|
263
|
+
// and under the Design System shell the project list -- the "Create new
|
|
264
|
+
// project" button and any existing folder included -- does not mount until
|
|
265
|
+
// the modal is dismissed. Gating on the workspace element alone therefore
|
|
266
|
+
// deadlocks: it never appears while the modal is up, and the dismissal in
|
|
267
|
+
// clickMessageBanner/clickCreateNewProjectButton only runs after this wait.
|
|
268
|
+
// Dismiss the modal on each poll until the workspace element mounts.
|
|
269
|
+
await (0, test_1.expect)(async () => {
|
|
270
|
+
await this.clickMessageBanner();
|
|
271
|
+
await (0, test_1.expect)(workspaceElement).toBeVisible({ timeout: 5000 });
|
|
272
|
+
}).toPass({ timeout: timeoutMs, intervals: [1000, 2000, 5000] });
|
|
214
273
|
}
|
|
215
274
|
async createCrossComponentProjectFolder() {
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
//
|
|
275
|
+
// Kept name: opens (creating if absent) the shared Workspace this suite puts
|
|
276
|
+
// every test's Project inside, leaving the browser on the Workspace's
|
|
277
|
+
// projects listing. The flag-off era created a top-level project folder here;
|
|
278
|
+
// under flag-on that folder maps to a Workspace.
|
|
279
|
+
await this.ensureWorkspaceOpen();
|
|
280
|
+
}
|
|
281
|
+
async clickCrossComponentProjectFolder() {
|
|
282
|
+
// Kept name: callers use this to (re-)enter the shared Workspace's projects
|
|
283
|
+
// listing after leaving Modeler. Creates the Workspace if it is somehow
|
|
284
|
+
// absent (e.g. a fresh cluster where the setup spec did not run first).
|
|
285
|
+
await this.ensureWorkspaceOpen();
|
|
286
|
+
}
|
|
287
|
+
// Ensures the shared "Cross Component Test Project" Workspace is open and the
|
|
288
|
+
// browser is on its projects listing (`/workspace/{wsId}/projects`), where the
|
|
289
|
+
// per-test Project is later created. Retries the whole navigation+login on a
|
|
290
|
+
// transient web-modeler 500 rather than a bare reload, which would drop the
|
|
291
|
+
// authenticated SPA state and land on the empty login form.
|
|
292
|
+
async ensureWorkspaceOpen() {
|
|
219
293
|
const maxAttempts = 3;
|
|
220
294
|
const retryWaitMs = 5000;
|
|
221
295
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
222
296
|
try {
|
|
297
|
+
// Go straight to the Workspaces overview rather than waiting on the
|
|
298
|
+
// post-login landing: after the OIDC callback the app briefly sits on
|
|
299
|
+
// Home (or /login-callback), which is not the workspace listing and on
|
|
300
|
+
// some builds does not auto-redirect to /workspaces. Navigating here
|
|
301
|
+
// explicitly is robust whether or not the root redirect fires.
|
|
302
|
+
await this.gotoWorkspacesOverview();
|
|
223
303
|
await this.waitForModelerReady();
|
|
224
304
|
await this.clickMessageBanner();
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
305
|
+
const flagOn = await this.becameVisible(this.createWorkspaceButton.or(this.workspaceOpenButton).first(), 30000);
|
|
306
|
+
if (!flagOn) {
|
|
307
|
+
// Legacy flag-off deployment (e.g. a pre-upgrade version in a
|
|
308
|
+
// migration flow): /workspaces is not a route, so the app served Home
|
|
309
|
+
// instead, where the shared folder IS the top-level project.
|
|
310
|
+
await this.openOrCreateLegacyFolder();
|
|
228
311
|
return;
|
|
229
312
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
313
|
+
// On the flag-on overview. Remember it for clickHomeBreadcrumb().
|
|
314
|
+
this.workspacesOverviewUrl = this.page.url();
|
|
315
|
+
const workspaceExists = await this.crossComponentProjectFolder
|
|
316
|
+
.isVisible()
|
|
317
|
+
.catch(() => false);
|
|
318
|
+
if (!workspaceExists) {
|
|
319
|
+
await this.createWorkspace(this.defaultFolderName);
|
|
320
|
+
}
|
|
321
|
+
await this.openWorkspaceTile();
|
|
233
322
|
return;
|
|
234
323
|
}
|
|
235
324
|
catch (error) {
|
|
236
325
|
if (attempt < maxAttempts - 1) {
|
|
237
|
-
console.log(`
|
|
238
|
-
// A transient web-modeler 500 on /api/internal/login right after the
|
|
239
|
-
// OAuth callback makes the SPA log the user out, destroying the
|
|
240
|
-
// Keycloak SSO session. A bare page.reload() then lands back on the
|
|
241
|
-
// empty login form and never re-authenticates, so every remaining
|
|
242
|
-
// attempt waits out its timeout on the login page. Re-run the full
|
|
243
|
-
// navigation+login instead so a recovered backend can complete the
|
|
244
|
-
// login on the next attempt.
|
|
326
|
+
console.log(`ensureWorkspaceOpen attempt ${attempt + 1} failed. Re-authenticating and retrying...`);
|
|
245
327
|
try {
|
|
246
328
|
await new NavigationPage_1.NavigationPage(this.page).goToModeler();
|
|
247
329
|
}
|
|
@@ -256,51 +338,99 @@ class ModelerHomePage {
|
|
|
256
338
|
}
|
|
257
339
|
}
|
|
258
340
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
341
|
+
// Navigates to the Workspaces overview by resolving it against the Modeler app
|
|
342
|
+
// root in the current URL, so it works whether Modeler is served under
|
|
343
|
+
// `/modeler` (Self-Managed combined ingress) or at the app root.
|
|
344
|
+
async gotoWorkspacesOverview() {
|
|
345
|
+
const current = new URL(this.page.url());
|
|
346
|
+
const appRootMatch = current.pathname.match(/^(.*\/modeler)(?:\/|$)/);
|
|
347
|
+
const appRoot = appRootMatch ? appRootMatch[1] : '';
|
|
348
|
+
await this.page.goto(`${current.origin}${appRoot}/workspaces`, {
|
|
349
|
+
waitUntil: 'domcontentloaded',
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
// Walks the create-workspace wizard (`/workspaces/create`): fill the general
|
|
353
|
+
// step, advance to the members step, and submit with the creator only (the
|
|
354
|
+
// creator is pre-added as Workspace Admin, satisfying the "at least one admin"
|
|
355
|
+
// guard). The wizard returns to the `/workspaces` overview on success. The
|
|
356
|
+
// single primary button relabels itself "Next" -> "Create workspace" between
|
|
357
|
+
// steps; match its stable data-test hook where present and fall back to the
|
|
358
|
+
// step-appropriate accessible name (the wizard route does not render the
|
|
359
|
+
// overview's own "Create workspace" button, so the name is unambiguous here).
|
|
360
|
+
async createWorkspace(name) {
|
|
361
|
+
await (0, test_1.expect)(this.createWorkspaceButton).toBeVisible({ timeout: 60000 });
|
|
362
|
+
await this.createWorkspaceButton.click({ timeout: 60000 });
|
|
363
|
+
await (0, test_1.expect)(this.workspaceWizardHeading).toBeVisible({ timeout: 60000 });
|
|
364
|
+
await this.workspaceNameInput.fill(name);
|
|
365
|
+
await this.workspaceDescriptionInput
|
|
366
|
+
.fill('Created by the cross-component E2E suite.')
|
|
367
|
+
.catch(() => { });
|
|
368
|
+
// Step 1 (General) -> members step.
|
|
369
|
+
await this.workspaceWizardSubmit
|
|
370
|
+
.or(this.page.getByRole('button', { name: 'Next', exact: true }))
|
|
371
|
+
.first()
|
|
372
|
+
.click({ timeout: 30000 });
|
|
373
|
+
await (0, test_1.expect)(this.workspaceMembersHeading).toBeVisible({ timeout: 60000 });
|
|
374
|
+
// Members step -> create.
|
|
375
|
+
await this.workspaceWizardSubmit
|
|
376
|
+
.or(this.page.getByRole('button', { name: 'Create workspace', exact: true }))
|
|
377
|
+
.first()
|
|
378
|
+
.click({ timeout: 30000 });
|
|
379
|
+
await this.page.waitForURL(/\/workspaces$/, { timeout: 120000 });
|
|
380
|
+
await this.waitForModelerReady();
|
|
381
|
+
this.workspacesOverviewUrl = this.page.url();
|
|
382
|
+
}
|
|
383
|
+
// Opens the shared Workspace's tile and waits until its projects listing has
|
|
384
|
+
// loaded, remembering that URL for later returns via clickWorkspaceBreadcrumb.
|
|
385
|
+
async openWorkspaceTile() {
|
|
386
|
+
await (0, test_1.expect)(this.crossComponentProjectFolder).toBeVisible({
|
|
387
|
+
timeout: 90000,
|
|
388
|
+
});
|
|
389
|
+
await this.dismissModals();
|
|
390
|
+
// Click the tile's "Open workspace" button specifically: the card also
|
|
391
|
+
// carries a plain title element (and a "Manage workspace" button) that the
|
|
392
|
+
// crossComponentProjectFolder .or(title) locator can resolve to first, and
|
|
393
|
+
// clicking those does not navigate to the projects listing.
|
|
394
|
+
const openButton = this.page.getByRole('button', {
|
|
395
|
+
name: `Open workspace ${this.defaultFolderName}`,
|
|
396
|
+
});
|
|
397
|
+
const target = (await openButton
|
|
398
|
+
.first()
|
|
399
|
+
.isVisible()
|
|
400
|
+
.catch(() => false))
|
|
401
|
+
? openButton.first()
|
|
402
|
+
: this.crossComponentProjectFolder;
|
|
403
|
+
await target.click({ timeout: 60000 });
|
|
404
|
+
await this.page.waitForURL(/\/workspace\/[^/]+\/projects/, {
|
|
405
|
+
timeout: 120000,
|
|
406
|
+
});
|
|
407
|
+
this.workspaceProjectsUrl = this.page.url();
|
|
408
|
+
await this.clickMessageBanner();
|
|
409
|
+
}
|
|
410
|
+
// Legacy flag-off flow, kept for pre-upgrade UIs in migration specs: the shared
|
|
411
|
+
// folder is the top-level project, created inline via "Create new project".
|
|
412
|
+
async openOrCreateLegacyFolder() {
|
|
413
|
+
if (await this.crossComponentProjectFolder.isVisible().catch(() => false)) {
|
|
414
|
+
await this.dismissModals();
|
|
415
|
+
await this.crossComponentProjectFolder.click({ timeout: 60000 });
|
|
416
|
+
await this.clickMessageBanner();
|
|
417
|
+
return;
|
|
303
418
|
}
|
|
419
|
+
await this.clickCreateNewProjectButton();
|
|
420
|
+
await this.enterNewProjectName(this.defaultFolderName);
|
|
421
|
+
await (0, test_1.expect)(this.page.getByRole('heading', { name: this.defaultFolderName })).toBeVisible({ timeout: 10000 });
|
|
422
|
+
}
|
|
423
|
+
// Dismisses the lazily-rendered "New features" / conditional-events modals that
|
|
424
|
+
// can intercept a click on the workspace tile or create controls.
|
|
425
|
+
async dismissModals() {
|
|
426
|
+
await this.closeButton
|
|
427
|
+
.waitFor({ state: 'visible', timeout: 5000 })
|
|
428
|
+
.then(() => this.closeButton.click())
|
|
429
|
+
.catch(() => { });
|
|
430
|
+
await this.modalCloseButton
|
|
431
|
+
.waitFor({ state: 'visible', timeout: 2000 })
|
|
432
|
+
.then(() => this.modalCloseButton.click())
|
|
433
|
+
.catch(() => { });
|
|
304
434
|
}
|
|
305
435
|
// Opens a resource where this test put it. Several specs leave Modeler
|
|
306
436
|
// entirely (to Optimize, to Operate) and come back via
|
|
@@ -313,6 +443,15 @@ class ModelerHomePage {
|
|
|
313
443
|
await process.click();
|
|
314
444
|
}
|
|
315
445
|
async clickHomeBreadcrumb() {
|
|
446
|
+
// Flag-on: "home" for Modeler is the `/workspaces` overview (the top of the
|
|
447
|
+
// hierarchy), remembered when the Workspace was opened.
|
|
448
|
+
if (this.workspacesOverviewUrl) {
|
|
449
|
+
await this.page.goto(this.workspacesOverviewUrl, {
|
|
450
|
+
waitUntil: 'domcontentloaded',
|
|
451
|
+
});
|
|
452
|
+
await this.waitForModelerReady();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
316
455
|
try {
|
|
317
456
|
await this.homeBreadcrumb.click({ timeout: 60000 });
|
|
318
457
|
}
|
|
@@ -456,8 +595,18 @@ class ModelerHomePage {
|
|
|
456
595
|
}
|
|
457
596
|
// The workspace ("project") level itself, for the surfaces that genuinely
|
|
458
597
|
// live there -- collaborators, IDP applications, published element
|
|
459
|
-
// templates.
|
|
598
|
+
// templates. Under flag-on this is the Workspace's projects listing.
|
|
460
599
|
async clickWorkspaceBreadcrumb() {
|
|
600
|
+
if (this.workspaceProjectsUrl) {
|
|
601
|
+
await this.page.goto(this.workspaceProjectsUrl, {
|
|
602
|
+
waitUntil: 'domcontentloaded',
|
|
603
|
+
});
|
|
604
|
+
await this.processApplicationPage.createProcessApplicationButton
|
|
605
|
+
.waitFor({ state: 'visible', timeout: 90000 })
|
|
606
|
+
.catch(() => { });
|
|
607
|
+
await this.clickMessageBanner();
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
461
610
|
await this.workspaceBreadcrumb.click({ timeout: 60000 });
|
|
462
611
|
}
|
|
463
612
|
// Creates, once per test, the process application that holds this test's
|
|
@@ -484,6 +633,16 @@ class ModelerHomePage {
|
|
|
484
633
|
await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
|
|
485
634
|
this.processApplicationUrl = this.page.url();
|
|
486
635
|
this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
|
|
636
|
+
// Under workspaces-enabled, a freshly created process application has no
|
|
637
|
+
// cluster assigned to its development stage: Deploy (and Play's "Configure
|
|
638
|
+
// environment") render a "Clusters not configured for stages" warning and
|
|
639
|
+
// stay disabled until one is. Assign it once here, right after the
|
|
640
|
+
// container is created, so every later deploy/run/play/connector/form flow
|
|
641
|
+
// in this test finds a usable cluster instead of independently reproducing
|
|
642
|
+
// this connection step (or failing on the warning). ensureStageCluster()
|
|
643
|
+
// memoizes per test, so runProcessInstance()'s own call is a no-op after
|
|
644
|
+
// this.
|
|
645
|
+
await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
|
|
487
646
|
}
|
|
488
647
|
// Which control creates a process application depends on the hub version
|
|
489
648
|
// under test. Since camunda/camunda-hub#25831 it is a plain primary button on
|
|
@@ -6,22 +6,30 @@ const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModal
|
|
|
6
6
|
// ModelerHomePage creates the container, while ModelerCreatePage later binds
|
|
7
7
|
// its stage. Both page objects share the same Playwright Page.
|
|
8
8
|
const resourceContainers = new WeakMap();
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
9
|
+
// Flag-on Self-Managed 8.10 (camunda-hub#27903: workspaces-enabled=true) renamed
|
|
10
|
+
// the entity hierarchy: the legacy "project folder" is now a Workspace and the
|
|
11
|
+
// legacy "process application" is now a Project (camunda-hub#28005). A Project is
|
|
12
|
+
// the resource container -- files, folders and uploads live directly inside it,
|
|
13
|
+
// there is no separate process-application level to descend into. It is created
|
|
14
|
+
// from the workspace's projects listing via the "Create project" split button +
|
|
15
|
+
// dialog, and the app lands on `/workspace/{wsId}/projects/{projectId}`.
|
|
16
|
+
//
|
|
17
|
+
// This page object owns that container's surface (creating a Project, and binding
|
|
18
|
+
// its development-stage cluster). Entering and leaving it is driven from
|
|
19
|
+
// ModelerHomePage so the ~150 existing spec call sites keep working unchanged.
|
|
20
|
+
// Method names are kept from the flag-off era ("...ProcessApplication...") so the
|
|
21
|
+
// callers do not have to change.
|
|
16
22
|
class ProcessApplicationPage {
|
|
17
23
|
page;
|
|
18
|
-
//
|
|
19
|
-
//
|
|
24
|
+
// The workspace projects listing's "Create project" split button. exact:true
|
|
25
|
+
// because the split button has a sibling "Create project options" chevron whose
|
|
26
|
+
// accessible name substring-matches otherwise (mirrors the hub team's own
|
|
27
|
+
// WorkspaceProjectsPage.newProjectButton). Falls back to the legacy flag-off
|
|
28
|
+
// "Create process application" button so a deployment with the flag still off
|
|
29
|
+
// keeps working.
|
|
20
30
|
createProcessApplicationButton;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
// than the primary path -- its own unit test asserts the item is absent
|
|
24
|
-
// there. Kept because the Hub PR is still moving this surface.
|
|
31
|
+
// Legacy flag-off fallback only: the "Create new" menu item. Not rendered under
|
|
32
|
+
// workspaces-enabled, where the split button above is the entry point.
|
|
25
33
|
createProcessApplicationOption;
|
|
26
34
|
creationDialog;
|
|
27
35
|
nameInput;
|
|
@@ -37,45 +45,54 @@ class ProcessApplicationPage {
|
|
|
37
45
|
defineStagesSaveButton;
|
|
38
46
|
constructor(page) {
|
|
39
47
|
this.page = page;
|
|
40
|
-
// .first() defensively only: hub renders exactly one of these today, in the
|
|
41
|
-
// workspace-root empty state. A future second placement should not turn
|
|
42
|
-
// every PA-creating test into a strict-mode violation.
|
|
43
48
|
this.createProcessApplicationButton = page
|
|
44
|
-
.getByRole('button', { name: 'Create
|
|
49
|
+
.getByRole('button', { name: 'Create project', exact: true })
|
|
50
|
+
.or(page.getByRole('button', { name: 'Create process application' }))
|
|
45
51
|
.first();
|
|
46
52
|
this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
53
|
+
// The workspace-mode create-project dialog carries a "Project name" field and
|
|
54
|
+
// has no accessible dialog name; scope to it via that field. Keeps the legacy
|
|
55
|
+
// flag-off "Create a process application" dialog as a fallback.
|
|
56
|
+
this.creationDialog = page
|
|
57
|
+
.getByRole('dialog')
|
|
58
|
+
.filter({ has: page.getByLabel('Project name') })
|
|
59
|
+
.or(page.getByRole('dialog', { name: 'Create a process application' }))
|
|
60
|
+
.first();
|
|
50
61
|
// Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
|
|
51
62
|
// getByLabel('Name', {exact: true}) for the properties panel, which would
|
|
52
63
|
// resolve to two elements while the creation modal is open.
|
|
53
|
-
this.nameInput = this.creationDialog
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
64
|
+
this.nameInput = this.creationDialog
|
|
65
|
+
.getByLabel('Project name')
|
|
66
|
+
.or(this.creationDialog.getByLabel('Name', { exact: true }))
|
|
67
|
+
.first();
|
|
68
|
+
this.submitButton = this.creationDialog
|
|
69
|
+
.getByRole('button', { name: 'Create project', exact: true })
|
|
70
|
+
.or(this.creationDialog.getByRole('button', { name: 'Create', exact: true }))
|
|
71
|
+
.first();
|
|
58
72
|
this.cancelButton = this.creationDialog.getByRole('button', {
|
|
59
73
|
name: 'Cancel',
|
|
60
74
|
exact: true,
|
|
61
75
|
});
|
|
62
|
-
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
// so matching either keeps createNewDropdown below correctly scoped.
|
|
76
|
+
// Some shells expose a labelled resources region ("Resources" on the DS
|
|
77
|
+
// twin, "Folder explorer" on Carbon); the 8.10 flag-on Project detail does
|
|
78
|
+
// not, so this is only a soft readiness signal, never a required scope.
|
|
66
79
|
this.folderExplorer = page
|
|
67
80
|
.getByRole('region', { name: 'Folder explorer' })
|
|
68
81
|
.or(page.getByRole('region', { name: 'Resources' }))
|
|
69
82
|
.first();
|
|
70
|
-
//
|
|
71
|
-
//
|
|
72
|
-
// breadcrumb-project is.
|
|
83
|
+
// Proof we are on a Project (resource container) page: the flag-on Project
|
|
84
|
+
// detail renders this title-bar hook.
|
|
73
85
|
this.titleBar = page.locator('[data-test="process-application-title-bar"]');
|
|
74
|
-
//
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
// Any Project-detail marker also serves as an "inside the container" probe.
|
|
87
|
+
this.breadcrumb = this.titleBar.or(this.folderExplorer).first();
|
|
88
|
+
// The Project detail's "Create new" menu. Flag-on 8.10 renders it as
|
|
89
|
+
// [data-test="create-action-dropdown"] (aria-label "Create new"); older
|
|
90
|
+
// shells used [data-test="diagram-dropdown"]. Match either, page-wide,
|
|
91
|
+
// because the flag-on Project detail exposes no resources region to scope to.
|
|
92
|
+
this.createNewDropdown = page
|
|
93
|
+
.locator('[data-test="create-action-dropdown"]')
|
|
94
|
+
.or(page.locator('[data-test="diagram-dropdown"]'))
|
|
95
|
+
.or(page.getByRole('button', { name: 'Create new' }))
|
|
79
96
|
.first();
|
|
80
97
|
this.inlineNameInput = page.locator('[data-test="editable-input"]');
|
|
81
98
|
this.defineStagesDialog = page
|
|
@@ -87,12 +104,13 @@ class ProcessApplicationPage {
|
|
|
87
104
|
exact: true,
|
|
88
105
|
});
|
|
89
106
|
}
|
|
90
|
-
// Any one of the title bar, the resource region or the
|
|
91
|
-
// are on the
|
|
92
|
-
// System and Carbon shells expose different subsets: only the title bar is
|
|
93
|
-
// present under both. .first() because several match once it has rendered.
|
|
107
|
+
// Any one of the title bar, the resource region or the create-new dropdown
|
|
108
|
+
// proves we are on the Project (resource container) page.
|
|
94
109
|
pageReady() {
|
|
95
|
-
return this.titleBar
|
|
110
|
+
return this.titleBar
|
|
111
|
+
.or(this.createNewDropdown)
|
|
112
|
+
.or(this.folderExplorer)
|
|
113
|
+
.first();
|
|
96
114
|
}
|
|
97
115
|
async waitForProcessApplication(timeout = 90000) {
|
|
98
116
|
await (0, test_1.expect)(this.pageReady()).toBeVisible({ timeout });
|
|
@@ -109,10 +127,8 @@ class ProcessApplicationPage {
|
|
|
109
127
|
});
|
|
110
128
|
await this.createProcessApplicationOption.click({ timeout: 60000 });
|
|
111
129
|
}
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// inline path is kept as a fallback — this is the one method that has to
|
|
115
|
-
// change if naming moves back inline.
|
|
130
|
+
// A Project is named up front in the create-project dialog. The inline path is
|
|
131
|
+
// kept as a fallback for the legacy flag-off "process application" surface.
|
|
116
132
|
async nameNewProcessApplication(name) {
|
|
117
133
|
const isModal = await this.creationDialog
|
|
118
134
|
.waitFor({ state: 'visible', timeout: 15000 })
|
|
@@ -123,6 +139,10 @@ class ProcessApplicationPage {
|
|
|
123
139
|
await this.nameInput.fill(name);
|
|
124
140
|
await (0, test_1.expect)(this.nameInput).toHaveValue(name, { timeout: 10000 });
|
|
125
141
|
await (0, submitModalWithRetry_1.submitModalWithRetry)(this.submitButton, this.creationDialog);
|
|
142
|
+
// The app navigates to the new Project's own detail page on submit.
|
|
143
|
+
await this.page
|
|
144
|
+
.waitForURL(/\/projects\/[^/?#]+/, { timeout: 60000 })
|
|
145
|
+
.catch(() => { });
|
|
126
146
|
await this.waitForProcessApplication();
|
|
127
147
|
return;
|
|
128
148
|
}
|
|
@@ -158,13 +178,19 @@ class ProcessApplicationPage {
|
|
|
158
178
|
return;
|
|
159
179
|
}
|
|
160
180
|
const returnTo = this.page.url();
|
|
181
|
+
// Under workspaces-enabled there is no `/connected-clusters` sub-route: the
|
|
182
|
+
// "Connected clusters" card lives in the Project detail's sidebar and its
|
|
183
|
+
// "Configure" button opens the Define stages modal (see camunda-hub
|
|
184
|
+
// ConnectedClustersDs). So navigate to the Project detail itself and let
|
|
185
|
+
// openStageDefinition() click that button.
|
|
161
186
|
const processApplicationUrl = container.url
|
|
162
187
|
.replace(/[?#].*$/, '')
|
|
163
188
|
.replace(/\/$/, '');
|
|
164
189
|
await (0, test_1.expect)(async () => {
|
|
165
|
-
await this.page.goto(
|
|
190
|
+
await this.page.goto(processApplicationUrl, {
|
|
166
191
|
waitUntil: 'domcontentloaded',
|
|
167
192
|
});
|
|
193
|
+
await this.waitForProcessApplication();
|
|
168
194
|
await this.openStageDefinition();
|
|
169
195
|
await this.defineDevelopmentStage(clusterName);
|
|
170
196
|
}).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
|
|
@@ -174,34 +200,38 @@ class ProcessApplicationPage {
|
|
|
174
200
|
}
|
|
175
201
|
async openStageDefinition() {
|
|
176
202
|
const opened = await this.defineStagesDialog
|
|
177
|
-
.waitFor({ state: 'visible', timeout:
|
|
203
|
+
.waitFor({ state: 'visible', timeout: 3000 })
|
|
178
204
|
.then(() => true)
|
|
179
205
|
.catch(() => false);
|
|
180
206
|
if (opened) {
|
|
181
207
|
return;
|
|
182
208
|
}
|
|
209
|
+
// The "Connected clusters" card's action is labelled "Configure"; earlier
|
|
210
|
+
// shells used "Define stages" / "Edit stages".
|
|
183
211
|
const reopen = this.page
|
|
184
|
-
.getByRole('button', { name:
|
|
212
|
+
.getByRole('button', { name: /^(define stages|edit stages|configure)$/i })
|
|
185
213
|
.first();
|
|
186
214
|
const canReopen = await reopen
|
|
187
|
-
.waitFor({ state: 'visible', timeout:
|
|
215
|
+
.waitFor({ state: 'visible', timeout: 15000 })
|
|
188
216
|
.then(() => true)
|
|
189
217
|
.catch(() => false);
|
|
190
218
|
if (!canReopen) {
|
|
191
|
-
throw new Error('Could not open the "Define stages" dialog
|
|
192
|
-
'
|
|
193
|
-
'
|
|
219
|
+
throw new Error('Could not open the "Define stages" dialog: it did not open by ' +
|
|
220
|
+
'itself and no "Configure" / "Define stages" / "Edit stages" ' +
|
|
221
|
+
'control was found on the Project detail page.');
|
|
194
222
|
}
|
|
195
223
|
await reopen.click({ timeout: 30000 });
|
|
196
224
|
}
|
|
197
225
|
async openByName(name) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
.
|
|
226
|
+
// Workspace-mode projects table: the whole row is the click target (see the
|
|
227
|
+
// hub team's WorkspaceProjectsPage). Fall back to the legacy title cell.
|
|
228
|
+
const projectRow = this.page
|
|
229
|
+
.getByRole('row', { name })
|
|
230
|
+
.first()
|
|
231
|
+
.or(this.page.getByRole('row').filter({ hasText: name }).getByTitle(name))
|
|
202
232
|
.first();
|
|
203
|
-
await (0, test_1.expect)(
|
|
204
|
-
await
|
|
233
|
+
await (0, test_1.expect)(projectRow).toBeVisible({ timeout: 90000 });
|
|
234
|
+
await projectRow.click({ timeout: 60000 });
|
|
205
235
|
await this.waitForProcessApplication();
|
|
206
236
|
}
|
|
207
237
|
}
|