@camunda/e2e-test-suite 0.0.1125 → 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
- 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,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;
@@ -69,6 +90,10 @@ class ModelerHomePage {
69
90
  .or(page.getByRole('button', {
70
91
  name: 'Create project',
71
92
  exact: true,
93
+ }))
94
+ .or(page.getByRole('button', {
95
+ name: 'Create workspace',
96
+ exact: true,
72
97
  }));
73
98
  this.projectNameInput = page.locator('[data-test="editable-input"]');
74
99
  // The rename kebab sits beside the breadcrumb in the app header. The
@@ -123,9 +148,8 @@ class ModelerHomePage {
123
148
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
124
149
  this.manageButton = page.getByRole('button', { name: 'Manage' });
125
150
  this.crossComponentProjectFolder = page
126
- .getByTitle(this.defaultFolderName, {
127
- exact: true,
128
- })
151
+ .getByRole('button', { name: `Open workspace ${this.defaultFolderName}` })
152
+ .or(page.getByTitle(this.defaultFolderName, { exact: true }))
129
153
  .first();
130
154
  // The Design System shell drops the breadcrumb-home hook and labels the
131
155
  // same crumb "My organisation".
@@ -150,6 +174,29 @@ class ModelerHomePage {
150
174
  this.messageBanner = page.locator('[data-test="close-top-banner"]');
151
175
  this.closeButton = page.getByRole('button', { name: 'Got it - Dismiss' });
152
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
+ });
153
200
  }
154
201
  async clickCreateNewProjectButton() {
155
202
  await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.createNewProjectButton, {
@@ -200,44 +247,83 @@ class ModelerHomePage {
200
247
  // the rest of the timeout. Transient upstream 50x is already absorbed by
201
248
  // goToModeler's gateway-error re-navigation before we get here.
202
249
  async waitForModelerReady(timeoutMs = 120000) {
203
- // .first(): when the project folder already exists on the shared cluster,
204
- // both the create-project button and the folder are present, so the .or()
205
- // resolves to multiple elements — waitFor() requires a single match.
206
- await this.createNewProjectButton
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)
207
260
  .or(this.crossComponentProjectFolder)
208
- .first()
209
- .waitFor({ state: 'visible', timeout: timeoutMs });
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] });
210
273
  }
211
274
  async createCrossComponentProjectFolder() {
212
- // After a fresh user login (e.g. lisa, bart) the Modeler pod may still be
213
- // initialising or returning transient 503s. Poll until the app shell banner
214
- // is visible before interacting with the project list.
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() {
215
293
  const maxAttempts = 3;
216
294
  const retryWaitMs = 5000;
217
295
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
218
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();
219
303
  await this.waitForModelerReady();
220
304
  await this.clickMessageBanner();
221
- if (await this.crossComponentProjectFolder.isVisible()) {
222
- console.log('Cross Component Project folder already exists. Clicking into it');
223
- await this.clickCrossComponentProjectFolder();
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();
224
311
  return;
225
312
  }
226
- await this.clickCreateNewProjectButton();
227
- await this.enterNewProjectName(this.defaultFolderName);
228
- await (0, test_1.expect)(this.page.getByRole('heading', { name: this.defaultFolderName })).toBeVisible({ timeout: 10000 });
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();
229
322
  return;
230
323
  }
231
324
  catch (error) {
232
325
  if (attempt < maxAttempts - 1) {
233
- console.log(`createCrossComponentProjectFolder attempt ${attempt + 1} failed. Re-authenticating and retrying...`);
234
- // A transient web-modeler 500 on /api/internal/login right after the
235
- // OAuth callback makes the SPA log the user out, destroying the
236
- // Keycloak SSO session. A bare page.reload() then lands back on the
237
- // empty login form and never re-authenticates, so every remaining
238
- // attempt waits out its timeout on the login page. Re-run the full
239
- // navigation+login instead so a recovered backend can complete the
240
- // login on the next attempt.
326
+ console.log(`ensureWorkspaceOpen attempt ${attempt + 1} failed. Re-authenticating and retrying...`);
241
327
  try {
242
328
  await new NavigationPage_1.NavigationPage(this.page).goToModeler();
243
329
  }
@@ -252,51 +338,99 @@ class ModelerHomePage {
252
338
  }
253
339
  }
254
340
  }
255
- async clickCrossComponentProjectFolder() {
256
- let attempts = 0;
257
- const maxAttempts = 3;
258
- const timeout = 60000;
259
- while (attempts < maxAttempts) {
260
- try {
261
- await this.clickMessageBanner();
262
- await (0, test_1.expect)(this.crossComponentProjectFolder).toBeVisible({ timeout });
263
- // The WhatsNew modal renders lazily after page load and intercepts clicks.
264
- // Explicitly wait for it to appear (up to 5s), dismiss it, then click.
265
- await this.closeButton
266
- .waitFor({ state: 'visible', timeout: 5000 })
267
- .then(() => this.closeButton.click())
268
- .catch(() => { });
269
- await this.modalCloseButton
270
- .waitFor({ state: 'visible', timeout: 2000 })
271
- .then(() => this.modalCloseButton.click())
272
- .catch(() => { });
273
- await this.crossComponentProjectFolder.click();
274
- await this.clickMessageBanner();
275
- return;
276
- }
277
- catch (error) {
278
- attempts++;
279
- if (attempts < maxAttempts) {
280
- console.log(`Attempt ${attempts} failed. Re-navigating to Modeler and retrying...`);
281
- // A transient web-modeler 500 on login right after the OAuth callback
282
- // logs the user out and leaves the page on the Keycloak login form. A
283
- // bare page.reload() then just reloads the login page and never
284
- // re-authenticates, so every remaining attempt waits out its timeout
285
- // on the login screen. Re-run the full navigation+login instead so a
286
- // recovered backend can complete the login on the next attempt.
287
- try {
288
- await new NavigationPage_1.NavigationPage(this.page).goToModeler();
289
- }
290
- catch (navError) {
291
- console.log(`Re-navigation to Modeler failed: ${navError}`);
292
- }
293
- await this.page.waitForLoadState('networkidle');
294
- }
295
- else {
296
- throw new Error('Failed to click crossComponentProjectFolder after 3 attempts.');
297
- }
298
- }
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;
299
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(() => { });
300
434
  }
301
435
  // Opens a resource where this test put it. Several specs leave Modeler
302
436
  // entirely (to Optimize, to Operate) and come back via
@@ -309,6 +443,15 @@ class ModelerHomePage {
309
443
  await process.click();
310
444
  }
311
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
+ }
312
455
  try {
313
456
  await this.homeBreadcrumb.click({ timeout: 60000 });
314
457
  }
@@ -452,8 +595,18 @@ class ModelerHomePage {
452
595
  }
453
596
  // The workspace ("project") level itself, for the surfaces that genuinely
454
597
  // live there -- collaborators, IDP applications, published element
455
- // templates.
598
+ // templates. Under flag-on this is the Workspace's projects listing.
456
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
+ }
457
610
  await this.workspaceBreadcrumb.click({ timeout: 60000 });
458
611
  }
459
612
  // Creates, once per test, the process application that holds this test's
@@ -480,6 +633,16 @@ class ModelerHomePage {
480
633
  await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
481
634
  this.processApplicationUrl = this.page.url();
482
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);
483
646
  }
484
647
  // Which control creates a process application depends on the hub version
485
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
- // 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.1125",
3
+ "version": "0.0.1127",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",