@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.
@@ -0,0 +1,237 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProcessApplicationPage = void 0;
4
+ const test_1 = require("@playwright/test");
5
+ const submitModalWithRetry_1 = require("../../utils/assertionHelpers/submitModalWithRetry");
6
+ // Which process application holds the current test's resources, and which
7
+ // cluster its development stage currently points at. Keyed by the Playwright
8
+ // Page because the object that creates the container (ModelerHomePage) and the
9
+ // objects that later need a specific cluster inside it (ModelerCreatePage,
10
+ // FormJsPage) hold separate ProcessApplicationPage instances but share one page.
11
+ const resourceContainers = new WeakMap();
12
+ // Since camunda/camunda-hub#25831 a process application is a first-class
13
+ // container rather than a folder, and a workspace root ("project" in today's
14
+ // UI) holds nothing but process applications and IDP applications. Every file
15
+ // and folder this suite creates therefore has to live inside a process
16
+ // application. This page object owns that container's surface only; entering
17
+ // and leaving it is driven from ModelerHomePage so the ~150 existing spec call
18
+ // sites keep working unchanged.
19
+ class ProcessApplicationPage {
20
+ page;
21
+ // Empty workspace root: a plain primary button rendered next to the "Create
22
+ // new" menu (hub's EmptyStateActionButtons).
23
+ createProcessApplicationButton;
24
+ // Non-empty root: the item inside the "Create new" menu. Hub passes
25
+ // hideProcessApplication at workspace level, so this is the fallback rather
26
+ // than the primary path -- its own unit test asserts the item is absent
27
+ // there. Kept because the Hub PR is still moving this surface.
28
+ createProcessApplicationOption;
29
+ creationDialog;
30
+ nameInput;
31
+ submitButton;
32
+ cancelButton;
33
+ breadcrumb;
34
+ folderExplorer;
35
+ createNewDropdown;
36
+ inlineNameInput;
37
+ // "Define stages" dialog, reached from the process application's "Connected
38
+ // clusters" card (or its /connected-clusters deep link). Assigning a cluster
39
+ // to a stage here is what makes it deployable: DeployModal renders
40
+ // ProcessAppClusterSelection for a process application, and that filters the
41
+ // cluster list through getProcessAppClustersByStage -- so a process
42
+ // application with no stage assigned offers no cluster at all.
43
+ defineStagesDialog;
44
+ developmentClusterSelect;
45
+ defineStagesSaveButton;
46
+ constructor(page) {
47
+ this.page = page;
48
+ // .first() defensively only: hub renders exactly one of these today, in the
49
+ // workspace-root empty state. A future second placement should not turn
50
+ // every PA-creating test into a strict-mode violation.
51
+ this.createProcessApplicationButton = page
52
+ .getByRole('button', { name: 'Create process application' })
53
+ .first();
54
+ this.createProcessApplicationOption = page.locator('[data-test="create-process-application"]');
55
+ this.creationDialog = page.getByRole('dialog', {
56
+ name: 'Create a process application',
57
+ });
58
+ // Deliberately scoped to the dialog: ModelerCreatePage owns an unscoped
59
+ // getByLabel('Name', {exact: true}) for the properties panel, which would
60
+ // resolve to two elements while the creation modal is open.
61
+ this.nameInput = this.creationDialog.getByLabel('Name', { exact: true });
62
+ this.submitButton = this.creationDialog.getByRole('button', {
63
+ name: 'Create',
64
+ exact: true,
65
+ });
66
+ this.cancelButton = this.creationDialog.getByRole('button', {
67
+ name: 'Cancel',
68
+ exact: true,
69
+ });
70
+ this.breadcrumb = page.locator('[data-test="breadcrumb-process-application-menu"]');
71
+ this.folderExplorer = page.getByRole('region', { name: 'Folder explorer' });
72
+ // The process application page renders more than one diagram-dropdown
73
+ // (hub's own unit tests reach for it with getAllByTestId), so the dropdown
74
+ // has to be resolved inside the folder explorer rather than page-wide.
75
+ this.createNewDropdown = this.folderExplorer
76
+ .locator('[data-test="diagram-dropdown"]')
77
+ .first();
78
+ this.inlineNameInput = page.locator('[data-test="editable-input"]');
79
+ this.defineStagesDialog = page
80
+ .getByRole('dialog')
81
+ .filter({ hasText: 'Define stages' });
82
+ // Carbon Dropdown / design-system Select both keep this id.
83
+ this.developmentClusterSelect = this.defineStagesDialog.locator('#dropdown-development-cluster');
84
+ this.defineStagesSaveButton = this.defineStagesDialog.getByRole('button', {
85
+ name: 'Save',
86
+ exact: true,
87
+ });
88
+ }
89
+ // Either the folder explorer or the breadcrumb proves we are on the process
90
+ // application page; .first() because both are present once it has rendered.
91
+ pageReady() {
92
+ return this.folderExplorer.or(this.breadcrumb).first();
93
+ }
94
+ async waitForProcessApplication(timeout = 90000) {
95
+ await (0, test_1.expect)(this.pageReady()).toBeVisible({ timeout });
96
+ }
97
+ async clickCreateProcessApplicationButton() {
98
+ await (0, test_1.expect)(this.createProcessApplicationButton).toBeVisible({
99
+ timeout: 60000,
100
+ });
101
+ await this.createProcessApplicationButton.click({ timeout: 60000 });
102
+ }
103
+ async clickCreateProcessApplicationOption() {
104
+ await (0, test_1.expect)(this.createProcessApplicationOption).toBeVisible({
105
+ timeout: 60000,
106
+ });
107
+ await this.createProcessApplicationOption.click({ timeout: 60000 });
108
+ }
109
+ // Hub creates a process application through a modal today. It used an inline
110
+ // editable title before, and #27593 is still moving this surface, so the
111
+ // inline path is kept as a fallback — this is the one method that has to
112
+ // change if naming moves back inline.
113
+ async nameNewProcessApplication(name) {
114
+ const isModal = await this.creationDialog
115
+ .waitFor({ state: 'visible', timeout: 15000 })
116
+ .then(() => true)
117
+ .catch(() => false);
118
+ if (isModal) {
119
+ await this.nameInput.click({ timeout: 30000 });
120
+ await this.nameInput.fill(name);
121
+ await (0, test_1.expect)(this.nameInput).toHaveValue(name, { timeout: 10000 });
122
+ await (0, submitModalWithRetry_1.submitModalWithRetry)(this.submitButton, this.creationDialog);
123
+ await this.waitForProcessApplication();
124
+ return;
125
+ }
126
+ await (0, test_1.expect)(this.inlineNameInput).toBeVisible({ timeout: 30000 });
127
+ await this.inlineNameInput.click({ timeout: 60000 });
128
+ await this.inlineNameInput.fill(name);
129
+ await this.inlineNameInput.press('Enter');
130
+ await this.waitForProcessApplication();
131
+ }
132
+ // Assigns a cluster to this process application's development stage.
133
+ //
134
+ // Deploying from inside a process application goes through
135
+ // ProcessAppClusterSelection, which only lists clusters the application has
136
+ // assigned to a stage, and does not auto-select anything when that list is
137
+ // empty. A freshly created process application has no stages, so the deploy
138
+ // dialog shows "Clusters not configured for stages" and offers nothing to
139
+ // pick -- which is why the old per-deploy cluster toggle was nowhere to be
140
+ // found. Assign the stage once, at creation, and the deploy dialog then
141
+ // auto-selects it.
142
+ //
143
+ // clusterName is matched as a prefix because the dropdown renders each
144
+ // cluster as "<name> <generation>". Without it, the first real cluster is
145
+ // taken -- callers that care assert the name in the deploy dialog anyway.
146
+ async defineDevelopmentStage(clusterName) {
147
+ await (0, test_1.expect)(this.defineStagesDialog).toBeVisible({ timeout: 60000 });
148
+ await this.developmentClusterSelect.click({ timeout: 30000 });
149
+ // Escape before interpolating: a cluster name can carry regex
150
+ // metacharacters -- "Test Cluster (v2)" is the common case -- which would
151
+ // otherwise mis-match or throw. Same treatment as
152
+ // utils/modelerCluster.selectModelerCluster.
153
+ const escapedClusterName = clusterName?.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
154
+ const option = clusterName
155
+ ? this.defineStagesDialog
156
+ .getByRole('option', { name: new RegExp(`^${escapedClusterName}`) })
157
+ .first()
158
+ : this.defineStagesDialog
159
+ .getByRole('option')
160
+ .filter({ hasNotText: 'Select a development cluster' })
161
+ .first();
162
+ await option.click({ timeout: 30000 });
163
+ await this.defineStagesSaveButton.click({ timeout: 30000 });
164
+ await (0, test_1.expect)(this.defineStagesDialog).toBeHidden({ timeout: 60000 });
165
+ }
166
+ rememberResourceContainer(url) {
167
+ resourceContainers.set(this.page, { url });
168
+ }
169
+ // A process application deploys to the clusters bound to its stages, and
170
+ // nothing else: inside a container DeployModal renders
171
+ // ProcessAppClusterSelection, which lists only those and auto-selects the
172
+ // first. Which cluster a test wants is known only once a deploy flow names
173
+ // it, so the binding happens here, on demand -- leave the diagram, bind the
174
+ // cluster to the development stage, come back. Repeat calls for the same
175
+ // cluster cost nothing, and a test deploying to a second cluster rebinds.
176
+ // Returns whether it navigated: the caller may have been on a view that a
177
+ // reload does not restore -- Play is a tab click, not a URL -- and has to
178
+ // re-enter it.
179
+ async ensureStageCluster(clusterName) {
180
+ const container = resourceContainers.get(this.page);
181
+ if (!container || container.developmentCluster === clusterName) {
182
+ return false;
183
+ }
184
+ const returnTo = this.page.url();
185
+ const paUrl = container.url.replace(/[?#].*$/, '').replace(/\/$/, '');
186
+ await this.page.goto(`${paUrl}/connected-clusters`, {
187
+ waitUntil: 'domcontentloaded',
188
+ });
189
+ await this.openStageDefinition();
190
+ await this.defineDevelopmentStage(clusterName);
191
+ container.developmentCluster = clusterName;
192
+ await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
193
+ return true;
194
+ }
195
+ // The /connected-clusters deep link opens the Define stages dialog by itself
196
+ // while the application has no stage bound. Once one is bound -- a test that
197
+ // deploys to a second cluster -- it has to be reopened from the page, so try
198
+ // the affordances hub offers before failing with exactly what was looked for.
199
+ // Worth the noise: an unbound cluster otherwise surfaces as a deploy dialog
200
+ // that silently offers nothing, which says nothing about its own cause.
201
+ async openStageDefinition() {
202
+ const opened = await this.defineStagesDialog
203
+ .waitFor({ state: 'visible', timeout: 15000 })
204
+ .then(() => true)
205
+ .catch(() => false);
206
+ if (opened) {
207
+ return;
208
+ }
209
+ // Page-scoped .first(): hub renders one such control on this page, and a
210
+ // second placement must not turn every rebinding test into a strict-mode
211
+ // violation.
212
+ const reopen = this.page
213
+ .getByRole('button', { name: /define stages|edit stages|configure/i })
214
+ .first();
215
+ const canReopen = await reopen
216
+ .waitFor({ state: 'visible', timeout: 10000 })
217
+ .then(() => true)
218
+ .catch(() => false);
219
+ if (!canReopen) {
220
+ throw new Error('Could not open the "Define stages" dialog on /connected-clusters: it ' +
221
+ 'did not open by itself and no "Define stages" / "Edit stages" / ' +
222
+ '"Configure" control was found.');
223
+ }
224
+ await reopen.click({ timeout: 30000 });
225
+ }
226
+ async openByName(name) {
227
+ const processApplication = this.page
228
+ .getByRole('row')
229
+ .filter({ hasText: name })
230
+ .getByTitle(name)
231
+ .first();
232
+ await (0, test_1.expect)(processApplication).toBeVisible({ timeout: 90000 });
233
+ await processApplication.click({ timeout: 60000 });
234
+ await this.waitForProcessApplication();
235
+ }
236
+ }
237
+ exports.ProcessApplicationPage = ProcessApplicationPage;
@@ -653,6 +653,14 @@ async function modelAndRunConnectorsTimerEventDiagram(modelerCreatePage, modeler
653
653
  timeout: 60000,
654
654
  });
655
655
  await connectorMarketplacePage.downloadConnectorToProject();
656
+ // Both lookups are container-relative: clickProjectBreadcrumb() returns to
657
+ // this test's process application, which is where the uploaded diagram
658
+ // lives. The downloaded element template is the open question -- hub
659
+ // publishes element templates to the workspace, but the marketplace download
660
+ // is initiated from inside the container, so it should land there. If a
661
+ // trace shows the template at workspace root instead, the first of these two
662
+ // calls becomes clickWorkspaceBreadcrumb() followed by
663
+ // returnToResourceContainer() before the diagram is reopened.
656
664
  await modelerHomePage.clickProjectBreadcrumb();
657
665
  await modelerHomePage.clickProcessDiagram('REST Outbound Connector');
658
666
  await connectorTemplatePage.publishConnector();
@@ -27,6 +27,7 @@ declare class FormJsPage {
27
27
  filldocumentReferenceInput(key: string): Promise<void>;
28
28
  clickDeployFormButton(): Promise<void>;
29
29
  clickDeployModalSubmitButton(): Promise<void>;
30
+ selectOnlyThisResource(): Promise<void>;
30
31
  deployForm(): Promise<void>;
31
32
  }
32
33
  export { FormJsPage };
@@ -85,10 +85,41 @@ class FormJsPage {
85
85
  async clickDeployModalSubmitButton() {
86
86
  await this.deployModalSubmitButton.click({ timeout: 60000 });
87
87
  }
88
+ // Since camunda/camunda-hub#25831 a form lives inside a process application,
89
+ // and Hub then scopes Deploy to the WHOLE application by default -- it calls
90
+ // deployProcessApplication, not deployFile, so the "Form deployed!" toast
91
+ // never fires (hub's own form-deploy-action.spec.sm.tsx asserts exactly that
92
+ // default). Narrowing the scope keeps this a form-deploy test instead of
93
+ // silently turning it into a process-application deploy. The combobox only
94
+ // exists inside a container, hence the boolean probe.
95
+ async selectOnlyThisResource() {
96
+ const scopeSelect = this.deployDialog.getByRole('combobox', {
97
+ name: 'Resources to deploy',
98
+ });
99
+ const hasScopeSelect = await scopeSelect
100
+ .waitFor({ state: 'visible', timeout: 10000 })
101
+ .then(() => true)
102
+ .catch(() => false);
103
+ if (!hasScopeSelect) {
104
+ return;
105
+ }
106
+ await scopeSelect.click();
107
+ // The scope select is a Radix UI Select: its listbox is portalled to
108
+ // document.body, OUTSIDE the dialog element, so a dialog-scoped option
109
+ // query never resolves even while the option is visible on screen. Query
110
+ // the portalled option at page scope instead; the trigger above stays
111
+ // dialog-scoped because it renders inside the dialog.
112
+ await this.page
113
+ .getByRole('option', { name: 'Only this resource' })
114
+ .or(this.page.getByText('Only this resource', { exact: true }))
115
+ .first()
116
+ .click({ timeout: 30000 });
117
+ }
88
118
  async deployForm() {
89
119
  await this.clickDeployFormButton();
90
120
  await (0, test_1.expect)(this.deployDialog).toBeVisible({ timeout: 30000 });
91
121
  await (0, modelerCluster_1.selectModelerCluster)(this.deployDialog);
122
+ await this.selectOnlyThisResource();
92
123
  await this.clickDeployModalSubmitButton();
93
124
  await (0, test_1.expect)(this.deploySuccessNotification).toBeVisible({ timeout: 30000 });
94
125
  }
@@ -1020,19 +1020,37 @@ class ModelerCreatePage {
1020
1020
  name: /^Configure (test case|scenario)$/,
1021
1021
  }))
1022
1022
  .first();
1023
- // Wait up to 15s for EITHER the new-flow setup panel OR the legacy Continue
1024
- // button to appear. Using .or() avoids separate sequential timeouts that would
1025
- // both expire before the page finishes loading on a slow cluster.
1026
- const setupOrContinue = setupDeployButton.or(this.continueToPlayButton);
1027
- const panelAppeared = await setupOrContinue
1028
- .first()
1029
- .isVisible({ timeout: 15000 })
1030
- .catch(() => false);
1031
- if (!panelAppeared) {
1032
- // Neither appeared — Play is already configured and ready.
1023
+ // Play settles into exactly ONE of three states, and which one is not
1024
+ // knowable up front, so race them instead of timing out on each in turn:
1025
+ // the setup overlay, the legacy Continue button, or the configure-test
1026
+ // panel (meaning setup is already done for this diagram).
1027
+ //
1028
+ // Waiting only for the first two and reading "neither" as "already ready"
1029
+ // was wrong once the diagram moved inside a process application.
1030
+ // ConfigurationOverlay renders null until isDeploymentStateValidated, and
1031
+ // it sets that flag synchronously ONLY when processApplicationId is
1032
+ // undefined -- exactly what a project-root diagram used to give it. With
1033
+ // the diagram in a process application the id is defined, so the overlay
1034
+ // must first await checkExistingDeployment(). The old 15s probe raced that
1035
+ // async call, concluded "already ready" and returned while the overlay was
1036
+ // still about to appear. ConfigureTestPanel returns null for as long as the
1037
+ // overlay is up, so the Start button never existed and
1038
+ // clickStartInstanceButton burned its entire retry budget looking for it.
1039
+ const configurationOverlay = this.page.locator('[data-test="play-configuration-overlay"]');
1040
+ const configureTestPanel = this.page.getByTestId('configure-test-panel');
1041
+ await (0, test_1.expect)(configurationOverlay
1042
+ .or(this.continueToPlayButton)
1043
+ .or(configureTestPanel)
1044
+ .first()).toBeVisible({ timeout: 90000 });
1045
+ if (await configureTestPanel.isVisible()) {
1046
+ // Already past setup for this diagram -- nothing to drive.
1033
1047
  return;
1034
1048
  }
1035
- const isNewFlow = await setupDeployButton.isVisible().catch(() => false);
1049
+ // Keyed on the overlay itself, not on its Deploy button: when
1050
+ // checkExistingDeployment finds a deployment the overlay opens with the
1051
+ // deploy step already complete, so there is no Deploy button to see and
1052
+ // keying on it would fall through to the legacy Continue flow.
1053
+ const isNewFlow = await configurationOverlay.isVisible();
1036
1054
  if (isNewFlow) {
1037
1055
  // Step 1: connect cluster if not yet connected (modal with cluster dropdown)
1038
1056
  const connectClusterButton = this.page.getByRole('button', {
@@ -1052,11 +1070,31 @@ class ModelerCreatePage {
1052
1070
  .getByRole('button', { name: 'Save' })
1053
1071
  .click({ timeout });
1054
1072
  }
1055
- // Step 2: deploy
1056
- await setupDeployButton.click({ timeout });
1057
- await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1058
- // Step 3: configure scenario
1059
- await configureScenarioButton.click({ timeout });
1073
+ // Step 2: deploy, unless the overlay already opened with the
1074
+ // deploy step complete. checkExistingDeployment only runs when
1075
+ // processApplicationId is defined, which it now is, so re-entering
1076
+ // Play on an already-deployed process application legitimately
1077
+ // has nothing to deploy and renders no Deploy button at all.
1078
+ const alreadyDeployed = await configureScenarioButton
1079
+ .isVisible({ timeout: 3000 })
1080
+ .catch(() => false);
1081
+ if (!alreadyDeployed) {
1082
+ // Step 2: deploy
1083
+ await setupDeployButton.click({ timeout });
1084
+ await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1085
+ }
1086
+ // Step 3: leave the overlay, and confirm we actually left it. The
1087
+ // button only calls hideConfigurationOverlay(); the overlay's own
1088
+ // cluster-change effect can then fire resetDeployment and bring it
1089
+ // straight back, and ConfigureTestPanel stays null while it is up.
1090
+ // Assert the panel really rendered, and re-drive the click if the
1091
+ // overlay returned.
1092
+ await (0, test_1.expect)(async () => {
1093
+ if (await configurationOverlay.isVisible()) {
1094
+ await configureScenarioButton.click({ timeout });
1095
+ }
1096
+ await (0, test_1.expect)(configureTestPanel).toBeVisible({ timeout: 15000 });
1097
+ }).toPass({ timeout: 90000 });
1060
1098
  return;
1061
1099
  }
1062
1100
  // Legacy flow: "Continue" button → "Play environment is ready" confirmation.
@@ -2,6 +2,9 @@ import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerHomePage {
3
3
  private page;
4
4
  private defaultFolderName;
5
+ private processApplicationPage;
6
+ private processApplicationUrl?;
7
+ private processApplicationName?;
5
8
  readonly modelerPageBanner: Locator;
6
9
  readonly createNewProjectButton: Locator;
7
10
  readonly projectNameInput: Locator;
@@ -14,6 +17,7 @@ declare class ModelerHomePage {
14
17
  readonly bpmnTemplateOption: Locator;
15
18
  readonly formTemplateOption: Locator;
16
19
  readonly projectBreadcrumb: Locator;
20
+ readonly workspaceBreadcrumb: Locator;
17
21
  readonly openOrganizationsButton: Locator;
18
22
  readonly manageButton: Locator;
19
23
  readonly crossComponentProjectFolder: Locator;
@@ -43,10 +47,17 @@ declare class ModelerHomePage {
43
47
  enterFormName(name: string): Promise<void>;
44
48
  private renameViaMoreActions;
45
49
  clickProjectBreadcrumb(): Promise<void>;
50
+ clickWorkspaceBreadcrumb(): Promise<void>;
51
+ enterResourceContainer(name?: string): Promise<void>;
52
+ private clickCreateProcessApplication;
53
+ private ensureInResourceContainer;
54
+ returnToResourceContainer(): Promise<void>;
55
+ startBlankBpmnDiagram(): Promise<void>;
46
56
  clickOpenOrganizationsButton(): Promise<void>;
47
57
  clickManageButton(): Promise<void>;
48
58
  clickUploadFilesButton(): Promise<void>;
49
59
  clickMessageBanner(): Promise<void>;
60
+ private becameVisible;
50
61
  private overlayBecameVisible;
51
62
  }
52
63
  export { ModelerHomePage };
@@ -3,10 +3,24 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModelerHomePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  const expectLocatorWithRetry_1 = require("../../utils/assertionHelpers/expectLocatorWithRetry");
6
+ const randomName_1 = require("../../utils/randomName");
6
7
  const NavigationPage_1 = require("./NavigationPage");
8
+ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
7
9
  class ModelerHomePage {
8
10
  page;
9
11
  defaultFolderName = 'Cross Component Test Project';
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/SM-8.10.ts). Specs that only
17
+ // need the workspace itself never trigger the hop.
18
+ processApplicationPage;
19
+ // Load-bearing: hub renders no process-application breadcrumb on a diagram
20
+ // page at container root, so the remembered URL -- not a breadcrumb click --
21
+ // is how we get back. Hub's own Playwright suite does the same.
22
+ processApplicationUrl;
23
+ processApplicationName;
10
24
  modelerPageBanner;
11
25
  createNewProjectButton;
12
26
  projectNameInput;
@@ -19,6 +33,7 @@ class ModelerHomePage {
19
33
  bpmnTemplateOption;
20
34
  formTemplateOption;
21
35
  projectBreadcrumb;
36
+ workspaceBreadcrumb;
22
37
  openOrganizationsButton;
23
38
  manageButton;
24
39
  crossComponentProjectFolder;
@@ -35,6 +50,7 @@ class ModelerHomePage {
35
50
  modalCloseButton;
36
51
  constructor(page) {
37
52
  this.page = page;
53
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
38
54
  // Matches both Modeler shells -- see NavigationPage.modelerPageBanner for
39
55
  // why the Design System branch excludes the /login href.
40
56
  this.modelerPageBanner = page
@@ -58,12 +74,11 @@ class ModelerHomePage {
58
74
  name: 'Choose BPMN template',
59
75
  });
60
76
  this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
61
- this.bpmnTemplateOption = page
62
- .locator('[data-test="create-bpmn-diagram"]')
63
- .getByText('BPMN Diagram');
64
- this.formTemplateOption = page
65
- .locator('[data-test="create-form"]')
66
- .getByText('Form');
77
+ // Matched on the test id alone: hub's design-system variant of the create
78
+ // menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
79
+ // day that variant ships.
80
+ this.bpmnTemplateOption = page.locator('[data-test="create-bpmn-diagram"]');
81
+ this.formTemplateOption = page.locator('[data-test="create-form"]');
67
82
  // The Design System shell drops the breadcrumb-project hook. Its context
68
83
  // breadcrumb is org / project / entity, so the project crumb is the second
69
84
  // item -- scoped to the breadcrumb nav, never a bare nth() on the page.
@@ -75,6 +90,21 @@ class ModelerHomePage {
75
90
  .getByRole('button')
76
91
  .first())
77
92
  .first();
93
+ // Kept separate from projectBreadcrumb, which now means "the container my
94
+ // resources live in". When hub's workspaces terminology flag flips,
95
+ // breadcrumb-project comes to denote the process application, so the
96
+ // workspace level needs its own name from the start. Carries the same
97
+ // Design System fallback as projectBreadcrumb: under that shell neither
98
+ // test-id hook exists, and the workspace level is the project crumb.
99
+ this.workspaceBreadcrumb = page
100
+ .locator('[data-test="breadcrumb-project"]')
101
+ .or(page.locator('[data-test="breadcrumb-workspace"]'))
102
+ .or(page
103
+ .locator('nav[aria-label="Context navigation"] [data-slot="nav-breadcrumb-item"]')
104
+ .nth(1)
105
+ .getByRole('button')
106
+ .first())
107
+ .first();
78
108
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
79
109
  this.manageButton = page.getByRole('button', { name: 'Manage' });
80
110
  this.crossComponentProjectFolder = page
@@ -248,7 +278,12 @@ class ModelerHomePage {
248
278
  }
249
279
  }
250
280
  }
281
+ // Opens a resource where this test put it. Several specs leave Modeler
282
+ // entirely (to Optimize, to Operate) and come back via
283
+ // clickCrossComponentProjectFolder(), which lands at workspace root -- the
284
+ // resource is one level down, so re-enter the container first.
251
285
  async clickProcessDiagram(name) {
286
+ await this.ensureInResourceContainer();
252
287
  const process = this.rows.filter({ hasText: name }).getByTitle(name).first();
253
288
  await (0, test_1.expect)(process).toBeVisible({ timeout: 90000 });
254
289
  await process.click();
@@ -267,13 +302,19 @@ class ModelerHomePage {
267
302
  async clickChooseBpmnTemplateButton() {
268
303
  await this.chooseBpmnTemplateButton.click();
269
304
  }
305
+ // Opens the create menu inside this test's process application. The menu at
306
+ // workspace root no longer offers BPMN, Form or Upload -- those resources
307
+ // cannot live there -- so the container is entered first. Callers are
308
+ // unchanged, which is why the ~150 existing spec call sites need no edit.
270
309
  async clickDiagramTypeDropdown() {
271
- await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, this.diagramTypeDropdown, {
310
+ await this.enterResourceContainer();
311
+ const dropdown = this.processApplicationPage.createNewDropdown;
312
+ await (0, expectLocatorWithRetry_1.expectLocatorWithRetry)(this.page, dropdown, {
272
313
  postAction: async () => {
273
314
  await this.page.reload();
274
315
  },
275
316
  });
276
- await this.diagramTypeDropdown.click();
317
+ await dropdown.click();
277
318
  }
278
319
  async clickBpmnTemplateOption() {
279
320
  await (0, test_1.expect)(this.bpmnTemplateOption).toBeVisible({ timeout: 60000 });
@@ -316,8 +357,97 @@ class ModelerHomePage {
316
357
  await this.renameSubmitButton.click();
317
358
  await (0, test_1.expect)(this.renameInput).toBeHidden({ timeout: 10000 });
318
359
  }
360
+ // Kept under its original name because it is what ~10 call sites already
361
+ // mean: "go back to where my resources live". That is now the process
362
+ // application, not the workspace root.
319
363
  async clickProjectBreadcrumb() {
320
- await this.projectBreadcrumb.click({ timeout: 60000 });
364
+ await this.returnToResourceContainer();
365
+ }
366
+ // The workspace ("project") level itself, for the surfaces that genuinely
367
+ // live there -- collaborators, IDP applications, published element
368
+ // templates.
369
+ async clickWorkspaceBreadcrumb() {
370
+ await this.workspaceBreadcrumb.click({ timeout: 60000 });
371
+ }
372
+ // Creates, once per test, the process application that holds this test's
373
+ // resources, and leaves the browser inside it. Idempotent: called again while
374
+ // already in the container it is a no-op, so a spec that creates several
375
+ // resources in a row pays for the container once.
376
+ //
377
+ // One container per test rather than one shared with the seeded workspace:
378
+ // deploying is process-application-wide, so a shared container would let one
379
+ // spec's deploy pick up every other spec's half-modelled diagram.
380
+ async enterResourceContainer(name) {
381
+ if (this.processApplicationUrl) {
382
+ const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
383
+ if (!stillInside) {
384
+ await this.returnToResourceContainer();
385
+ }
386
+ return;
387
+ }
388
+ this.processApplicationName =
389
+ name ??
390
+ (await (0, randomName_1.randomNameAgregator)('Cross Component Process Application'));
391
+ console.log(`Creating process application ${this.processApplicationName} inside ${this.defaultFolderName}`);
392
+ await this.clickCreateProcessApplication();
393
+ await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
394
+ this.processApplicationUrl = this.page.url();
395
+ }
396
+ // Which control creates a process application depends on the hub version
397
+ // under test. Since camunda/camunda-hub#25831 it is a plain primary button on
398
+ // the workspace root, and the "Create new" menu explicitly hides the item
399
+ // (hideProcessApplication) because a process application is no longer just
400
+ // another resource. Before that change the menu item was the only way in.
401
+ // Race both entry points so neither version pays a full timeout, then use
402
+ // whichever one rendered.
403
+ async clickCreateProcessApplication() {
404
+ const button = this.processApplicationPage.createProcessApplicationButton;
405
+ const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
406
+ if (!rootLoaded) {
407
+ await this.page.reload();
408
+ }
409
+ // Short: the joint probe above already settled which branch rendered, and
410
+ // hub renders exactly one of them (EntityTable renders the toolbar's create
411
+ // menu OR the empty state, never both), so this only confirms which.
412
+ if (await this.becameVisible(button, 5000)) {
413
+ await this.processApplicationPage.clickCreateProcessApplicationButton();
414
+ return;
415
+ }
416
+ await this.diagramTypeDropdown.click({ timeout: 60000 });
417
+ await this.processApplicationPage.clickCreateProcessApplicationOption();
418
+ }
419
+ // Re-enters this test's container if it already has one, without creating
420
+ // one: opening an existing resource must not have the side effect of
421
+ // creating a container for a spec that never asked for it.
422
+ async ensureInResourceContainer() {
423
+ if (!this.processApplicationUrl) {
424
+ return;
425
+ }
426
+ await this.enterResourceContainer();
427
+ }
428
+ // Navigates back to this test's process application by the remembered URL
429
+ // rather than by a breadcrumb click: hub renders no process-application
430
+ // breadcrumb on a diagram page at container root, and the breadcrumb that
431
+ // does exist on the container page doubles as its rename control. Hub's own
432
+ // Playwright suite navigates back the same way.
433
+ async returnToResourceContainer() {
434
+ if (!this.processApplicationUrl) {
435
+ await this.clickWorkspaceBreadcrumb();
436
+ return;
437
+ }
438
+ await this.page.goto(this.processApplicationUrl, {
439
+ waitUntil: 'domcontentloaded',
440
+ });
441
+ await this.processApplicationPage.waitForProcessApplication();
442
+ await this.clickMessageBanner();
443
+ }
444
+ // Single seam for "give me an empty BPMN diagram in this test's container".
445
+ // A process application always ships an auto-created main process; this adds
446
+ // a new diagram rather than adopting that one, so every existing modelling
447
+ // assertion keeps its current subject.
448
+ async startBlankBpmnDiagram() {
449
+ await this.clickDiagramTypeDropdown();
450
+ await this.clickBpmnTemplateOption();
321
451
  }
322
452
  async clickOpenOrganizationsButton() {
323
453
  await this.openOrganizationsButton.click({ timeout: 30000 });
@@ -351,6 +481,14 @@ class ModelerHomePage {
351
481
  }
352
482
  }
353
483
  }
484
+ // Boolean visibility probe. waitFor rather than isVisible, which resolves
485
+ // immediately and would report a false negative on a still-rendering page.
486
+ async becameVisible(locator, timeout) {
487
+ return locator
488
+ .waitFor({ state: 'visible', timeout })
489
+ .then(() => true)
490
+ .catch(() => false);
491
+ }
354
492
  // Probe with waitFor, not isVisible: the "New features" modal renders lazily
355
493
  // after page load, and isVisible resolves immediately — Playwright ignores
356
494
  // its timeout option — so it misses an overlay that has yet to paint.