@camunda/e2e-test-suite 0.0.1012 → 0.0.1013

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;
@@ -35,6 +36,7 @@ declare class FormJsPage {
35
36
  selectCluster(clusterName: string): Promise<void>;
36
37
  clickDeployButton(): Promise<void>;
37
38
  clickDeploySubButton(): Promise<void>;
39
+ selectOnlyThisResource(): Promise<void>;
38
40
  deployForm(clusterName: string): Promise<void>;
39
41
  }
40
42
  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;
@@ -25,6 +27,7 @@ class FormJsPage {
25
27
  deploySuccessNotification;
26
28
  constructor(page) {
27
29
  this.page = page;
30
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
28
31
  this.aiFormGeneratorButton = page.getByRole('button', {
29
32
  name: 'AI Form Generator',
30
33
  });
@@ -50,9 +53,12 @@ class FormJsPage {
50
53
  this.deployButton = page
51
54
  .locator('[data-test="action-bar"]')
52
55
  .getByRole('button', { name: 'Deploy', exact: true });
56
+ // Inside a process application hub titles this modal for the application
57
+ // rather than for the form, so 'Deploy form' never matches and the dialog
58
+ // reads as absent. pages/SM-8.10 filters on 'Deploy' for the same reason.
53
59
  this.dialog = page
54
60
  .locator('.cds--modal-container[role="dialog"]')
55
- .filter({ hasText: 'Deploy form' });
61
+ .filter({ hasText: 'Deploy' });
56
62
  this.deploySubButton = this.dialog.getByRole('button', {
57
63
  name: 'Deploy',
58
64
  exact: true,
@@ -124,6 +130,46 @@ class FormJsPage {
124
130
  async selectCluster(clusterName) {
125
131
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
126
132
  const healthCheckTimeout = 30000;
133
+ // Inside a process application the deploy dialog renders
134
+ // ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
135
+ // list: it shows only clusters assigned to one of the application's stages
136
+ // and auto-selects the first, so there is no label to click. Confirm the
137
+ // intended cluster is the one on offer instead.
138
+ //
139
+ // The unconfigured case is called out explicitly: without this the symptom
140
+ // is the cluster toggle never appearing, retried until the test times out,
141
+ // which says nothing about the actual cause.
142
+ const notConfigured = this.dialog.getByText('Clusters not configured for stages');
143
+ if (await notConfigured.isVisible().catch(() => false)) {
144
+ throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
145
+ 'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
146
+ }
147
+ const legacyClusterToggle = this.dialog.locator('label').filter({
148
+ hasText: new RegExp(`^${clusterName}`),
149
+ });
150
+ const isLegacyDialog = await legacyClusterToggle
151
+ .first()
152
+ .isVisible()
153
+ .catch(() => false);
154
+ if (!isLegacyDialog) {
155
+ const offered = await this.dialog
156
+ .getByText(clusterName, { exact: false })
157
+ .first()
158
+ .waitFor({ state: 'visible', timeout: 30000 })
159
+ .then(() => true)
160
+ .catch(() => false);
161
+ if (!offered) {
162
+ // Forget the binding before failing. The dialog lists exactly the
163
+ // clusters this application's stages point at, so a missing cluster
164
+ // means the binding did not take -- and no amount of reopening the
165
+ // dialog will make it appear. Clearing the record lets the caller's
166
+ // next attempt rebind instead of re-checking an unchanged dialog.
167
+ this.processApplicationPage.forgetStageCluster();
168
+ throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
169
+ 'binding did not take. It will be rebound on the next attempt.');
170
+ }
171
+ return;
172
+ }
127
173
  const healthyRegex = new RegExp(`${clusterName}[\\s\\S]*?HealthydevManage`);
128
174
  const cluster = this.dialog.locator(`[title="${clusterName}"]`);
129
175
  await (0, test_1.expect)(cluster).toBeVisible({ timeout: 30000 });
@@ -142,12 +188,39 @@ class FormJsPage {
142
188
  async clickDeploySubButton() {
143
189
  await this.deploySubButton.click({ timeout: 60000 });
144
190
  }
191
+ // Since camunda/camunda-hub#25831 a form lives inside a process application,
192
+ // and Hub then scopes Deploy to the WHOLE application by default -- it calls
193
+ // deployProcessApplication, not deployFile, so the "Form deployed!" toast
194
+ // never fires (hub's own form-deploy-action.spec.sm.tsx asserts exactly that
195
+ // default). Narrowing the scope keeps this a form-deploy test instead of
196
+ // silently turning it into a process-application deploy. The combobox only
197
+ // exists inside a container, hence the boolean probe.
198
+ async selectOnlyThisResource() {
199
+ const scopeSelect = this.dialog.getByRole('combobox', {
200
+ name: 'Resources to deploy',
201
+ });
202
+ const hasScopeSelect = await scopeSelect
203
+ .waitFor({ state: 'visible', timeout: 10000 })
204
+ .then(() => true)
205
+ .catch(() => false);
206
+ if (!hasScopeSelect) {
207
+ return;
208
+ }
209
+ await scopeSelect.click();
210
+ await this.dialog
211
+ .getByRole('option', { name: 'Only this resource' })
212
+ .or(this.dialog.getByText('Only this resource', { exact: true }))
213
+ .first()
214
+ .click({ timeout: 30000 });
215
+ }
145
216
  async deployForm(clusterName) {
217
+ await this.processApplicationPage.ensureStageCluster(clusterName);
146
218
  await this.clickDeployButton();
147
219
  await (0, test_1.expect)(this.dialog).toBeVisible({
148
220
  timeout: 30000,
149
221
  });
150
222
  await this.selectCluster(clusterName);
223
+ await this.selectOnlyThisResource();
151
224
  await this.clickDeploySubButton();
152
225
  await (0, test_1.expect)(this.deploySuccessNotification).toBeVisible({ timeout: 60000 });
153
226
  }
@@ -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;
@@ -5,8 +5,10 @@ const test_1 = require("@playwright/test");
5
5
  const sleep_1 = require("../../utils/sleep");
6
6
  const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
7
7
  const clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
8
+ const ProcessApplicationPage_1 = require("./ProcessApplicationPage");
8
9
  class ModelerCreatePage {
9
10
  page;
11
+ processApplicationPage;
10
12
  generalPanel;
11
13
  deployDiagramDialog;
12
14
  processIdInput;
@@ -115,6 +117,7 @@ class ModelerCreatePage {
115
117
  diagramNameBreadcrumb;
116
118
  constructor(page) {
117
119
  this.page = page;
120
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
118
121
  this.generalPanel = page.locator('[data-group-id="group-general"]');
119
122
  this.processIdInput = page.getByLabel('ID', { exact: true });
120
123
  this.startEventElement = page.locator('.djs-hit').first();
@@ -250,8 +253,12 @@ class ModelerCreatePage {
250
253
  this.rbaEnabledDeployedDialog = page.getByRole('dialog', {
251
254
  name: 'Process successfully deployed',
252
255
  });
256
+ // Since camunda/camunda-hub#25831 a diagram deploys as part of its process
257
+ // application and hub titles the modal for the application, not for the
258
+ // single resource, so an exact 'Deploy diagram' name no longer matches.
259
+ // Match on the verb -- pages/SM-8.10 already leaves this unfiltered.
253
260
  this.deployDiagramDialog = page.getByRole('dialog', {
254
- name: 'Deploy diagram',
261
+ name: /^Deploy/,
255
262
  });
256
263
  this.appendGatewayButton = page.getByRole('button', {
257
264
  name: 'Append gateway',
@@ -567,6 +574,7 @@ class ModelerCreatePage {
567
574
  await this.deployMenuItem.click({ timeout: 60000 });
568
575
  }
569
576
  async deployDiagram(clusterName) {
577
+ await this.processApplicationPage.ensureStageCluster(clusterName);
570
578
  await this.clickDeployButtonWithRetry();
571
579
  await (0, test_1.expect)(this.deployDiagramDialog).toBeVisible({
572
580
  timeout: 30000,
@@ -587,6 +595,10 @@ class ModelerCreatePage {
587
595
  const retryDelayMs = 20000;
588
596
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
589
597
  try {
598
+ // Inside the loop, not before it: a no-op once bound, but selectCluster
599
+ // clears the binding when the dialog does not offer the cluster, so the
600
+ // next attempt rebinds rather than reopening an unchanged dialog.
601
+ await this.processApplicationPage.ensureStageCluster(clusterName);
590
602
  await this.clickStartInstanceMainButton();
591
603
  await this.selectCluster(clusterName);
592
604
  if (variables.length > 0) {
@@ -642,6 +654,46 @@ class ModelerCreatePage {
642
654
  async selectCluster(clusterName) {
643
655
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
644
656
  const healthCheckTimeout = 30000;
657
+ // Inside a process application the deploy dialog renders
658
+ // ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
659
+ // list: it shows only clusters assigned to one of the application's stages
660
+ // and auto-selects the first, so there is no label to click. Confirm the
661
+ // intended cluster is the one on offer instead.
662
+ //
663
+ // The unconfigured case is called out explicitly: without this the symptom
664
+ // is the cluster toggle never appearing, retried until the test times out,
665
+ // which says nothing about the actual cause.
666
+ const notConfigured = this.dialog.getByText('Clusters not configured for stages');
667
+ if (await notConfigured.isVisible().catch(() => false)) {
668
+ throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
669
+ 'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
670
+ }
671
+ const legacyClusterToggle = this.dialog.locator('label').filter({
672
+ hasText: new RegExp(`^${clusterName}`),
673
+ });
674
+ const isLegacyDialog = await legacyClusterToggle
675
+ .first()
676
+ .isVisible()
677
+ .catch(() => false);
678
+ if (!isLegacyDialog) {
679
+ const offered = await this.dialog
680
+ .getByText(clusterName, { exact: false })
681
+ .first()
682
+ .waitFor({ state: 'visible', timeout: 30000 })
683
+ .then(() => true)
684
+ .catch(() => false);
685
+ if (!offered) {
686
+ // Forget the binding before failing. The dialog lists exactly the
687
+ // clusters this application's stages point at, so a missing cluster
688
+ // means the binding did not take -- and no amount of reopening the
689
+ // dialog will make it appear. Clearing the record lets the caller's
690
+ // next attempt rebind instead of re-checking an unchanged dialog.
691
+ this.processApplicationPage.forgetStageCluster();
692
+ throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
693
+ 'binding did not take. It will be rebound on the next attempt.');
694
+ }
695
+ return;
696
+ }
645
697
  const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
646
698
  const clusterLabel = this.dialog
647
699
  .locator('label')
@@ -667,8 +719,45 @@ class ModelerCreatePage {
667
719
  async clickDiagramBreadcrumb() {
668
720
  await this.diagramBreadcrumb.click({ timeout: 10000 });
669
721
  }
722
+ // Opening the breadcrumb menu leaves the pointer on the breadcrumb button,
723
+ // and hub renders that button's truncated-name tooltip on top of the menu,
724
+ // where it swallows the click on "Rename" -- playwright reports
725
+ // `<div role="tooltip">…</div> intercepts pointer events`. Since
726
+ // camunda/camunda-hub#25831 the breadcrumb carries an extra
727
+ // process-application crumb, so the name truncates, and the tooltip renders,
728
+ // as the rule rather than the exception. Park the pointer away from the
729
+ // breadcrumb and let the tooltip unmount before clicking; reopen the menu if
730
+ // a previous attempt closed it.
670
731
  async clickEditDiagramNameButton() {
671
- await this.renameDiagramNameButton.click({ timeout: 10000 });
732
+ const openTooltip = this.page.locator('[role="tooltip"][aria-hidden="false"]');
733
+ await (0, test_1.expect)(async () => {
734
+ const menuOpen = await this.renameDiagramNameButton
735
+ .isVisible()
736
+ .catch(() => false);
737
+ if (!menuOpen) {
738
+ await this.clickDiagramBreadcrumb();
739
+ }
740
+ // Park the pointer so the breadcrumb's own tooltip gets a chance to
741
+ // unmount -- best effort, because it is not reliably hover-driven, and
742
+ // requiring it to go turned an interceptable click into a hard failure.
743
+ await this.page.mouse.move(0, 0);
744
+ await openTooltip
745
+ .first()
746
+ .waitFor({ state: 'hidden', timeout: 2000 })
747
+ .catch(() => false);
748
+ try {
749
+ await this.renameDiagramNameButton.click({ timeout: 10000 });
750
+ }
751
+ catch {
752
+ // The tooltip renders over the menu and swallows the click (playwright
753
+ // reports `<div role="tooltip">…</div> intercepts pointer events`).
754
+ // Dispatching on the element bypasses hit-testing, which no amount of
755
+ // waiting achieves when the tooltip never unmounts. Not a swallowed
756
+ // failure: the assertion below is what decides whether this worked.
757
+ await this.renameDiagramNameButton.dispatchEvent('click');
758
+ }
759
+ await (0, test_1.expect)(this.diagramNameInput).toBeVisible({ timeout: 10000 });
760
+ }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
672
761
  }
673
762
  async enterDiagramName(name) {
674
763
  await this.diagramNameInput.fill(name, { timeout: 90000 });
@@ -1224,6 +1313,16 @@ class ModelerCreatePage {
1224
1313
  await this.playTab.click();
1225
1314
  }
1226
1315
  async completePlayConfiguration(clusterName) {
1316
+ // Play is a tab on the diagram page, not its own URL, so the stage-cluster
1317
+ // detour's goto() lands back on the Modeler tab and every Play locator
1318
+ // below would then look for a panel that is no longer rendered. Re-enter
1319
+ // Play when the detour actually navigated. Binding cannot simply move
1320
+ // before switchToPlay(): this is the first point in the flow that knows
1321
+ // which cluster the test plays on.
1322
+ const reboundStageCluster = await this.processApplicationPage.ensureStageCluster(clusterName);
1323
+ if (reboundStageCluster) {
1324
+ await this.switchToPlay();
1325
+ }
1227
1326
  const timeout = 30000;
1228
1327
  // New flow (8.10+): "Setup environment" panel with three steps:
1229
1328
  // 1. Connect cluster → "Configure environment" modal → select cluster → Save
@@ -1247,19 +1346,37 @@ class ModelerCreatePage {
1247
1346
  name: /^Configure (test case|scenario)$/,
1248
1347
  }))
1249
1348
  .first();
1250
- // Wait up to 15s for EITHER the new-flow setup panel OR the legacy Continue
1251
- // button to appear. Using .or() avoids separate sequential timeouts that would
1252
- // both expire before the page finishes loading on a slow cluster.
1253
- const setupOrContinue = setupDeployButton.or(this.continueToPlayButton);
1254
- const panelAppeared = await setupOrContinue
1255
- .first()
1256
- .isVisible({ timeout: 15000 })
1257
- .catch(() => false);
1258
- if (!panelAppeared) {
1259
- // Neither appeared — Play is already configured and ready.
1349
+ // Play settles into exactly ONE of three states, and which one is not
1350
+ // knowable up front, so race them instead of timing out on each in turn:
1351
+ // the setup overlay, the legacy Continue button, or the configure-test
1352
+ // panel (meaning setup is already done for this diagram).
1353
+ //
1354
+ // Waiting only for the first two and reading "neither" as "already ready"
1355
+ // was wrong once the diagram moved inside a process application.
1356
+ // ConfigurationOverlay renders null until isDeploymentStateValidated, and
1357
+ // it sets that flag synchronously ONLY when processApplicationId is
1358
+ // undefined -- exactly what a project-root diagram used to give it. With
1359
+ // the diagram in a process application the id is defined, so the overlay
1360
+ // must first await checkExistingDeployment(). The old 15s probe raced that
1361
+ // async call, concluded "already ready" and returned while the overlay was
1362
+ // still about to appear. ConfigureTestPanel returns null for as long as the
1363
+ // overlay is up, so the Start button never existed and
1364
+ // clickStartInstanceButton burned its entire retry budget looking for it.
1365
+ const configurationOverlay = this.page.locator('[data-test="play-configuration-overlay"]');
1366
+ const configureTestPanel = this.page.getByTestId('configure-test-panel');
1367
+ await (0, test_1.expect)(configurationOverlay
1368
+ .or(this.continueToPlayButton)
1369
+ .or(configureTestPanel)
1370
+ .first()).toBeVisible({ timeout: 90000 });
1371
+ if (await configureTestPanel.isVisible()) {
1372
+ // Already past setup for this diagram -- nothing to drive.
1260
1373
  return;
1261
1374
  }
1262
- const isNewFlow = await setupDeployButton.isVisible().catch(() => false);
1375
+ // Keyed on the overlay itself, not on its Deploy button: when
1376
+ // checkExistingDeployment finds a deployment the overlay opens with the
1377
+ // deploy step already complete, so there is no Deploy button to see and
1378
+ // keying on it would fall through to the legacy Continue flow.
1379
+ const isNewFlow = await configurationOverlay.isVisible();
1263
1380
  if (isNewFlow) {
1264
1381
  // Step 1: connect cluster if not yet connected (modal with cluster dropdown)
1265
1382
  const connectClusterButton = this.page.getByRole('button', {
@@ -1298,13 +1415,33 @@ class ModelerCreatePage {
1298
1415
  .getByRole('button', { name: 'Save' })
1299
1416
  .click({ timeout });
1300
1417
  }
1301
- // Step 2: deploy — wait for enabled; button stays disabled until the cluster
1302
- // connection is confirmed by the backend after the Save in step 1.
1303
- await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
1304
- await setupDeployButton.click({ timeout });
1305
- await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1306
- // Step 3: configure scenario
1307
- await configureScenarioButton.click({ timeout });
1418
+ // Step 2: deploy, unless the overlay already opened with the
1419
+ // deploy step complete. checkExistingDeployment only runs when
1420
+ // processApplicationId is defined, which it now is, so re-entering
1421
+ // Play on an already-deployed process application legitimately
1422
+ // has nothing to deploy and renders no Deploy button at all.
1423
+ const alreadyDeployed = await configureScenarioButton
1424
+ .isVisible({ timeout: 3000 })
1425
+ .catch(() => false);
1426
+ if (!alreadyDeployed) {
1427
+ // Step 2: deploy — wait for enabled; button stays disabled until the cluster
1428
+ // connection is confirmed by the backend after the Save in step 1.
1429
+ await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
1430
+ await setupDeployButton.click({ timeout });
1431
+ await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1432
+ }
1433
+ // Step 3: leave the overlay, and confirm we actually left it. The
1434
+ // button only calls hideConfigurationOverlay(); the overlay's own
1435
+ // cluster-change effect can then fire resetDeployment and bring it
1436
+ // straight back, and ConfigureTestPanel stays null while it is up.
1437
+ // Assert the panel really rendered, and re-drive the click if the
1438
+ // overlay returned.
1439
+ await (0, test_1.expect)(async () => {
1440
+ if (await configurationOverlay.isVisible()) {
1441
+ await configureScenarioButton.click({ timeout });
1442
+ }
1443
+ await (0, test_1.expect)(configureTestPanel).toBeVisible({ timeout: 15000 });
1444
+ }).toPass({ timeout: 90000 });
1308
1445
  return;
1309
1446
  }
1310
1447
  // 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,10 +39,18 @@ 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
  clickProjectBreadcrumb(): Promise<void>;
47
+ clickWorkspaceBreadcrumb(): Promise<void>;
48
+ enterResourceContainer(name?: string): Promise<void>;
49
+ private clickCreateProcessApplication;
50
+ private ensureInResourceContainer;
51
+ returnToResourceContainer(): Promise<void>;
52
+ startBlankBpmnDiagram(): Promise<void>;
53
+ private becameVisible;
42
54
  clickHomeBreadcrumb(): Promise<void>;
43
55
  clickOpenOrganizationsButton(): Promise<void>;
44
56
  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.9.ts). Specs that only
17
+ // need the workspace itself -- collaborators, IDP applications -- never
18
+ // trigger 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' });
@@ -134,12 +158,32 @@ class ModelerHomePage {
134
158
  .map((el) => el.textContent || '')
135
159
  .filter((text) => text !== ''));
136
160
  }
161
+ // Opens a resource where this test put it. Several specs leave Modeler
162
+ // entirely (to Operate, to Optimize) and come back via
163
+ // clickCrossComponentProjectFolder(), which lands at workspace root -- the
164
+ // resource is one level down, so re-enter the container first.
137
165
  async clickProcessDiagram(name) {
166
+ await this.ensureInResourceContainer();
138
167
  const process = this.rows.filter({ hasText: name }).getByTitle(name).first();
139
168
  await (0, test_1.expect)(process).toBeVisible({ timeout: 90000 });
140
169
  await process.click();
141
170
  }
171
+ // Opens the create menu inside this test's process application. The menu at
172
+ // workspace root no longer offers BPMN, Form or Upload -- those resources
173
+ // cannot live there -- so the container is entered first. Callers are
174
+ // unchanged, which is why the existing spec call sites need no edit.
142
175
  async clickDiagramTypeDropdown() {
176
+ await this.enterResourceContainer();
177
+ const dropdown = this.processApplicationPage.createNewDropdown;
178
+ await (0, test_1.expect)(dropdown).toBeVisible({
179
+ timeout: 60000,
180
+ });
181
+ await dropdown.click();
182
+ }
183
+ // Opens the create menu at workspace root, which is the only place process
184
+ // applications and IDP applications can be created. Everything else has to
185
+ // go through clickDiagramTypeDropdown().
186
+ async clickWorkspaceDiagramTypeDropdown() {
143
187
  await (0, test_1.expect)(this.diagramTypeDropdown).toBeVisible({
144
188
  timeout: 15000,
145
189
  });
@@ -159,8 +203,109 @@ class ModelerHomePage {
159
203
  await this.formNameInput.fill(name);
160
204
  await this.formNameInput.press('Enter');
161
205
  }
206
+ // Kept under its original name because it is what the existing call sites
207
+ // already mean: "go back to where my resources live". That is now the
208
+ // process application, not the workspace root.
162
209
  async clickProjectBreadcrumb() {
163
- await this.projectBreadcrumb.click({ timeout: 60000 });
210
+ await this.returnToResourceContainer();
211
+ }
212
+ // The workspace ("project") level itself, for the surfaces that genuinely
213
+ // live there -- collaborators, IDP applications, published element
214
+ // templates.
215
+ async clickWorkspaceBreadcrumb() {
216
+ await this.workspaceBreadcrumb.click({ timeout: 60000 });
217
+ }
218
+ // Creates, once per test, the process application that holds this test's
219
+ // resources, and leaves the browser inside it. Idempotent: called again while
220
+ // already in the container it is a no-op, so a spec that creates several
221
+ // resources in a row pays for the container once.
222
+ //
223
+ // One container per test rather than one shared with the seeded workspace:
224
+ // deploying is process-application-wide, so a shared container would let one
225
+ // spec's deploy pick up every other spec's half-modelled diagram.
226
+ async enterResourceContainer(name) {
227
+ if (this.processApplicationUrl) {
228
+ const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
229
+ if (!stillInside) {
230
+ await this.returnToResourceContainer();
231
+ }
232
+ return;
233
+ }
234
+ this.processApplicationName =
235
+ name ??
236
+ (await (0, randomName_1.randomNameAgregator)('Cross Component Process Application'));
237
+ console.log(`Creating process application ${this.processApplicationName} inside ${this.defaultFolderName}`);
238
+ await this.clickCreateProcessApplication();
239
+ await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
240
+ this.processApplicationUrl = this.page.url();
241
+ // Remember the container; its stage cluster is bound on the first deploy,
242
+ // by ProcessApplicationPage.ensureStageCluster, which -- unlike this point
243
+ // in the flow -- knows which cluster the test is going to deploy to.
244
+ this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
245
+ }
246
+ // Which control creates a process application depends on the hub version
247
+ // under test. Since camunda/camunda-hub#25831 it is a plain primary button on
248
+ // the workspace root, and the "Create new" menu explicitly hides the item
249
+ // (hideProcessApplication) because a process application is no longer just
250
+ // another resource. Before that change the menu item was the only way in.
251
+ // Race both entry points so neither version pays a full timeout, then use
252
+ // whichever one rendered.
253
+ async clickCreateProcessApplication() {
254
+ const button = this.processApplicationPage.createProcessApplicationButton;
255
+ const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
256
+ if (!rootLoaded) {
257
+ await this.page.reload();
258
+ }
259
+ // Short: the joint probe above already settled which branch rendered, and
260
+ // hub renders exactly one of them (EntityTable renders the toolbar's create
261
+ // menu OR the empty state, never both), so this only confirms which.
262
+ if (await this.becameVisible(button, 5000)) {
263
+ await this.processApplicationPage.clickCreateProcessApplicationButton();
264
+ return;
265
+ }
266
+ await this.clickWorkspaceDiagramTypeDropdown();
267
+ await this.processApplicationPage.clickCreateProcessApplicationOption();
268
+ }
269
+ // Re-enters this test's container if it already has one, without creating
270
+ // one: opening an existing resource must not have the side effect of
271
+ // creating a container for a spec that never asked for it.
272
+ async ensureInResourceContainer() {
273
+ if (!this.processApplicationUrl) {
274
+ return;
275
+ }
276
+ await this.enterResourceContainer();
277
+ }
278
+ // Navigates back to this test's process application by the remembered URL
279
+ // rather than by a breadcrumb click: hub renders no process-application
280
+ // breadcrumb on a diagram page at container root, and the breadcrumb that
281
+ // does exist on the container page doubles as its rename control. Hub's own
282
+ // Playwright suite navigates back the same way.
283
+ async returnToResourceContainer() {
284
+ if (!this.processApplicationUrl) {
285
+ await this.clickWorkspaceBreadcrumb();
286
+ return;
287
+ }
288
+ await this.page.goto(this.processApplicationUrl, {
289
+ waitUntil: 'domcontentloaded',
290
+ });
291
+ await this.processApplicationPage.waitForProcessApplication();
292
+ await this.dismissOverlays();
293
+ }
294
+ // Single seam for "give me an empty BPMN diagram in this test's container".
295
+ // A process application always ships an auto-created main process; this adds
296
+ // a new diagram rather than adopting that one, so every existing modelling
297
+ // assertion keeps its current subject.
298
+ async startBlankBpmnDiagram() {
299
+ await this.clickDiagramTypeDropdown();
300
+ await this.clickBpmnTemplateOption();
301
+ }
302
+ // Boolean visibility probe. waitFor rather than isVisible, which resolves
303
+ // immediately and would report a false negative on a still-rendering page.
304
+ async becameVisible(locator, timeout) {
305
+ return locator
306
+ .waitFor({ state: 'visible', timeout })
307
+ .then(() => true)
308
+ .catch(() => false);
164
309
  }
165
310
  async clickHomeBreadcrumb() {
166
311
  try {
@@ -0,0 +1,30 @@
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
+ forgetStageCluster(): void;
26
+ ensureStageCluster(clusterName: string): Promise<boolean>;
27
+ private openStageDefinition;
28
+ openByName(name: string): Promise<void>;
29
+ }
30
+ export { ProcessApplicationPage };
@@ -0,0 +1,260 @@
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
+ // Drops the recorded development-stage binding so the next ensureStageCluster
170
+ // call binds again. Called when the deploy dialog turns out not to offer the
171
+ // cluster we believe we bound: the dialog offers exactly what the
172
+ // application's stages point at, so if the cluster is missing the binding
173
+ // did not take, and reopening the dialog can never change that. Without
174
+ // this, a caller's retries all re-check a state nothing is changing.
175
+ forgetStageCluster() {
176
+ const container = resourceContainers.get(this.page);
177
+ if (container) {
178
+ container.developmentCluster = undefined;
179
+ }
180
+ }
181
+ // A process application deploys to the clusters bound to its stages, and
182
+ // nothing else: inside a container DeployModal renders
183
+ // ProcessAppClusterSelection, which lists only those and auto-selects the
184
+ // first. Which cluster a test wants is known only once a deploy flow names
185
+ // it, so the binding happens here, on demand -- leave the diagram, bind the
186
+ // cluster to the development stage, come back. Repeat calls for the same
187
+ // cluster cost nothing, and a test deploying to a second cluster rebinds.
188
+ // Returns whether it navigated: the caller may have been on a view that a
189
+ // reload does not restore -- Play is a tab click, not a URL -- and has to
190
+ // re-enter it.
191
+ async ensureStageCluster(clusterName) {
192
+ const container = resourceContainers.get(this.page);
193
+ if (!container || container.developmentCluster === clusterName) {
194
+ return false;
195
+ }
196
+ const returnTo = this.page.url();
197
+ const paUrl = container.url.replace(/[?#].*$/, '').replace(/\/$/, '');
198
+ // Retried as a whole, re-entering the page on every attempt. This binding
199
+ // is a one-shot precondition for every deploy the test goes on to make,
200
+ // and unlike the deploy itself it sits outside runProcessInstance's retry
201
+ // loop -- so a single transient miss (the session bounced to /login, the
202
+ // cluster list not yet arrived when the dropdown opened) failed the test
203
+ // outright instead of being absorbed the way every other modal in this
204
+ // suite is. Re-navigating is the recovery; waiting longer on a page that
205
+ // has already gone wrong is not, which is why the inner timeouts stay
206
+ // short. A genuinely unbindable cluster still fails, with the same error.
207
+ await (0, test_1.expect)(async () => {
208
+ await this.page.goto(`${paUrl}/connected-clusters`, {
209
+ waitUntil: 'domcontentloaded',
210
+ });
211
+ await this.openStageDefinition();
212
+ await this.defineDevelopmentStage(clusterName);
213
+ }).toPass({ timeout: 240000, intervals: [1000, 2000, 5000] });
214
+ container.developmentCluster = clusterName;
215
+ await this.page.goto(returnTo, { waitUntil: 'domcontentloaded' });
216
+ return true;
217
+ }
218
+ // The /connected-clusters deep link opens the Define stages dialog by itself
219
+ // while the application has no stage bound. Once one is bound -- a test that
220
+ // deploys to a second cluster -- it has to be reopened from the page, so try
221
+ // the affordances hub offers before failing with exactly what was looked for.
222
+ // Worth the noise: an unbound cluster otherwise surfaces as a deploy dialog
223
+ // that silently offers nothing, which says nothing about its own cause.
224
+ async openStageDefinition() {
225
+ const opened = await this.defineStagesDialog
226
+ .waitFor({ state: 'visible', timeout: 15000 })
227
+ .then(() => true)
228
+ .catch(() => false);
229
+ if (opened) {
230
+ return;
231
+ }
232
+ // Page-scoped .first(): hub renders one such control on this page, and a
233
+ // second placement must not turn every rebinding test into a strict-mode
234
+ // violation.
235
+ const reopen = this.page
236
+ .getByRole('button', { name: /define stages|edit stages|configure/i })
237
+ .first();
238
+ const canReopen = await reopen
239
+ .waitFor({ state: 'visible', timeout: 10000 })
240
+ .then(() => true)
241
+ .catch(() => false);
242
+ if (!canReopen) {
243
+ throw new Error('Could not open the "Define stages" dialog on /connected-clusters: it ' +
244
+ 'did not open by itself and no "Define stages" / "Edit stages" / ' +
245
+ '"Configure" control was found.');
246
+ }
247
+ await reopen.click({ timeout: 30000 });
248
+ }
249
+ async openByName(name) {
250
+ const processApplication = this.page
251
+ .getByRole('row')
252
+ .filter({ hasText: name })
253
+ .getByTitle(name)
254
+ .first();
255
+ await (0, test_1.expect)(processApplication).toBeVisible({ timeout: 90000 });
256
+ await processApplication.click({ timeout: 60000 });
257
+ await this.waitForProcessApplication();
258
+ }
259
+ }
260
+ exports.ProcessApplicationPage = ProcessApplicationPage;
@@ -465,6 +465,14 @@ async function modelAndRunConnectorsTimerEventDiagram(modelerCreatePage, modeler
465
465
  timeout: 60000,
466
466
  });
467
467
  await connectorMarketplacePage.downloadConnectorToProject();
468
+ // Both lookups are container-relative: clickProjectBreadcrumb() returns to
469
+ // this test's process application, which is where the uploaded diagram
470
+ // lives. The downloaded element template is the open question -- hub
471
+ // publishes element templates to the workspace, but the marketplace download
472
+ // is initiated from inside the container, so it should land there. If a
473
+ // trace shows the template at workspace root instead, this call becomes
474
+ // clickWorkspaceBreadcrumb() followed by returnToResourceContainer() before
475
+ // the diagram is reopened.
468
476
  await modelerHomePage.clickProjectBreadcrumb();
469
477
  await modelerHomePage.clickProcessDiagram('REST Outbound Connector');
470
478
  await connectorTemplatePage.publishConnector();
@@ -125,7 +125,7 @@ _8_9_1.test.describe('User Roles User Flow', () => {
125
125
  await modelerCreatePage.clickAppendEndEventButton();
126
126
  await (0, sleep_1.sleep)(20000);
127
127
  await modelerCreatePage.runProcessInstance(clusterName);
128
- await (0, test_1.expect)(page.getByText('The diagram failed to deploy & run')).toBeVisible({ timeout: 60000 });
128
+ await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).toBeVisible({ timeout: 60000 });
129
129
  await modelerCreatePage.clickCloseButton();
130
130
  });
131
131
  });
@@ -216,7 +216,7 @@ _8_9_1.test.describe('User Roles User Flow', () => {
216
216
  await modelerCreatePage.clickAppendEndEventButton();
217
217
  await (0, sleep_1.sleep)(20000);
218
218
  await modelerCreatePage.runProcessInstance(clusterName, '', {}, '', false);
219
- await (0, test_1.expect)(page.getByText('The diagram failed to deploy & run')).toBeVisible({ timeout: 60000 });
219
+ await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).toBeVisible({ timeout: 60000 });
220
220
  await modelerCreatePage.clickCloseButton();
221
221
  });
222
222
  });
@@ -312,7 +312,7 @@ _8_9_1.test.describe('User Roles User Flow', () => {
312
312
  await modelerCreatePage.clickAppendEndEventButton();
313
313
  await (0, sleep_1.sleep)(20000);
314
314
  await modelerCreatePage.runProcessInstance(clusterName);
315
- await (0, test_1.expect)(page.getByText('The diagram failed to deploy & run')).not.toBeVisible({ timeout: 60000 });
315
+ await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).not.toBeVisible({ timeout: 60000 });
316
316
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeVisible({
317
317
  timeout: 120000,
318
318
  });
@@ -94,7 +94,9 @@ _8_9_1.test.describe('IDP User Flow Tests', () => {
94
94
  await modelerHomePage.clickCrossComponentProjectFolder();
95
95
  });
96
96
  await _8_9_1.test.step('Add an IDP application to the project', async () => {
97
- await modelerHomePage.clickDiagramTypeDropdown();
97
+ // Workspace-level menu: an IDP application can only be created at
98
+ // workspace root, never inside a process application.
99
+ await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
98
100
  await modelerHomePage.clickIdpApplicationTemplateOption();
99
101
  });
100
102
  await _8_9_1.test.step('Creates a document extraction template', async () => {
@@ -166,7 +168,9 @@ _8_9_1.test.describe('IDP User Flow Tests', () => {
166
168
  await modelerHomePage.clickCrossComponentProjectFolder();
167
169
  });
168
170
  await _8_9_1.test.step('Add an IDP application to the project', async () => {
169
- await modelerHomePage.clickDiagramTypeDropdown();
171
+ // Workspace-level menu: an IDP application can only be created at
172
+ // workspace root, never inside a process application.
173
+ await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
170
174
  await modelerHomePage.clickIdpApplicationTemplateOption();
171
175
  });
172
176
  await _8_9_1.test.step('Creates a document extraction template', async () => {
@@ -246,7 +250,9 @@ _8_9_1.test.describe('IDP User Flow Tests', () => {
246
250
  await modelerHomePage.clickCrossComponentProjectFolder();
247
251
  });
248
252
  await _8_9_1.test.step('Add an IDP application to the project', async () => {
249
- await modelerHomePage.clickDiagramTypeDropdown();
253
+ // Workspace-level menu: an IDP application can only be created at
254
+ // workspace root, never inside a process application.
255
+ await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
250
256
  await modelerHomePage.clickIdpApplicationTemplateOption();
251
257
  });
252
258
  await _8_9_1.test.step('Creates a classification template', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1012",
3
+ "version": "0.0.1013",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",