@camunda/e2e-test-suite 0.0.1126 → 0.0.1128

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
- this.embedButton = page.locator('[data-test="confirm-move"]');
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
- await this.page
428
+ const legacyItem = this.page
419
429
  .locator(`[data-test="item-${name}"]`)
420
- .getByText(name)
421
- .click({ timeout: 90000 });
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,9 +5,22 @@ 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;
21
+ readonly workspaceWizardNameInput: Locator;
22
+ readonly workspaceWizardSubmitButton: Locator;
23
+ readonly workspaceWizardMembersStep: Locator;
11
24
  readonly moreActionsButton: Locator;
12
25
  readonly renameMenuItem: Locator;
13
26
  readonly renameInput: Locator;
@@ -36,9 +49,16 @@ declare class ModelerHomePage {
36
49
  constructor(page: Page);
37
50
  clickCreateNewProjectButton(): Promise<void>;
38
51
  enterNewProjectName(name: string): Promise<void>;
52
+ private createWorkspaceViaWizard;
39
53
  waitForModelerReady(timeoutMs?: number): Promise<void>;
40
54
  createCrossComponentProjectFolder(): Promise<void>;
41
55
  clickCrossComponentProjectFolder(): Promise<void>;
56
+ private ensureWorkspaceOpen;
57
+ private gotoWorkspacesOverview;
58
+ private createWorkspace;
59
+ private openWorkspaceTile;
60
+ private openOrCreateLegacyFolder;
61
+ private dismissModals;
42
62
  clickProcessDiagram(name: string): Promise<void>;
43
63
  clickHomeBreadcrumb(): Promise<void>;
44
64
  clickChooseBpmnTemplateButton(): Promise<void>;
@@ -21,9 +21,33 @@ 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;
48
+ workspaceWizardNameInput;
49
+ workspaceWizardSubmitButton;
50
+ workspaceWizardMembersStep;
27
51
  moreActionsButton;
28
52
  renameMenuItem;
29
53
  renameInput;
@@ -75,6 +99,14 @@ class ModelerHomePage {
75
99
  exact: true,
76
100
  }));
77
101
  this.projectNameInput = page.locator('[data-test="editable-input"]');
102
+ // The Design System shell no longer creates an empty project and renames it
103
+ // inline; "Create workspace" opens a two-step wizard (General -> Members)
104
+ // that names the workspace up front. The name lives on the General step, and
105
+ // the submit button keeps this test id across both steps ("Next" on General,
106
+ // "Create workspace" on Members).
107
+ this.workspaceWizardNameInput = page.locator('#workspace-name');
108
+ this.workspaceWizardSubmitButton = page.locator('[data-test="workspace-wizard-submit"]');
109
+ this.workspaceWizardMembersStep = page.locator('[data-test="step-members"]');
78
110
  // The rename kebab sits beside the breadcrumb in the app header. The
79
111
  // editor's action bar is a <header> too, so it also carries the banner
80
112
  // role -- getByRole('banner') alone matches three headers in the editor
@@ -127,9 +159,8 @@ class ModelerHomePage {
127
159
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
128
160
  this.manageButton = page.getByRole('button', { name: 'Manage' });
129
161
  this.crossComponentProjectFolder = page
130
- .getByTitle(this.defaultFolderName, {
131
- exact: true,
132
- })
162
+ .getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
163
+ .or(page.getByTitle(this.defaultFolderName, { exact: true }))
133
164
  .first();
134
165
  // The Design System shell drops the breadcrumb-home hook and labels the
135
166
  // same crumb "My organisation".
@@ -154,6 +185,29 @@ class ModelerHomePage {
154
185
  this.messageBanner = page.locator('[data-test="close-top-banner"]');
155
186
  this.closeButton = page.getByRole('button', { name: 'Got it - Dismiss' });
156
187
  this.modalCloseButton = page.locator('.cds--modal.is-visible .cds--modal-close');
188
+ // exact:true — the Workspaces overview "Create workspace" split button has a
189
+ // sibling options chevron whose name would otherwise substring-match.
190
+ this.createWorkspaceButton = page.getByRole('button', {
191
+ name: 'Create workspace',
192
+ });
193
+ this.workspaceOpenButton = page
194
+ .getByRole('button', { name: /^open workspace /i })
195
+ .first();
196
+ this.noWorkspacesHeading = page.getByRole('heading', {
197
+ name: /no workspaces/i,
198
+ });
199
+ // Create-workspace wizard (`/workspaces/create`). Its single primary button
200
+ // relabels itself "Next" -> "Create workspace" as the step changes, so it is
201
+ // located by the stable data-test hook rather than its changing name.
202
+ this.workspaceWizardHeading = page.getByRole('heading', {
203
+ name: 'New workspace',
204
+ });
205
+ this.workspaceNameInput = page.getByLabel('Workspace name');
206
+ this.workspaceDescriptionInput = page.getByLabel('Description');
207
+ this.workspaceWizardSubmit = page.locator('[data-test="workspace-wizard-submit"]');
208
+ this.workspaceMembersHeading = page.getByRole('heading', {
209
+ name: /workspace members/i,
210
+ });
157
211
  }
158
212
  async clickCreateNewProjectButton() {
159
213
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewProjectButton, {
@@ -179,8 +233,16 @@ class ModelerHomePage {
179
233
  async enterNewProjectName(name) {
180
234
  await this.projectNameInput
181
235
  .or(this.moreActionsButton)
236
+ .or(this.workspaceWizardNameInput)
182
237
  .first()
183
238
  .waitFor({ state: 'visible', timeout: 30000 });
239
+ // Design System shell: "Create workspace" opens the creation wizard, which
240
+ // names the workspace on its first step, so there is no post-creation rename
241
+ // to perform. Check this before the inline branch — the wizard replaces it.
242
+ if (await this.workspaceWizardNameInput.isVisible()) {
243
+ await this.createWorkspaceViaWizard(name);
244
+ return;
245
+ }
184
246
  // Inline first: when the Carbon shell renders it, it is the whole flow.
185
247
  if (await this.projectNameInput.isVisible()) {
186
248
  await this.projectNameInput.click({ timeout: 60000 });
@@ -190,6 +252,17 @@ class ModelerHomePage {
190
252
  }
191
253
  await this.renameViaMoreActions(name);
192
254
  }
255
+ // Drives hub's two-step workspace-creation wizard. The name is entered on the
256
+ // General step, which enables "Next"; the Members step is optional and the
257
+ // second submit ("Create workspace") creates the workspace. Gate that second
258
+ // click on the Members step becoming active so the shared submit button is not
259
+ // clicked twice while still on General.
260
+ async createWorkspaceViaWizard(name) {
261
+ await this.workspaceWizardNameInput.fill(name);
262
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
263
+ await (0, test_1.expect)(this.workspaceWizardMembersStep).toHaveAttribute('data-state', 'active', { timeout: 30000 });
264
+ await this.workspaceWizardSubmitButton.click({ timeout: 60000 });
265
+ }
193
266
  // Wait for the Modeler project workspace to become interactive. The caller
194
267
  // arrives here via goToModeler(), which has already loaded and authenticated
195
268
  // the app shell (banner visible). The nav banner mounts before the project
@@ -204,44 +277,83 @@ class ModelerHomePage {
204
277
  // the rest of the timeout. Transient upstream 50x is already absorbed by
205
278
  // goToModeler's gateway-error re-navigation before we get here.
206
279
  async waitForModelerReady(timeoutMs = 120000) {
207
- // .first(): when the project folder already exists on the shared cluster,
208
- // both the create-project button and the folder are present, so the .or()
209
- // resolves to multiple elements — waitFor() requires a single match.
210
- await this.createNewProjectButton
280
+ // Flag-on: `/` redirects to the `/workspaces` overview, which renders the
281
+ // "Create workspace" CTA (always present) plus an "Open workspace ..." tile
282
+ // per existing workspace — the legacy "Create new project" button never
283
+ // renders there, which is what used to time out. Gate on those overview
284
+ // markers as well as the legacy flag-off home markers, so the wait resolves
285
+ // whichever shell the deployment renders. .first(): several can be present
286
+ // at once (e.g. the CTA and a tile), and waitFor() requires a single match.
287
+ const workspaceElement = this.createWorkspaceButton
288
+ .or(this.workspaceOpenButton)
289
+ .or(this.createNewProjectButton)
211
290
  .or(this.crossComponentProjectFolder)
212
- .first()
213
- .waitFor({ state: 'visible', timeout: timeoutMs });
291
+ .first();
292
+ // The "New features" release modal renders lazily over the Modeler home,
293
+ // and under the Design System shell the project list -- the "Create new
294
+ // project" button and any existing folder included -- does not mount until
295
+ // the modal is dismissed. Gating on the workspace element alone therefore
296
+ // deadlocks: it never appears while the modal is up, and the dismissal in
297
+ // clickMessageBanner/clickCreateNewProjectButton only runs after this wait.
298
+ // Dismiss the modal on each poll until the workspace element mounts.
299
+ await (0, test_1.expect)(async () => {
300
+ await this.clickMessageBanner();
301
+ await (0, test_1.expect)(workspaceElement).toBeVisible({ timeout: 5000 });
302
+ }).toPass({ timeout: timeoutMs, intervals: [1000, 2000, 5000] });
214
303
  }
215
304
  async createCrossComponentProjectFolder() {
216
- // After a fresh user login (e.g. lisa, bart) the Modeler pod may still be
217
- // initialising or returning transient 503s. Poll until the app shell banner
218
- // is visible before interacting with the project list.
305
+ // Kept name: opens (creating if absent) the shared Workspace this suite puts
306
+ // every test's Project inside, leaving the browser on the Workspace's
307
+ // projects listing. The flag-off era created a top-level project folder here;
308
+ // under flag-on that folder maps to a Workspace.
309
+ await this.ensureWorkspaceOpen();
310
+ }
311
+ async clickCrossComponentProjectFolder() {
312
+ // Kept name: callers use this to (re-)enter the shared Workspace's projects
313
+ // listing after leaving Modeler. Creates the Workspace if it is somehow
314
+ // absent (e.g. a fresh cluster where the setup spec did not run first).
315
+ await this.ensureWorkspaceOpen();
316
+ }
317
+ // Ensures the shared "Cross Component Test Project" Workspace is open and the
318
+ // browser is on its projects listing (`/workspace/{wsId}/projects`), where the
319
+ // per-test Project is later created. Retries the whole navigation+login on a
320
+ // transient web-modeler 500 rather than a bare reload, which would drop the
321
+ // authenticated SPA state and land on the empty login form.
322
+ async ensureWorkspaceOpen() {
219
323
  const maxAttempts = 3;
220
324
  const retryWaitMs = 5000;
221
325
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
222
326
  try {
327
+ // Go straight to the Workspaces overview rather than waiting on the
328
+ // post-login landing: after the OIDC callback the app briefly sits on
329
+ // Home (or /login-callback), which is not the workspace listing and on
330
+ // some builds does not auto-redirect to /workspaces. Navigating here
331
+ // explicitly is robust whether or not the root redirect fires.
332
+ await this.gotoWorkspacesOverview();
223
333
  await this.waitForModelerReady();
224
334
  await this.clickMessageBanner();
225
- if (await this.crossComponentProjectFolder.isVisible()) {
226
- console.log('Cross Component Project folder already exists. Clicking into it');
227
- await this.clickCrossComponentProjectFolder();
335
+ const flagOn = await this.becameVisible(this.createWorkspaceButton.or(this.workspaceOpenButton).first(), 30000);
336
+ if (!flagOn) {
337
+ // Legacy flag-off deployment (e.g. a pre-upgrade version in a
338
+ // migration flow): /workspaces is not a route, so the app served Home
339
+ // instead, where the shared folder IS the top-level project.
340
+ await this.openOrCreateLegacyFolder();
228
341
  return;
229
342
  }
230
- await this.clickCreateNewProjectButton();
231
- await this.enterNewProjectName(this.defaultFolderName);
232
- await (0, test_1.expect)(this.page.getByRole('heading', { name: this.defaultFolderName })).toBeVisible({ timeout: 10000 });
343
+ // On the flag-on overview. Remember it for clickHomeBreadcrumb().
344
+ this.workspacesOverviewUrl = this.page.url();
345
+ const workspaceExists = await this.crossComponentProjectFolder
346
+ .isVisible()
347
+ .catch(() => false);
348
+ if (!workspaceExists) {
349
+ await this.createWorkspace(this.defaultFolderName);
350
+ }
351
+ await this.openWorkspaceTile();
233
352
  return;
234
353
  }
235
354
  catch (error) {
236
355
  if (attempt < maxAttempts - 1) {
237
- console.log(`createCrossComponentProjectFolder attempt ${attempt + 1} failed. Re-authenticating and retrying...`);
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.
356
+ console.log(`ensureWorkspaceOpen attempt ${attempt + 1} failed. Re-authenticating and retrying...`);
245
357
  try {
246
358
  await new NavigationPage_1.NavigationPage(this.page).goToModeler();
247
359
  }
@@ -256,51 +368,99 @@ class ModelerHomePage {
256
368
  }
257
369
  }
258
370
  }
259
- async clickCrossComponentProjectFolder() {
260
- let attempts = 0;
261
- const maxAttempts = 3;
262
- const timeout = 60000;
263
- while (attempts < maxAttempts) {
264
- try {
265
- await this.clickMessageBanner();
266
- await (0, test_1.expect)(this.crossComponentProjectFolder).toBeVisible({ timeout });
267
- // The WhatsNew modal renders lazily after page load and intercepts clicks.
268
- // Explicitly wait for it to appear (up to 5s), dismiss it, then click.
269
- await this.closeButton
270
- .waitFor({ state: 'visible', timeout: 5000 })
271
- .then(() => this.closeButton.click())
272
- .catch(() => { });
273
- await this.modalCloseButton
274
- .waitFor({ state: 'visible', timeout: 2000 })
275
- .then(() => this.modalCloseButton.click())
276
- .catch(() => { });
277
- await this.crossComponentProjectFolder.click();
278
- await this.clickMessageBanner();
279
- return;
280
- }
281
- catch (error) {
282
- attempts++;
283
- if (attempts < maxAttempts) {
284
- console.log(`Attempt ${attempts} failed. Re-navigating to Modeler and retrying...`);
285
- // A transient web-modeler 500 on login right after the OAuth callback
286
- // logs the user out and leaves the page on the Keycloak login form. A
287
- // bare page.reload() then just reloads the login page and never
288
- // re-authenticates, so every remaining attempt waits out its timeout
289
- // on the login screen. Re-run the full navigation+login instead so a
290
- // recovered backend can complete the login on the next attempt.
291
- try {
292
- await new NavigationPage_1.NavigationPage(this.page).goToModeler();
293
- }
294
- catch (navError) {
295
- console.log(`Re-navigation to Modeler failed: ${navError}`);
296
- }
297
- await this.page.waitForLoadState('networkidle');
298
- }
299
- else {
300
- throw new Error('Failed to click crossComponentProjectFolder after 3 attempts.');
301
- }
302
- }
371
+ // Navigates to the Workspaces overview by resolving it against the Modeler app
372
+ // root in the current URL, so it works whether Modeler is served under
373
+ // `/modeler` (Self-Managed combined ingress) or at the app root.
374
+ async gotoWorkspacesOverview() {
375
+ const current = new URL(this.page.url());
376
+ const appRootMatch = current.pathname.match(/^(.*\/modeler)(?:\/|$)/);
377
+ const appRoot = appRootMatch ? appRootMatch[1] : '';
378
+ await this.page.goto(`${current.origin}${appRoot}/workspaces`, {
379
+ waitUntil: 'domcontentloaded',
380
+ });
381
+ }
382
+ // Walks the create-workspace wizard (`/workspaces/create`): fill the general
383
+ // step, advance to the members step, and submit with the creator only (the
384
+ // creator is pre-added as Workspace Admin, satisfying the "at least one admin"
385
+ // guard). The wizard returns to the `/workspaces` overview on success. The
386
+ // single primary button relabels itself "Next" -> "Create workspace" between
387
+ // steps; match its stable data-test hook where present and fall back to the
388
+ // step-appropriate accessible name (the wizard route does not render the
389
+ // overview's own "Create workspace" button, so the name is unambiguous here).
390
+ async createWorkspace(name) {
391
+ await (0, test_1.expect)(this.createWorkspaceButton).toBeVisible({ timeout: 60000 });
392
+ await this.createWorkspaceButton.click({ timeout: 60000 });
393
+ await (0, test_1.expect)(this.workspaceWizardHeading).toBeVisible({ timeout: 60000 });
394
+ await this.workspaceNameInput.fill(name);
395
+ await this.workspaceDescriptionInput
396
+ .fill('Created by the cross-component E2E suite.')
397
+ .catch(() => { });
398
+ // Step 1 (General) -> members step.
399
+ await this.workspaceWizardSubmit
400
+ .or(this.page.getByRole('button', { name: 'Next', exact: true }))
401
+ .first()
402
+ .click({ timeout: 30000 });
403
+ await (0, test_1.expect)(this.workspaceMembersHeading).toBeVisible({ timeout: 60000 });
404
+ // Members step -> create.
405
+ await this.workspaceWizardSubmit
406
+ .or(this.page.getByRole('button', { name: 'Create workspace', exact: true }))
407
+ .first()
408
+ .click({ timeout: 30000 });
409
+ await this.page.waitForURL(/\/workspaces$/, { timeout: 120000 });
410
+ await this.waitForModelerReady();
411
+ this.workspacesOverviewUrl = this.page.url();
412
+ }
413
+ // Opens the shared Workspace's tile and waits until its projects listing has
414
+ // loaded, remembering that URL for later returns via clickWorkspaceBreadcrumb.
415
+ async openWorkspaceTile() {
416
+ await (0, test_1.expect)(this.crossComponentProjectFolder).toBeVisible({
417
+ timeout: 90000,
418
+ });
419
+ await this.dismissModals();
420
+ // Click the tile's "Open workspace" button specifically: the card also
421
+ // carries a plain title element (and a "Manage workspace" button) that the
422
+ // crossComponentProjectFolder .or(title) locator can resolve to first, and
423
+ // clicking those does not navigate to the projects listing.
424
+ const openButton = this.page.getByRole('button', {
425
+ name: `Open workspace ${this.defaultFolderName}`,
426
+ });
427
+ const target = (await openButton
428
+ .first()
429
+ .isVisible()
430
+ .catch(() => false))
431
+ ? openButton.first()
432
+ : this.crossComponentProjectFolder;
433
+ await target.click({ timeout: 60000 });
434
+ await this.page.waitForURL(/\/workspace\/[^/]+\/projects/, {
435
+ timeout: 120000,
436
+ });
437
+ this.workspaceProjectsUrl = this.page.url();
438
+ await this.clickMessageBanner();
439
+ }
440
+ // Legacy flag-off flow, kept for pre-upgrade UIs in migration specs: the shared
441
+ // folder is the top-level project, created inline via "Create new project".
442
+ async openOrCreateLegacyFolder() {
443
+ if (await this.crossComponentProjectFolder.isVisible().catch(() => false)) {
444
+ await this.dismissModals();
445
+ await this.crossComponentProjectFolder.click({ timeout: 60000 });
446
+ await this.clickMessageBanner();
447
+ return;
303
448
  }
449
+ await this.clickCreateNewProjectButton();
450
+ await this.enterNewProjectName(this.defaultFolderName);
451
+ await (0, test_1.expect)(this.page.getByRole('heading', { name: this.defaultFolderName })).toBeVisible({ timeout: 10000 });
452
+ }
453
+ // Dismisses the lazily-rendered "New features" / conditional-events modals that
454
+ // can intercept a click on the workspace tile or create controls.
455
+ async dismissModals() {
456
+ await this.closeButton
457
+ .waitFor({ state: 'visible', timeout: 5000 })
458
+ .then(() => this.closeButton.click())
459
+ .catch(() => { });
460
+ await this.modalCloseButton
461
+ .waitFor({ state: 'visible', timeout: 2000 })
462
+ .then(() => this.modalCloseButton.click())
463
+ .catch(() => { });
304
464
  }
305
465
  // Opens a resource where this test put it. Several specs leave Modeler
306
466
  // entirely (to Optimize, to Operate) and come back via
@@ -313,6 +473,15 @@ class ModelerHomePage {
313
473
  await process.click();
314
474
  }
315
475
  async clickHomeBreadcrumb() {
476
+ // Flag-on: "home" for Modeler is the `/workspaces` overview (the top of the
477
+ // hierarchy), remembered when the Workspace was opened.
478
+ if (this.workspacesOverviewUrl) {
479
+ await this.page.goto(this.workspacesOverviewUrl, {
480
+ waitUntil: 'domcontentloaded',
481
+ });
482
+ await this.waitForModelerReady();
483
+ return;
484
+ }
316
485
  try {
317
486
  await this.homeBreadcrumb.click({ timeout: 60000 });
318
487
  }
@@ -456,8 +625,18 @@ class ModelerHomePage {
456
625
  }
457
626
  // The workspace ("project") level itself, for the surfaces that genuinely
458
627
  // live there -- collaborators, IDP applications, published element
459
- // templates.
628
+ // templates. Under flag-on this is the Workspace's projects listing.
460
629
  async clickWorkspaceBreadcrumb() {
630
+ if (this.workspaceProjectsUrl) {
631
+ await this.page.goto(this.workspaceProjectsUrl, {
632
+ waitUntil: 'domcontentloaded',
633
+ });
634
+ await this.processApplicationPage.createProcessApplicationButton
635
+ .waitFor({ state: 'visible', timeout: 90000 })
636
+ .catch(() => { });
637
+ await this.clickMessageBanner();
638
+ return;
639
+ }
461
640
  await this.workspaceBreadcrumb.click({ timeout: 60000 });
462
641
  }
463
642
  // Creates, once per test, the process application that holds this test's
@@ -484,6 +663,16 @@ class ModelerHomePage {
484
663
  await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
485
664
  this.processApplicationUrl = this.page.url();
486
665
  this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
666
+ // Under workspaces-enabled, a freshly created process application has no
667
+ // cluster assigned to its development stage: Deploy (and Play's "Configure
668
+ // environment") render a "Clusters not configured for stages" warning and
669
+ // stay disabled until one is. Assign it once here, right after the
670
+ // container is created, so every later deploy/run/play/connector/form flow
671
+ // in this test finds a usable cluster instead of independently reproducing
672
+ // this connection step (or failing on the warning). ensureStageCluster()
673
+ // memoizes per test, so runProcessInstance()'s own call is a no-op after
674
+ // this.
675
+ await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
487
676
  }
488
677
  // Which control creates a process application depends on the hub version
489
678
  // 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
- // Since camunda/camunda-hub#25831 a process application is a first-class
10
- // container rather than a folder, and a workspace root ("project" in today's
11
- // UI) holds nothing but process applications and IDP applications. Every file
12
- // and folder this suite creates therefore has to live inside a process
13
- // application. This page object owns that container's surface only; entering
14
- // and leaving it is driven from ModelerHomePage so the ~150 existing spec call
15
- // sites keep working unchanged.
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
- // Empty workspace root: a plain primary button rendered next to the "Create
19
- // new" menu (hub's EmptyStateActionButtons).
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
- // Non-empty root: the item inside the "Create new" menu. Hub passes
22
- // hideProcessApplication at workspace level, so this is the fallback rather
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 process application' })
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
- this.creationDialog = page.getByRole('dialog', {
48
- name: 'Create a process application',
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.getByLabel('Name', { exact: true });
54
- this.submitButton = this.creationDialog.getByRole('button', {
55
- name: 'Create',
56
- exact: true,
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
- this.breadcrumb = page.locator('[data-test="breadcrumb-process-application-menu"]');
63
- // The Design System shell renames this region "Resources"; the Carbon shell
64
- // calls it "Folder explorer". Both hold the resource table and its toolbar,
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
- // Design System proof that we are on a process application page: the
71
- // Carbon-era breadcrumb hook is gone under that shell, the same way
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
- // The process application page renders more than one diagram-dropdown
75
- // (hub's own unit tests reach for it with getAllByTestId), so the dropdown
76
- // has to be resolved inside the folder explorer rather than page-wide.
77
- this.createNewDropdown = this.folderExplorer
78
- .locator('[data-test="diagram-dropdown"]')
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 breadcrumb proves we
91
- // are on the process application page. Three branches because the Design
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.or(this.folderExplorer).or(this.breadcrumb).first();
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
- // Hub creates a process application through a modal today. It used an inline
113
- // editable title before, and #27593 is still moving this surface, so the
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(`${processApplicationUrl}/connected-clusters`, {
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: 15000 })
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: /define stages|edit stages|configure/i })
212
+ .getByRole('button', { name: /^(define stages|edit stages|configure)$/i })
185
213
  .first();
186
214
  const canReopen = await reopen
187
- .waitFor({ state: 'visible', timeout: 10000 })
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 on /connected-clusters: it ' +
192
- 'did not open by itself and no "Define stages" / "Edit stages" / ' +
193
- '"Configure" control was found.');
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
- const processApplication = this.page
199
- .getByRole('row')
200
- .filter({ hasText: name })
201
- .getByTitle(name)
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)(processApplication).toBeVisible({ timeout: 90000 });
204
- await processApplication.click({ timeout: 60000 });
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1126",
3
+ "version": "0.0.1128",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",