@camunda/e2e-test-suite 0.0.1009 → 0.0.1011

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.
@@ -1,6 +1,7 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class FormJsPage {
3
3
  private page;
4
+ private processApplicationPage;
4
5
  readonly aiFormGeneratorButton: Locator;
5
6
  readonly formRequestInput: Locator;
6
7
  readonly generateFormButton: Locator;
@@ -31,6 +32,7 @@ declare class FormJsPage {
31
32
  selectCluster(clusterName: string): Promise<void>;
32
33
  clickDeployButton(): Promise<void>;
33
34
  clickDeploySubButton(): Promise<void>;
35
+ selectOnlyThisResource(): Promise<void>;
34
36
  deployForm(clusterName: string): Promise<void>;
35
37
  }
36
38
  export { FormJsPage };
@@ -3,8 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FormJsPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
+ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
6
7
  class FormJsPage {
7
8
  page;
9
+ processApplicationPage;
8
10
  aiFormGeneratorButton;
9
11
  formRequestInput;
10
12
  generateFormButton;
@@ -23,6 +25,7 @@ class FormJsPage {
23
25
  deploySuccessNotification;
24
26
  constructor(page) {
25
27
  this.page = page;
28
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
26
29
  this.aiFormGeneratorButton = page.getByRole('button', {
27
30
  name: 'AI Form Generator',
28
31
  });
@@ -44,9 +47,12 @@ class FormJsPage {
44
47
  this.deployButton = page
45
48
  .locator('[data-test="action-bar"]')
46
49
  .getByRole('button', { name: 'Deploy', exact: true });
50
+ // Inside a process application hub titles this modal for the application
51
+ // rather than for the form, so 'Deploy form' never matches and the dialog
52
+ // reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
47
53
  this.dialog = page
48
54
  .locator('.cds--modal-container[role="dialog"]')
49
- .filter({ hasText: 'Deploy form' });
55
+ .filter({ hasText: 'Deploy' });
50
56
  this.deploySubButton = this.dialog.getByRole('button', {
51
57
  name: 'Deploy',
52
58
  exact: true,
@@ -120,6 +126,31 @@ class FormJsPage {
120
126
  async selectCluster(clusterName) {
121
127
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
122
128
  const healthCheckTimeout = 30000;
129
+ // Inside a process application the deploy dialog renders
130
+ // ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
131
+ // list: it shows only clusters assigned to one of the application's stages
132
+ // and auto-selects the first, so there is no label to click. Confirm the
133
+ // intended cluster is the one on offer instead.
134
+ //
135
+ // The unconfigured case is called out explicitly: without this the symptom
136
+ // is the cluster toggle never appearing, retried until the test times out,
137
+ // which says nothing about the actual cause.
138
+ const notConfigured = this.dialog.getByText('Clusters not configured for stages');
139
+ if (await notConfigured.isVisible().catch(() => false)) {
140
+ throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
141
+ 'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
142
+ }
143
+ const legacyClusterToggle = this.dialog.locator('label').filter({
144
+ hasText: new RegExp(`^${clusterName}`),
145
+ });
146
+ const isLegacyDialog = await legacyClusterToggle
147
+ .first()
148
+ .isVisible()
149
+ .catch(() => false);
150
+ if (!isLegacyDialog) {
151
+ await (0, test_1.expect)(this.dialog.getByText(clusterName, { exact: false }).first()).toBeVisible({ timeout: 30000 });
152
+ return;
153
+ }
123
154
  const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?HealthydevManage`);
124
155
  const cluster = this.dialog.locator(`[title="${clusterName}"]`);
125
156
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
@@ -138,12 +169,39 @@ class FormJsPage {
138
169
  async clickDeploySubButton() {
139
170
  await this.deploySubButton.click({ timeout: 60000 });
140
171
  }
172
+ // Since camunda/camunda-hub#25831 a form lives inside a process application,
173
+ // and Hub then scopes Deploy to the WHOLE application by default -- it calls
174
+ // deployProcessApplication, not deployFile, so the "Form deployed!" toast
175
+ // never fires (hub's own form-deploy-action.spec.sm.tsx asserts exactly that
176
+ // default). Narrowing the scope keeps this a form-deploy test instead of
177
+ // silently turning it into a process-application deploy. The combobox only
178
+ // exists inside a container, hence the boolean probe.
179
+ async selectOnlyThisResource() {
180
+ const scopeSelect = this.dialog.getByRole('combobox', {
181
+ name: 'Resources to deploy',
182
+ });
183
+ const hasScopeSelect = await scopeSelect
184
+ .waitFor({ state: 'visible', timeout: 10000 })
185
+ .then(() => true)
186
+ .catch(() => false);
187
+ if (!hasScopeSelect) {
188
+ return;
189
+ }
190
+ await scopeSelect.click();
191
+ await this.dialog
192
+ .getByRole('option', { name: 'Only this resource' })
193
+ .or(this.dialog.getByText('Only this resource', { exact: true }))
194
+ .first()
195
+ .click({ timeout: 30000 });
196
+ }
141
197
  async deployForm(clusterName) {
198
+ await this.processApplicationPage.ensureStageCluster(clusterName);
142
199
  await this.clickDeployButton();
143
200
  await (0, test_1.expect)(this.dialog).toBeVisible({
144
201
  timeout: 30000,
145
202
  });
146
203
  await this.selectCluster(clusterName);
204
+ await this.selectOnlyThisResource();
147
205
  await this.clickDeploySubButton();
148
206
  await (0, test_1.expect)(this.deploySuccessNotification).toBeVisible({ timeout: 30000 });
149
207
  }
@@ -1,6 +1,7 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerCreatePage {
3
3
  private page;
4
+ private processApplicationPage;
4
5
  readonly generalPanel: Locator;
5
6
  private deployDiagramDialog;
6
7
  readonly processIdInput: Locator;
@@ -4,9 +4,11 @@ exports.ModelerCreatePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
7
+ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
7
8
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
8
9
  class ModelerCreatePage {
9
10
  page;
11
+ processApplicationPage;
10
12
  generalPanel;
11
13
  deployDiagramDialog;
12
14
  processIdInput;
@@ -117,6 +119,7 @@ class ModelerCreatePage {
117
119
  diagramNameBreadcrumb;
118
120
  constructor(page) {
119
121
  this.page = page;
122
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
120
123
  this.generalPanel = page.locator('[data-group-id="group-general"]');
121
124
  this.processIdInput = page.getByLabel('ID', { exact: true });
122
125
  this.startEventElement = page.locator('.djs-hit').first();
@@ -269,8 +272,12 @@ class ModelerCreatePage {
269
272
  this.rbaEnabledDeployedDialog = page.getByRole('dialog', {
270
273
  name: 'Process successfully deployed',
271
274
  });
275
+ // Since camunda/camunda-hub#25831 a diagram deploys as part of its process
276
+ // application and hub titles the modal for the application, not for the
277
+ // single resource, so an exact 'Deploy diagram' name no longer matches.
278
+ // Match on the verb -- pages/SM-8.10 already leaves this unfiltered.
272
279
  this.deployDiagramDialog = page.getByRole('dialog', {
273
- name: 'Deploy diagram',
280
+ name: /^Deploy/,
274
281
  });
275
282
  this.appendGatewayButton = page.getByRole('button', {
276
283
  name: 'Append gateway',
@@ -604,6 +611,7 @@ class ModelerCreatePage {
604
611
  await this.deployMenuItem.click({ timeout: 60000 });
605
612
  }
606
613
  async deployDiagram(clusterName) {
614
+ await this.processApplicationPage.ensureStageCluster(clusterName);
607
615
  await this.clickDeployButtonWithRetry();
608
616
  await (0, test_1.expect)(this.deployDiagramDialog).toBeVisible({
609
617
  timeout: 30000,
@@ -620,6 +628,7 @@ class ModelerCreatePage {
620
628
  await this.deployAndRunSubButton.click({ timeout: 30000 });
621
629
  }
622
630
  async runProcessInstance(clusterName, variables = '', formVariables = {}, textToVisible = '', hasDeploymentPermission = true, businessId = '') {
631
+ await this.processApplicationPage.ensureStageCluster(clusterName);
623
632
  const maxRetries = 10;
624
633
  const retryDelayMs = 20000;
625
634
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
@@ -683,6 +692,31 @@ class ModelerCreatePage {
683
692
  async selectCluster(clusterName) {
684
693
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
685
694
  const healthCheckTimeout = 30000;
695
+ // Inside a process application the deploy dialog renders
696
+ // ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
697
+ // list: it shows only clusters assigned to one of the application's stages
698
+ // and auto-selects the first, so there is no label to click. Confirm the
699
+ // intended cluster is the one on offer instead.
700
+ //
701
+ // The unconfigured case is called out explicitly: without this the symptom
702
+ // is the cluster toggle never appearing, retried until the test times out,
703
+ // which says nothing about the actual cause.
704
+ const notConfigured = this.dialog.getByText('Clusters not configured for stages');
705
+ if (await notConfigured.isVisible().catch(() => false)) {
706
+ throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
707
+ 'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
708
+ }
709
+ const legacyClusterToggle = this.dialog.locator('label').filter({
710
+ hasText: new RegExp(`^${clusterName}`),
711
+ });
712
+ const isLegacyDialog = await legacyClusterToggle
713
+ .first()
714
+ .isVisible()
715
+ .catch(() => false);
716
+ if (!isLegacyDialog) {
717
+ await (0, test_1.expect)(this.dialog.getByText(clusterName, { exact: false }).first()).toBeVisible({ timeout: 30000 });
718
+ return;
719
+ }
686
720
  const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
687
721
  const clusterLabel = this.dialog
688
722
  .locator('label')
@@ -708,8 +742,45 @@ class ModelerCreatePage {
708
742
  async clickDiagramBreadcrumb() {
709
743
  await this.diagramBreadcrumb.click({ timeout: 10000 });
710
744
  }
745
+ // Opening the breadcrumb menu leaves the pointer on the breadcrumb button,
746
+ // and hub renders that button's truncated-name tooltip on top of the menu,
747
+ // where it swallows the click on "Rename" -- playwright reports
748
+ // `<div role="tooltip">…</div> intercepts pointer events`. Since
749
+ // camunda/camunda-hub#25831 the breadcrumb carries an extra
750
+ // process-application crumb, so the name truncates, and the tooltip renders,
751
+ // as the rule rather than the exception. Park the pointer away from the
752
+ // breadcrumb and let the tooltip unmount before clicking; reopen the menu if
753
+ // a previous attempt closed it.
711
754
  async clickEditDiagramNameButton() {
712
- await this.renameDiagramNameButton.click({ timeout: 10000 });
755
+ const openTooltip = this.page.locator('[role="tooltip"][aria-hidden="false"]');
756
+ await (0, test_1.expect)(async () => {
757
+ const menuOpen = await this.renameDiagramNameButton
758
+ .isVisible()
759
+ .catch(() => false);
760
+ if (!menuOpen) {
761
+ await this.clickDiagramBreadcrumb();
762
+ }
763
+ // Park the pointer so the breadcrumb's own tooltip gets a chance to
764
+ // unmount -- best effort, because it is not reliably hover-driven, and
765
+ // requiring it to go turned an interceptable click into a hard failure.
766
+ await this.page.mouse.move(0, 0);
767
+ await openTooltip
768
+ .first()
769
+ .waitFor({ state: 'hidden', timeout: 2000 })
770
+ .catch(() => false);
771
+ try {
772
+ await this.renameDiagramNameButton.click({ timeout: 10000 });
773
+ }
774
+ catch {
775
+ // The tooltip renders over the menu and swallows the click (playwright
776
+ // reports `<div role="tooltip">…</div> intercepts pointer events`).
777
+ // Dispatching on the element bypasses hit-testing, which no amount of
778
+ // waiting achieves when the tooltip never unmounts. Not a swallowed
779
+ // failure: the assertion below is what decides whether this worked.
780
+ await this.renameDiagramNameButton.dispatchEvent('click');
781
+ }
782
+ await (0, test_1.expect)(this.diagramNameInput).toBeVisible({ timeout: 10000 });
783
+ }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
713
784
  }
714
785
  // The rename can silently no-op: camunda-hub's breadcrumb name editor
715
786
  // (InlineEditable) resets its input back to the stored diagram name whenever
@@ -1288,6 +1359,16 @@ class ModelerCreatePage {
1288
1359
  await this.playTab.click();
1289
1360
  }
1290
1361
  async completePlayConfiguration(clusterName) {
1362
+ // Play is a tab on the diagram page, not its own URL, so the stage-cluster
1363
+ // detour's goto() lands back on the Modeler tab and every Play locator
1364
+ // below would then look for a panel that is no longer rendered. Re-enter
1365
+ // Play when the detour actually navigated. Binding cannot simply move
1366
+ // before switchToPlay(): this is the first point in the flow that knows
1367
+ // which cluster the test plays on.
1368
+ const reboundStageCluster = await this.processApplicationPage.ensureStageCluster(clusterName);
1369
+ if (reboundStageCluster) {
1370
+ await this.switchToPlay();
1371
+ }
1291
1372
  const timeout = 30000;
1292
1373
  // New flow (8.10+): "Setup environment" panel with three steps:
1293
1374
  // 1. Connect cluster → "Configure environment" modal → select cluster → Save
@@ -1311,19 +1392,37 @@ class ModelerCreatePage {
1311
1392
  name: /^Configure (test case|scenario)$/,
1312
1393
  }))
1313
1394
  .first();
1314
- // Wait up to 15s for EITHER the new-flow setup panel OR the legacy Continue
1315
- // button to appear. Using .or() avoids separate sequential timeouts that would
1316
- // both expire before the page finishes loading on a slow cluster.
1317
- const setupOrContinue = setupDeployButton.or(this.continueToPlayButton);
1318
- const panelAppeared = await setupOrContinue
1319
- .first()
1320
- .isVisible({ timeout: 15000 })
1321
- .catch(() => false);
1322
- if (!panelAppeared) {
1323
- // Neither appeared — Play is already configured and ready.
1395
+ // Play settles into exactly ONE of three states, and which one is not
1396
+ // knowable up front, so race them instead of timing out on each in turn:
1397
+ // the setup overlay, the legacy Continue button, or the configure-test
1398
+ // panel (meaning setup is already done for this diagram).
1399
+ //
1400
+ // Waiting only for the first two and reading "neither" as "already ready"
1401
+ // was wrong once the diagram moved inside a process application.
1402
+ // ConfigurationOverlay renders null until isDeploymentStateValidated, and
1403
+ // it sets that flag synchronously ONLY when processApplicationId is
1404
+ // undefined -- exactly what a project-root diagram used to give it. With
1405
+ // the diagram in a process application the id is defined, so the overlay
1406
+ // must first await checkExistingDeployment(). The old 15s probe raced that
1407
+ // async call, concluded "already ready" and returned while the overlay was
1408
+ // still about to appear. ConfigureTestPanel returns null for as long as the
1409
+ // overlay is up, so the Start button never existed and
1410
+ // clickStartInstanceButton burned its entire retry budget looking for it.
1411
+ const configurationOverlay = this.page.locator('[data-test="play-configuration-overlay"]');
1412
+ const configureTestPanel = this.page.getByTestId('configure-test-panel');
1413
+ await (0, test_1.expect)(configurationOverlay
1414
+ .or(this.continueToPlayButton)
1415
+ .or(configureTestPanel)
1416
+ .first()).toBeVisible({ timeout: 90000 });
1417
+ if (await configureTestPanel.isVisible()) {
1418
+ // Already past setup for this diagram -- nothing to drive.
1324
1419
  return;
1325
1420
  }
1326
- const isNewFlow = await setupDeployButton.isVisible().catch(() => false);
1421
+ // Keyed on the overlay itself, not on its Deploy button: when
1422
+ // checkExistingDeployment finds a deployment the overlay opens with the
1423
+ // deploy step already complete, so there is no Deploy button to see and
1424
+ // keying on it would fall through to the legacy Continue flow.
1425
+ const isNewFlow = await configurationOverlay.isVisible();
1327
1426
  if (isNewFlow) {
1328
1427
  // Step 1: connect a cluster only if none is connected yet.
1329
1428
  //
@@ -1386,13 +1485,33 @@ class ModelerCreatePage {
1386
1485
  // Wait for Deploy button to become enabled after cluster is saved.
1387
1486
  await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
1388
1487
  }
1389
- // Step 2: deploy — wait for enabled; button stays disabled until the cluster
1390
- // connection is confirmed by the backend after the Save in step 1.
1391
- await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
1392
- await setupDeployButton.click({ timeout });
1393
- await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1394
- // Step 3: configure scenario
1395
- await configureScenarioButton.click({ timeout });
1488
+ // Step 2: deploy, unless the overlay already opened with the
1489
+ // deploy step complete. checkExistingDeployment only runs when
1490
+ // processApplicationId is defined, which it now is, so re-entering
1491
+ // Play on an already-deployed process application legitimately
1492
+ // has nothing to deploy and renders no Deploy button at all.
1493
+ const alreadyDeployed = await configureScenarioButton
1494
+ .isVisible({ timeout: 3000 })
1495
+ .catch(() => false);
1496
+ if (!alreadyDeployed) {
1497
+ // Step 2: deploy — wait for enabled; button stays disabled until the cluster
1498
+ // connection is confirmed by the backend after the Save in step 1.
1499
+ await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
1500
+ await setupDeployButton.click({ timeout });
1501
+ await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1502
+ }
1503
+ // Step 3: leave the overlay, and confirm we actually left it. The
1504
+ // button only calls hideConfigurationOverlay(); the overlay's own
1505
+ // cluster-change effect can then fire resetDeployment and bring it
1506
+ // straight back, and ConfigureTestPanel stays null while it is up.
1507
+ // Assert the panel really rendered, and re-drive the click if the
1508
+ // overlay returned.
1509
+ await (0, test_1.expect)(async () => {
1510
+ if (await configurationOverlay.isVisible()) {
1511
+ await configureScenarioButton.click({ timeout });
1512
+ }
1513
+ await (0, test_1.expect)(configureTestPanel).toBeVisible({ timeout: 15000 });
1514
+ }).toPass({ timeout: 90000 });
1396
1515
  return;
1397
1516
  }
1398
1517
  // Legacy flow: select cluster via dialog, then click Continue.
@@ -2,6 +2,9 @@ import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerHomePage {
3
3
  private defaultFolderName;
4
4
  private page;
5
+ private processApplicationPage;
6
+ private processApplicationUrl?;
7
+ private processApplicationName?;
5
8
  readonly modelerPageBanner: Locator;
6
9
  readonly createNewProjectButton: Locator;
7
10
  readonly projectNameInput: Locator;
@@ -11,6 +14,7 @@ declare class ModelerHomePage {
11
14
  readonly bpmnTemplateOption: Locator;
12
15
  readonly formTemplateOption: Locator;
13
16
  readonly projectBreadcrumb: Locator;
17
+ readonly workspaceBreadcrumb: Locator;
14
18
  readonly openOrganizationsButton: Locator;
15
19
  readonly manageButton: Locator;
16
20
  readonly crossComponentProjectFolder: Locator;
@@ -35,11 +39,19 @@ declare class ModelerHomePage {
35
39
  getProjectNames(name: string): Promise<string[]>;
36
40
  clickProcessDiagram(name: string): Promise<void>;
37
41
  clickDiagramTypeDropdown(): Promise<void>;
42
+ clickWorkspaceDiagramTypeDropdown(): Promise<void>;
38
43
  clickBpmnTemplateOption(): Promise<void>;
39
44
  clickFormOption(): Promise<void>;
40
45
  enterFormName(name: string): Promise<void>;
41
46
  enterFormNameWithRetry(name: string): Promise<void>;
42
47
  clickProjectBreadcrumb(): Promise<void>;
48
+ clickWorkspaceBreadcrumb(): Promise<void>;
49
+ enterResourceContainer(name?: string): Promise<void>;
50
+ private clickCreateProcessApplication;
51
+ private ensureInResourceContainer;
52
+ returnToResourceContainer(): Promise<void>;
53
+ startBlankBpmnDiagram(): Promise<void>;
54
+ private becameVisible;
43
55
  clickHomeBreadcrumb(): Promise<void>;
44
56
  clickOpenOrganizationsButton(): Promise<void>;
45
57
  createForm(formName: string): Promise<void>;
@@ -4,9 +4,24 @@ exports.ModelerHomePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
7
+ const randomName_1 = require("../../utils/randomName");
8
+ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
7
9
  class ModelerHomePage {
8
10
  defaultFolderName = 'Cross Component Test Project';
9
11
  page;
12
+ // Since camunda/camunda-hub#25831 files and folders cannot sit at a
13
+ // workspace ("project" in today's UI) root -- only process applications and
14
+ // IDP applications can. The container is therefore entered lazily, at the
15
+ // point a resource is created, and remembered for the lifetime of this page
16
+ // object (one instance per test, see fixtures/8.10.ts). Specs that only need
17
+ // the workspace itself -- collaborators, IDP applications -- never trigger
18
+ // the hop.
19
+ processApplicationPage;
20
+ // Load-bearing: hub renders no process-application breadcrumb on a diagram
21
+ // page at container root, so the remembered URL -- not a breadcrumb click --
22
+ // is how we get back. Hub's own Playwright suite does the same.
23
+ processApplicationUrl;
24
+ processApplicationName;
10
25
  modelerPageBanner;
11
26
  createNewProjectButton;
12
27
  projectNameInput;
@@ -16,6 +31,7 @@ class ModelerHomePage {
16
31
  bpmnTemplateOption;
17
32
  formTemplateOption;
18
33
  projectBreadcrumb;
34
+ workspaceBreadcrumb;
19
35
  openOrganizationsButton;
20
36
  manageButton;
21
37
  crossComponentProjectFolder;
@@ -32,6 +48,7 @@ class ModelerHomePage {
32
48
  formNameBreadcrumb;
33
49
  constructor(page) {
34
50
  this.page = page;
51
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
35
52
  this.modelerPageBanner = page.getByRole('banner', {
36
53
  name: 'Camunda Modeler',
37
54
  });
@@ -52,13 +69,20 @@ class ModelerHomePage {
52
69
  .locator('[data-test="create-idp-application"]')
53
70
  .getByText('IDP Application');
54
71
  this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
55
- this.bpmnTemplateOption = page
56
- .locator('[data-test="create-bpmn-diagram"]')
57
- .getByText('BPMN Diagram');
58
- this.formTemplateOption = page
59
- .locator('[data-test="create-form"]')
60
- .getByText('Form');
72
+ // Matched on the test id alone: hub's design-system variant of the create
73
+ // menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
74
+ // day that variant ships.
75
+ this.bpmnTemplateOption = page.locator('[data-test="create-bpmn-diagram"]');
76
+ this.formTemplateOption = page.locator('[data-test="create-form"]');
61
77
  this.projectBreadcrumb = page.locator('[data-test="breadcrumb-project"]');
78
+ // Kept separate from projectBreadcrumb, which now means "the container my
79
+ // resources live in". When hub's workspaces terminology flag flips,
80
+ // breadcrumb-project comes to denote the process application, so the
81
+ // workspace level needs its own name from the start.
82
+ this.workspaceBreadcrumb = page
83
+ .locator('[data-test="breadcrumb-project"]')
84
+ .or(page.locator('[data-test="breadcrumb-workspace"]'))
85
+ .first();
62
86
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
63
87
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
64
88
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
@@ -157,17 +181,33 @@ class ModelerHomePage {
157
181
  .map((el) => el.textContent || '')
158
182
  .filter((text) => text !== ''));
159
183
  }
184
+ // Opens a resource where this test put it. Several specs leave Modeler
185
+ // entirely (to Operate, to Optimize) and come back via
186
+ // clickCrossComponentProjectFolder(), which lands at workspace root -- the
187
+ // resource is one level down, so re-enter the container first.
160
188
  async clickProcessDiagram(name) {
189
+ await this.ensureInResourceContainer();
161
190
  const process = this.rows.filter({ hasText: name }).getByTitle(name).first();
162
191
  await (0, test_1.expect)(process).toBeVisible({ timeout: 90000 });
163
192
  await process.click();
164
193
  }
194
+ // Opens the create menu inside this test's process application. The menu at
195
+ // workspace root no longer offers BPMN, Form or Upload -- those resources
196
+ // cannot live there -- so the container is entered first. Callers are
197
+ // unchanged, which is why the existing spec call sites need no edit.
165
198
  async clickDiagramTypeDropdown() {
166
- // Navigation-class wait: this is the first control on the project page, so
167
- // it is reached right after navigating into the project (or back via the
168
- // breadcrumb) and its render waits on the project's resource list. 15s was
169
- // short enough that a slow project load failed with
170
- // `locator('[data-test="diagram-dropdown"]') ... element(s) not found`.
199
+ await this.enterResourceContainer();
200
+ const dropdown = this.processApplicationPage.createNewDropdown;
201
+ // Navigation-class wait: the dropdown's render waits on the container's
202
+ // resource list, so a slow load otherwise fails with
203
+ // `element(s) not found`.
204
+ await (0, test_1.expect)(dropdown).toBeVisible({ timeout: 60000 });
205
+ await dropdown.click();
206
+ }
207
+ // Opens the create menu at workspace root, which is the only place process
208
+ // applications and IDP applications can be created. Everything else has to
209
+ // go through clickDiagramTypeDropdown().
210
+ async clickWorkspaceDiagramTypeDropdown() {
171
211
  await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
172
212
  timeout: 60000,
173
213
  });
@@ -214,8 +254,109 @@ class ModelerHomePage {
214
254
  `(attempt ${attempt}/${maxRetries}). Re-entering the name.`);
215
255
  }
216
256
  }
257
+ // Kept under its original name because it is what the existing call sites
258
+ // already mean: "go back to where my resources live". That is now the
259
+ // process application, not the workspace root.
217
260
  async clickProjectBreadcrumb() {
218
- await this.projectBreadcrumb.click({ timeout: 60000 });
261
+ await this.returnToResourceContainer();
262
+ }
263
+ // The workspace ("project") level itself, for the surfaces that genuinely
264
+ // live there -- collaborators, IDP applications, published element
265
+ // templates.
266
+ async clickWorkspaceBreadcrumb() {
267
+ await this.workspaceBreadcrumb.click({ timeout: 60000 });
268
+ }
269
+ // Creates, once per test, the process application that holds this test's
270
+ // resources, and leaves the browser inside it. Idempotent: called again while
271
+ // already in the container it is a no-op, so a spec that creates several
272
+ // resources in a row pays for the container once.
273
+ //
274
+ // One container per test rather than one shared with the seeded workspace:
275
+ // deploying is process-application-wide, so a shared container would let one
276
+ // spec's deploy pick up every other spec's half-modelled diagram.
277
+ async enterResourceContainer(name) {
278
+ if (this.processApplicationUrl) {
279
+ const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
280
+ if (!stillInside) {
281
+ await this.returnToResourceContainer();
282
+ }
283
+ return;
284
+ }
285
+ this.processApplicationName =
286
+ name ??
287
+ (await (0, randomName_1.randomNameAgregator)('Cross Component Process Application'));
288
+ console.log(`Creating process application ${this.processApplicationName} inside ${this.defaultFolderName}`);
289
+ await this.clickCreateProcessApplication();
290
+ await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
291
+ this.processApplicationUrl = this.page.url();
292
+ // Remember the container; its stage cluster is bound on the first deploy,
293
+ // by ProcessApplicationPage.ensureStageCluster, which -- unlike this point
294
+ // in the flow -- knows which cluster the test is going to deploy to.
295
+ this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
296
+ }
297
+ // Which control creates a process application depends on the hub version
298
+ // under test. Since camunda/camunda-hub#25831 it is a plain primary button on
299
+ // the workspace root, and the "Create new" menu explicitly hides the item
300
+ // (hideProcessApplication) because a process application is no longer just
301
+ // another resource. Before that change the menu item was the only way in.
302
+ // Race both entry points so neither version pays a full timeout, then use
303
+ // whichever one rendered.
304
+ async clickCreateProcessApplication() {
305
+ const button = this.processApplicationPage.createProcessApplicationButton;
306
+ const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
307
+ if (!rootLoaded) {
308
+ await this.page.reload();
309
+ }
310
+ // Short: the joint probe above already settled which branch rendered, and
311
+ // hub renders exactly one of them (EntityTable renders the toolbar's create
312
+ // menu OR the empty state, never both), so this only confirms which.
313
+ if (await this.becameVisible(button, 5000)) {
314
+ await this.processApplicationPage.clickCreateProcessApplicationButton();
315
+ return;
316
+ }
317
+ await this.clickWorkspaceDiagramTypeDropdown();
318
+ await this.processApplicationPage.clickCreateProcessApplicationOption();
319
+ }
320
+ // Re-enters this test's container if it already has one, without creating
321
+ // one: opening an existing resource must not have the side effect of
322
+ // creating a container for a spec that never asked for it.
323
+ async ensureInResourceContainer() {
324
+ if (!this.processApplicationUrl) {
325
+ return;
326
+ }
327
+ await this.enterResourceContainer();
328
+ }
329
+ // Navigates back to this test's process application by the remembered URL
330
+ // rather than by a breadcrumb click: hub renders no process-application
331
+ // breadcrumb on a diagram page at container root, and the breadcrumb that
332
+ // does exist on the container page doubles as its rename control. Hub's own
333
+ // Playwright suite navigates back the same way.
334
+ async returnToResourceContainer() {
335
+ if (!this.processApplicationUrl) {
336
+ await this.clickWorkspaceBreadcrumb();
337
+ return;
338
+ }
339
+ await this.page.goto(this.processApplicationUrl, {
340
+ waitUntil: 'domcontentloaded',
341
+ });
342
+ await this.processApplicationPage.waitForProcessApplication();
343
+ await this.dismissBlockingDialog();
344
+ }
345
+ // Single seam for "give me an empty BPMN diagram in this test's container".
346
+ // A process application always ships an auto-created main process; this adds
347
+ // a new diagram rather than adopting that one, so every existing modelling
348
+ // assertion keeps its current subject.
349
+ async startBlankBpmnDiagram() {
350
+ await this.clickDiagramTypeDropdown();
351
+ await this.clickBpmnTemplateOption();
352
+ }
353
+ // Boolean visibility probe. waitFor rather than isVisible, which resolves
354
+ // immediately and would report a false negative on a still-rendering page.
355
+ async becameVisible(locator, timeout) {
356
+ return locator
357
+ .waitFor({ state: 'visible', timeout })
358
+ .then(() => true)
359
+ .catch(() => false);
219
360
  }
220
361
  async clickHomeBreadcrumb() {
221
362
  try {
@@ -0,0 +1,29 @@
1
+ import { Locator, Page } from '@playwright/test';
2
+ declare class ProcessApplicationPage {
3
+ private page;
4
+ readonly createProcessApplicationButton: Locator;
5
+ readonly createProcessApplicationOption: Locator;
6
+ readonly creationDialog: Locator;
7
+ readonly nameInput: Locator;
8
+ readonly submitButton: Locator;
9
+ readonly cancelButton: Locator;
10
+ readonly breadcrumb: Locator;
11
+ readonly folderExplorer: Locator;
12
+ readonly createNewDropdown: Locator;
13
+ readonly inlineNameInput: Locator;
14
+ readonly defineStagesDialog: Locator;
15
+ readonly developmentClusterSelect: Locator;
16
+ readonly defineStagesSaveButton: Locator;
17
+ constructor(page: Page);
18
+ private pageReady;
19
+ waitForProcessApplication(timeout?: number): Promise<void>;
20
+ clickCreateProcessApplicationButton(): Promise<void>;
21
+ clickCreateProcessApplicationOption(): Promise<void>;
22
+ nameNewProcessApplication(name: string): Promise<void>;
23
+ defineDevelopmentStage(clusterName?: string): Promise<void>;
24
+ rememberResourceContainer(url: string): void;
25
+ ensureStageCluster(clusterName: string): Promise<boolean>;
26
+ private openStageDefinition;
27
+ openByName(name: string): Promise<void>;
28
+ }
29
+ export { ProcessApplicationPage };