@camunda/e2e-test-suite 0.0.1014 → 0.0.1016

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 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 clickLocatorWithRetry_1 = require("../../utils/assertionHelpers/clickLocatorWithRetry");
7
7
  const ConnectorMarketplacePage_1 = require("./ConnectorMarketplacePage");
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();
@@ -253,8 +256,12 @@ class ModelerCreatePage {
253
256
  this.rbaEnabledDeployedDialog = page.getByRole('dialog', {
254
257
  name: 'Process successfully deployed',
255
258
  });
259
+ // Since camunda/camunda-hub#25831 a diagram deploys as part of its process
260
+ // application and hub titles the modal for the application, not for the
261
+ // single resource, so an exact 'Deploy diagram' name no longer matches.
262
+ // Match on the verb -- pages/SM-8.10 already leaves this unfiltered.
256
263
  this.deployDiagramDialog = page.getByRole('dialog', {
257
- name: 'Deploy diagram',
264
+ name: /^Deploy/,
258
265
  });
259
266
  this.appendGatewayButton = page.getByRole('button', {
260
267
  name: 'Append gateway',
@@ -565,6 +572,7 @@ class ModelerCreatePage {
565
572
  await this.deployMenuItem.click({ timeout: 60000 });
566
573
  }
567
574
  async deployDiagram(clusterName) {
575
+ await this.processApplicationPage.ensureStageCluster(clusterName);
568
576
  await this.clickDeployButtonWithRetry();
569
577
  await (0, test_1.expect)(this.deployDiagramDialog).toBeVisible({
570
578
  timeout: 30000,
@@ -605,6 +613,10 @@ class ModelerCreatePage {
605
613
  const retryDelayMs = 20000;
606
614
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
607
615
  try {
616
+ // Inside the loop, not before it: a no-op once bound, but selectCluster
617
+ // clears the binding when the dialog does not offer the cluster, so the
618
+ // next attempt rebinds rather than reopening an unchanged dialog.
619
+ await this.processApplicationPage.ensureStageCluster(clusterName);
608
620
  await this.clickStartInstanceMainButton();
609
621
  await this.selectCluster(clusterName);
610
622
  if (variables.length > 0) {
@@ -661,6 +673,46 @@ class ModelerCreatePage {
661
673
  async selectCluster(clusterName) {
662
674
  await (0, test_1.expect)(this.dialog).toBeVisible({ timeout: 30000 });
663
675
  const healthCheckTimeout = 30000;
676
+ // Inside a process application the deploy dialog renders
677
+ // ProcessAppClusterSelection, not the per-deploy ClusterSelection toggle
678
+ // list: it shows only clusters assigned to one of the application's stages
679
+ // and auto-selects the first, so there is no label to click. Confirm the
680
+ // intended cluster is the one on offer instead.
681
+ //
682
+ // The unconfigured case is called out explicitly: without this the symptom
683
+ // is the cluster toggle never appearing, retried until the test times out,
684
+ // which says nothing about the actual cause.
685
+ const notConfigured = this.dialog.getByText('Clusters not configured for stages');
686
+ if (await notConfigured.isVisible().catch(() => false)) {
687
+ throw new Error(`Process application has no cluster assigned to any stage, so "${clusterName}" cannot be deployed to. ` +
688
+ 'ProcessApplicationPage.ensureStageCluster() should have bound it before the deploy dialog was opened.');
689
+ }
690
+ const legacyClusterToggle = this.dialog.locator('label').filter({
691
+ hasText: new RegExp(`^${clusterName}`),
692
+ });
693
+ const isLegacyDialog = await legacyClusterToggle
694
+ .first()
695
+ .isVisible()
696
+ .catch(() => false);
697
+ if (!isLegacyDialog) {
698
+ const offered = await this.dialog
699
+ .getByText(clusterName, { exact: false })
700
+ .first()
701
+ .waitFor({ state: 'visible', timeout: 30000 })
702
+ .then(() => true)
703
+ .catch(() => false);
704
+ if (!offered) {
705
+ // Forget the binding before failing. The dialog lists exactly the
706
+ // clusters this application's stages point at, so a missing cluster
707
+ // means the binding did not take -- and no amount of reopening the
708
+ // dialog will make it appear. Clearing the record lets the caller's
709
+ // next attempt rebind instead of re-checking an unchanged dialog.
710
+ this.processApplicationPage.forgetStageCluster();
711
+ throw new Error(`The deploy dialog does not offer "${clusterName}": its stage ` +
712
+ 'binding did not take. It will be rebound on the next attempt.');
713
+ }
714
+ return;
715
+ }
664
716
  const healthyRegex = new RegExp(`${clusterName}.*?HealthydevManage`);
665
717
  const clusterLabel = this.dialog
666
718
  .locator('label')
@@ -704,8 +756,45 @@ class ModelerCreatePage {
704
756
  async clickDiagramBreadcrumb() {
705
757
  await this.diagramBreadcrumb.click({ timeout: 10000 });
706
758
  }
759
+ // Opening the breadcrumb menu leaves the pointer on the breadcrumb button,
760
+ // and hub renders that button's truncated-name tooltip on top of the menu,
761
+ // where it swallows the click on "Rename" -- playwright reports
762
+ // `<div role="tooltip">…</div> intercepts pointer events`. Since
763
+ // camunda/camunda-hub#25831 the breadcrumb carries an extra
764
+ // process-application crumb, so the name truncates, and the tooltip renders,
765
+ // as the rule rather than the exception. Park the pointer away from the
766
+ // breadcrumb and let the tooltip unmount before clicking; reopen the menu if
767
+ // a previous attempt closed it.
707
768
  async clickEditDiagramNameButton() {
708
- await this.renameDiagramNameButton.click({ timeout: 10000 });
769
+ const openTooltip = this.page.locator('[role="tooltip"][aria-hidden="false"]');
770
+ await (0, test_1.expect)(async () => {
771
+ const menuOpen = await this.renameDiagramNameButton
772
+ .isVisible()
773
+ .catch(() => false);
774
+ if (!menuOpen) {
775
+ await this.clickDiagramBreadcrumb();
776
+ }
777
+ // Park the pointer so the breadcrumb's own tooltip gets a chance to
778
+ // unmount -- best effort, because it is not reliably hover-driven, and
779
+ // requiring it to go turned an interceptable click into a hard failure.
780
+ await this.page.mouse.move(0, 0);
781
+ await openTooltip
782
+ .first()
783
+ .waitFor({ state: 'hidden', timeout: 2000 })
784
+ .catch(() => false);
785
+ try {
786
+ await this.renameDiagramNameButton.click({ timeout: 10000 });
787
+ }
788
+ catch {
789
+ // The tooltip renders over the menu and swallows the click (playwright
790
+ // reports `<div role="tooltip">…</div> intercepts pointer events`).
791
+ // Dispatching on the element bypasses hit-testing, which no amount of
792
+ // waiting achieves when the tooltip never unmounts. Not a swallowed
793
+ // failure: the assertion below is what decides whether this worked.
794
+ await this.renameDiagramNameButton.dispatchEvent('click');
795
+ }
796
+ await (0, test_1.expect)(this.diagramNameInput).toBeVisible({ timeout: 10000 });
797
+ }).toPass({ timeout: 60000, intervals: [1000, 2000, 5000] });
709
798
  }
710
799
  async enterDiagramName(name) {
711
800
  await this.diagramNameInput.fill(name, { timeout: 90000 });
@@ -1278,6 +1367,16 @@ class ModelerCreatePage {
1278
1367
  await this.playTab.click();
1279
1368
  }
1280
1369
  async completePlayConfiguration(clusterName) {
1370
+ // Play is a tab on the diagram page, not its own URL, so the stage-cluster
1371
+ // detour's goto() lands back on the Modeler tab and every Play locator
1372
+ // below would then look for a panel that is no longer rendered. Re-enter
1373
+ // Play when the detour actually navigated. Binding cannot simply move
1374
+ // before switchToPlay(): this is the first point in the flow that knows
1375
+ // which cluster the test plays on.
1376
+ const reboundStageCluster = await this.processApplicationPage.ensureStageCluster(clusterName);
1377
+ if (reboundStageCluster) {
1378
+ await this.switchToPlay();
1379
+ }
1281
1380
  const timeout = 30000;
1282
1381
  // New flow (8.10+): "Setup environment" panel with three steps:
1283
1382
  // 1. Connect cluster → "Configure environment" modal → select cluster → Save
@@ -1301,19 +1400,37 @@ class ModelerCreatePage {
1301
1400
  name: /^Configure (test case|scenario)$/,
1302
1401
  }))
1303
1402
  .first();
1304
- // Wait up to 15s for EITHER the new-flow setup panel OR the legacy Continue
1305
- // button to appear. Using .or() avoids separate sequential timeouts that would
1306
- // both expire before the page finishes loading on a slow cluster.
1307
- const setupOrContinue = setupDeployButton.or(this.continueToPlayButton);
1308
- const panelAppeared = await setupOrContinue
1309
- .first()
1310
- .isVisible({ timeout: 15000 })
1311
- .catch(() => false);
1312
- if (!panelAppeared) {
1313
- // Neither appeared — Play is already configured and ready.
1403
+ // Play settles into exactly ONE of three states, and which one is not
1404
+ // knowable up front, so race them instead of timing out on each in turn:
1405
+ // the setup overlay, the legacy Continue button, or the configure-test
1406
+ // panel (meaning setup is already done for this diagram).
1407
+ //
1408
+ // Waiting only for the first two and reading "neither" as "already ready"
1409
+ // was wrong once the diagram moved inside a process application.
1410
+ // ConfigurationOverlay renders null until isDeploymentStateValidated, and
1411
+ // it sets that flag synchronously ONLY when processApplicationId is
1412
+ // undefined -- exactly what a project-root diagram used to give it. With
1413
+ // the diagram in a process application the id is defined, so the overlay
1414
+ // must first await checkExistingDeployment(). The old 15s probe raced that
1415
+ // async call, concluded "already ready" and returned while the overlay was
1416
+ // still about to appear. ConfigureTestPanel returns null for as long as the
1417
+ // overlay is up, so the Start button never existed and
1418
+ // clickStartInstanceButton burned its entire retry budget looking for it.
1419
+ const configurationOverlay = this.page.locator('[data-test="play-configuration-overlay"]');
1420
+ const configureTestPanel = this.page.getByTestId('configure-test-panel');
1421
+ await (0, test_1.expect)(configurationOverlay
1422
+ .or(this.continueToPlayButton)
1423
+ .or(configureTestPanel)
1424
+ .first()).toBeVisible({ timeout: 90000 });
1425
+ if (await configureTestPanel.isVisible()) {
1426
+ // Already past setup for this diagram -- nothing to drive.
1314
1427
  return;
1315
1428
  }
1316
- const isNewFlow = await setupDeployButton.isVisible().catch(() => false);
1429
+ // Keyed on the overlay itself, not on its Deploy button: when
1430
+ // checkExistingDeployment finds a deployment the overlay opens with the
1431
+ // deploy step already complete, so there is no Deploy button to see and
1432
+ // keying on it would fall through to the legacy Continue flow.
1433
+ const isNewFlow = await configurationOverlay.isVisible();
1317
1434
  if (isNewFlow) {
1318
1435
  // Step 1: connect cluster if not yet connected (modal with cluster dropdown)
1319
1436
  const connectClusterButton = this.page.getByRole('button', {
@@ -1352,13 +1469,33 @@ class ModelerCreatePage {
1352
1469
  .getByRole('button', { name: 'Save' })
1353
1470
  .click({ timeout });
1354
1471
  }
1355
- // Step 2: deploy — wait for enabled; button stays disabled until the cluster
1356
- // connection is confirmed by the backend after the Save in step 1.
1357
- await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
1358
- await setupDeployButton.click({ timeout });
1359
- await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1360
- // Step 3: configure scenario
1361
- await configureScenarioButton.click({ timeout });
1472
+ // Step 2: deploy, unless the overlay already opened with the
1473
+ // deploy step complete. checkExistingDeployment only runs when
1474
+ // processApplicationId is defined, which it now is, so re-entering
1475
+ // Play on an already-deployed process application legitimately
1476
+ // has nothing to deploy and renders no Deploy button at all.
1477
+ const alreadyDeployed = await configureScenarioButton
1478
+ .isVisible({ timeout: 3000 })
1479
+ .catch(() => false);
1480
+ if (!alreadyDeployed) {
1481
+ // Step 2: deploy — wait for enabled; button stays disabled until the cluster
1482
+ // connection is confirmed by the backend after the Save in step 1.
1483
+ await (0, test_1.expect)(setupDeployButton).toBeEnabled({ timeout: 30000 });
1484
+ await setupDeployButton.click({ timeout });
1485
+ await (0, test_1.expect)(this.page.getByText('Process has been successfully deployed')).toBeVisible({ timeout: 90000 });
1486
+ }
1487
+ // Step 3: leave the overlay, and confirm we actually left it. The
1488
+ // button only calls hideConfigurationOverlay(); the overlay's own
1489
+ // cluster-change effect can then fire resetDeployment and bring it
1490
+ // straight back, and ConfigureTestPanel stays null while it is up.
1491
+ // Assert the panel really rendered, and re-drive the click if the
1492
+ // overlay returned.
1493
+ await (0, test_1.expect)(async () => {
1494
+ if (await configurationOverlay.isVisible()) {
1495
+ await configureScenarioButton.click({ timeout });
1496
+ }
1497
+ await (0, test_1.expect)(configureTestPanel).toBeVisible({ timeout: 15000 });
1498
+ }).toPass({ timeout: 90000 });
1362
1499
  return;
1363
1500
  }
1364
1501
  // 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;
@@ -37,10 +41,18 @@ declare class ModelerHomePage {
37
41
  getProjectNames(name: string): Promise<string[]>;
38
42
  clickProcessDiagram(name: string): Promise<void>;
39
43
  clickDiagramTypeDropdown(): Promise<void>;
44
+ clickWorkspaceDiagramTypeDropdown(): Promise<void>;
40
45
  clickBpmnTemplateOption(): Promise<void>;
41
46
  clickFormOption(): Promise<void>;
42
47
  enterFormName(name: string): Promise<void>;
43
48
  clickProjectBreadcrumb(): Promise<void>;
49
+ clickWorkspaceBreadcrumb(): Promise<void>;
50
+ enterResourceContainer(name?: string): Promise<void>;
51
+ private clickCreateProcessApplication;
52
+ private ensureInResourceContainer;
53
+ returnToResourceContainer(): Promise<void>;
54
+ startBlankBpmnDiagram(): Promise<void>;
55
+ private becameVisible;
44
56
  clickHomeBreadcrumb(): Promise<void>;
45
57
  private dismissBlockingDialogs;
46
58
  clickOpenOrganizationsButton(): 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.8.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;
@@ -34,6 +50,7 @@ class ModelerHomePage {
34
50
  buttonSkipCustomization;
35
51
  constructor(page) {
36
52
  this.page = page;
53
+ this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
37
54
  this.modelerPageBanner = page.getByRole('banner', {
38
55
  name: 'Camunda Modeler',
39
56
  });
@@ -55,13 +72,20 @@ class ModelerHomePage {
55
72
  .locator('[data-test="create-idp-application"]')
56
73
  .getByText('IDP Application');
57
74
  this.diagramTypeDropdown = page.locator('[data-test="diagram-dropdown"]');
58
- this.bpmnTemplateOption = page
59
- .locator('[data-test="create-bpmn-diagram"]')
60
- .getByText('BPMN Diagram');
61
- this.formTemplateOption = page
62
- .locator('[data-test="create-form"]')
63
- .getByText('Form');
75
+ // Matched on the test id alone: hub's design-system variant of the create
76
+ // menu renders "BPMN diagram" (lowercase d), so a text filter breaks the
77
+ // day that variant ships.
78
+ this.bpmnTemplateOption = page.locator('[data-test="create-bpmn-diagram"]');
79
+ this.formTemplateOption = page.locator('[data-test="create-form"]');
64
80
  this.projectBreadcrumb = page.locator('[data-test="breadcrumb-project"]');
81
+ // Kept separate from projectBreadcrumb, which now means "the container my
82
+ // resources live in". When hub's workspaces terminology flag flips,
83
+ // breadcrumb-project comes to denote the process application, so the
84
+ // workspace level needs its own name from the start.
85
+ this.workspaceBreadcrumb = page
86
+ .locator('[data-test="breadcrumb-project"]')
87
+ .or(page.locator('[data-test="breadcrumb-workspace"]'))
88
+ .first();
65
89
  this.homeBreadcrumb = page.locator('[data-test="breadcrumb-home"]');
66
90
  this.openOrganizationsButton = page.getByLabel('Open Organizations');
67
91
  this.manageButton = page.getByRole('menuitem', { name: 'Manage' });
@@ -157,12 +181,49 @@ 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() {
199
+ await this.enterResourceContainer();
200
+ const dropdown = this.processApplicationPage.createNewDropdown;
201
+ for (let attempt = 0; attempt < 4; attempt++) {
202
+ try {
203
+ await this.dismissOverlays();
204
+ await (0, test_1.expect)(dropdown).toBeVisible({
205
+ timeout: 45000,
206
+ });
207
+ await dropdown.click({ timeout: 30000 });
208
+ return;
209
+ }
210
+ catch (error) {
211
+ if (attempt >= 3) {
212
+ throw error;
213
+ }
214
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
215
+ await this.page.waitForLoadState('domcontentloaded');
216
+ // Recover into the container rather than the workspace: the reload
217
+ // above can land on either, and the create menu we want is the
218
+ // container's.
219
+ await this.returnToResourceContainer();
220
+ }
221
+ }
222
+ }
223
+ // Opens the create menu at workspace root, which is the only place process
224
+ // applications and IDP applications can be created. Everything else has to
225
+ // go through clickDiagramTypeDropdown().
226
+ async clickWorkspaceDiagramTypeDropdown() {
166
227
  for (let attempt = 0; attempt < 4; attempt++) {
167
228
  try {
168
229
  await this.dismissOverlays();
@@ -194,8 +255,109 @@ class ModelerHomePage {
194
255
  await this.formNameInput.fill(name, { timeout: 60000 });
195
256
  await this.formNameInput.press('Enter');
196
257
  }
258
+ // Kept under its original name because it is what the existing call sites
259
+ // already mean: "go back to where my resources live". That is now the
260
+ // process application, not the workspace root.
197
261
  async clickProjectBreadcrumb() {
198
- await this.projectBreadcrumb.click({ timeout: 60000 });
262
+ await this.returnToResourceContainer();
263
+ }
264
+ // The workspace ("project") level itself, for the surfaces that genuinely
265
+ // live there -- collaborators, IDP applications, published element
266
+ // templates.
267
+ async clickWorkspaceBreadcrumb() {
268
+ await this.workspaceBreadcrumb.click({ timeout: 60000 });
269
+ }
270
+ // Creates, once per test, the process application that holds this test's
271
+ // resources, and leaves the browser inside it. Idempotent: called again while
272
+ // already in the container it is a no-op, so a spec that creates several
273
+ // resources in a row pays for the container once.
274
+ //
275
+ // One container per test rather than one shared with the seeded workspace:
276
+ // deploying is process-application-wide, so a shared container would let one
277
+ // spec's deploy pick up every other spec's half-modelled diagram.
278
+ async enterResourceContainer(name) {
279
+ if (this.processApplicationUrl) {
280
+ const stillInside = await this.becameVisible(this.processApplicationPage.breadcrumb, 5000);
281
+ if (!stillInside) {
282
+ await this.returnToResourceContainer();
283
+ }
284
+ return;
285
+ }
286
+ this.processApplicationName =
287
+ name ??
288
+ (await (0, randomName_1.randomNameAgregator)('Cross Component Process Application'));
289
+ console.log(`Creating process application ${this.processApplicationName} inside ${this.defaultFolderName}`);
290
+ await this.clickCreateProcessApplication();
291
+ await this.processApplicationPage.nameNewProcessApplication(this.processApplicationName);
292
+ this.processApplicationUrl = this.page.url();
293
+ // Remember the container; its stage cluster is bound on the first deploy,
294
+ // by ProcessApplicationPage.ensureStageCluster, which -- unlike this point
295
+ // in the flow -- knows which cluster the test is going to deploy to.
296
+ this.processApplicationPage.rememberResourceContainer(this.processApplicationUrl);
297
+ }
298
+ // Which control creates a process application depends on the hub version
299
+ // under test. Since camunda/camunda-hub#25831 it is a plain primary button on
300
+ // the workspace root, and the "Create new" menu explicitly hides the item
301
+ // (hideProcessApplication) because a process application is no longer just
302
+ // another resource. Before that change the menu item was the only way in.
303
+ // Race both entry points so neither version pays a full timeout, then use
304
+ // whichever one rendered.
305
+ async clickCreateProcessApplication() {
306
+ const button = this.processApplicationPage.createProcessApplicationButton;
307
+ const rootLoaded = await this.becameVisible(button.or(this.diagramTypeDropdown).first(), 60000);
308
+ if (!rootLoaded) {
309
+ await this.page.reload();
310
+ }
311
+ // Short: the joint probe above already settled which branch rendered, and
312
+ // hub renders exactly one of them (EntityTable renders the toolbar's create
313
+ // menu OR the empty state, never both), so this only confirms which.
314
+ if (await this.becameVisible(button, 5000)) {
315
+ await this.processApplicationPage.clickCreateProcessApplicationButton();
316
+ return;
317
+ }
318
+ await this.clickWorkspaceDiagramTypeDropdown();
319
+ await this.processApplicationPage.clickCreateProcessApplicationOption();
320
+ }
321
+ // Re-enters this test's container if it already has one, without creating
322
+ // one: opening an existing resource must not have the side effect of
323
+ // creating a container for a spec that never asked for it.
324
+ async ensureInResourceContainer() {
325
+ if (!this.processApplicationUrl) {
326
+ return;
327
+ }
328
+ await this.enterResourceContainer();
329
+ }
330
+ // Navigates back to this test's process application by the remembered URL
331
+ // rather than by a breadcrumb click: hub renders no process-application
332
+ // breadcrumb on a diagram page at container root, and the breadcrumb that
333
+ // does exist on the container page doubles as its rename control. Hub's own
334
+ // Playwright suite navigates back the same way.
335
+ async returnToResourceContainer() {
336
+ if (!this.processApplicationUrl) {
337
+ await this.clickWorkspaceBreadcrumb();
338
+ return;
339
+ }
340
+ await this.page.goto(this.processApplicationUrl, {
341
+ waitUntil: 'domcontentloaded',
342
+ });
343
+ await this.processApplicationPage.waitForProcessApplication();
344
+ await this.dismissOverlays();
345
+ }
346
+ // Single seam for "give me an empty BPMN diagram in this test's container".
347
+ // A process application always ships an auto-created main process; this adds
348
+ // a new diagram rather than adopting that one, so every existing modelling
349
+ // assertion keeps its current subject.
350
+ async startBlankBpmnDiagram() {
351
+ await this.clickDiagramTypeDropdown();
352
+ await this.clickBpmnTemplateOption();
353
+ }
354
+ // Boolean visibility probe. waitFor rather than isVisible, which resolves
355
+ // immediately and would report a false negative on a still-rendering page.
356
+ async becameVisible(locator, timeout) {
357
+ return locator
358
+ .waitFor({ state: 'visible', timeout })
359
+ .then(() => true)
360
+ .catch(() => false);
199
361
  }
200
362
  async clickHomeBreadcrumb() {
201
363
  // The Console shell's HelpCenter panel ("Your recommendations") renders on
@@ -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 };