@camunda/e2e-test-suite 0.0.1250 → 0.0.1251

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.
Files changed (49) hide show
  1. package/dist/pages/BrowseAllConnectorMarketplacePage.d.ts +17 -0
  2. package/dist/pages/BrowseAllConnectorMarketplacePage.js +145 -0
  3. package/dist/pages/SM-8.10/ClusterDetailsPage.d.ts +1 -1
  4. package/dist/pages/SM-8.10/ClusterDetailsPage.js +13 -6
  5. package/dist/pages/SM-8.10/ClusterPage.js +3 -1
  6. package/dist/pages/SM-8.10/ConnectorMarketplacePage.d.ts +2 -0
  7. package/dist/pages/SM-8.10/ConnectorMarketplacePage.js +16 -1
  8. package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +4 -1
  9. package/dist/pages/SM-8.10/ModelerCreatePage.js +78 -51
  10. package/dist/pages/SM-8.10/ModelerHomePage.d.ts +2 -1
  11. package/dist/pages/SM-8.10/ModelerHomePage.js +24 -0
  12. package/dist/pages/SM-8.10/OperateHomePage.js +8 -1
  13. package/dist/pages/SM-8.10/OptimizeHomePage.js +13 -4
  14. package/dist/pages/SM-8.10/ProcessApplicationPage.d.ts +10 -5
  15. package/dist/pages/SM-8.10/ProcessApplicationPage.js +163 -68
  16. package/dist/pages/SM-8.10/UtilitiesPage.js +6 -6
  17. package/dist/pages/SM-8.7/NavigationPage.js +2 -1
  18. package/dist/tests/8.10/connectors-user-flows.spec.js +4 -2
  19. package/dist/tests/8.10/saas-migration-path-user-flows.spec.js +2 -1
  20. package/dist/tests/8.10/smoke-tests.spec.js +2 -1
  21. package/dist/tests/8.7/connectors-user-flows.spec.js +8 -4
  22. package/dist/tests/8.7/saas-migration-path-user-flows.spec.js +2 -1
  23. package/dist/tests/8.7/smoke-tests.spec.js +2 -1
  24. package/dist/tests/8.8/connectors-user-flows.spec.js +4 -2
  25. package/dist/tests/8.8/saas-migration-path-user-flows.spec.js +2 -1
  26. package/dist/tests/8.8/smoke-tests.spec.js +2 -1
  27. package/dist/tests/8.9/connectors-user-flows.spec.js +4 -2
  28. package/dist/tests/8.9/saas-migration-path-user-flows.spec.js +2 -1
  29. package/dist/tests/8.9/smoke-tests.spec.js +2 -1
  30. package/dist/tests/SM-8.10/cluster-management-user-flows.spec.js +13 -6
  31. package/dist/tests/SM-8.10/connectors-user-flows.spec.js +2 -1
  32. package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.d.ts +1 -0
  33. package/dist/tests/SM-8.10/hub-web-modeler-smoke.spec.js +24 -0
  34. package/dist/tests/SM-8.10/mt-enabled-user-flows.spec.js +3 -1
  35. package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.d.ts +1 -0
  36. package/dist/tests/SM-8.10/topology-orchestration-smoke.spec.js +58 -0
  37. package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.d.ts +1 -0
  38. package/dist/tests/SM-8.7/topology-orchestration-smoke.spec.js +62 -0
  39. package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.d.ts +1 -0
  40. package/dist/tests/SM-8.8/topology-orchestration-smoke.spec.js +58 -0
  41. package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.d.ts +1 -0
  42. package/dist/tests/SM-8.9/topology-orchestration-smoke.spec.js +58 -0
  43. package/dist/tests/c8Run-8.7/connectors-user-flows.spec.js +6 -3
  44. package/dist/tests/c8Run-8.8/connectors-user-flows.spec.js +6 -3
  45. package/dist/tests/c8Run-8.9/connectors-user-flows.spec.js +6 -3
  46. package/dist/utils/modelerCluster.d.ts +4 -6
  47. package/dist/utils/modelerCluster.js +55 -32
  48. package/package.json +1 -1
  49. package/playwright.config.ts +18 -5
@@ -0,0 +1,17 @@
1
+ import { Locator, Page } from '@playwright/test';
2
+ declare class BrowseAllConnectorMarketplacePage {
3
+ private page;
4
+ readonly searchForConnectorTextbox: Locator;
5
+ private readonly marketplaceConnectorList;
6
+ private readonly marketplaceConnectorCard;
7
+ private readonly marketplaceDetailFooter;
8
+ private readonly availableElementsHeading;
9
+ private readonly importModal;
10
+ constructor(page: Page);
11
+ isOpen(): Promise<boolean>;
12
+ waitForConnectorSearchResult(connectorName?: string): Promise<Locator>;
13
+ importConnectorToProject(connectorName?: string): Promise<void>;
14
+ private completeImportReviewIfNecessary;
15
+ private findConnectorCard;
16
+ }
17
+ export { BrowseAllConnectorMarketplacePage };
@@ -0,0 +1,145 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BrowseAllConnectorMarketplacePage = void 0;
4
+ class BrowseAllConnectorMarketplacePage {
5
+ page;
6
+ searchForConnectorTextbox;
7
+ marketplaceConnectorList;
8
+ marketplaceConnectorCard;
9
+ marketplaceDetailFooter;
10
+ availableElementsHeading;
11
+ importModal;
12
+ constructor(page) {
13
+ this.page = page;
14
+ this.searchForConnectorTextbox = page.getByRole('searchbox', {
15
+ name: 'Search elements and reusable assets',
16
+ });
17
+ this.marketplaceConnectorList = page.getByRole('list', {
18
+ name: 'Marketplace connectors',
19
+ });
20
+ this.marketplaceConnectorCard = this.marketplaceConnectorList
21
+ .getByRole('button', {
22
+ name: /^View details for .+ from Marketplace$/,
23
+ })
24
+ .first();
25
+ this.marketplaceDetailFooter = page.locator('[data-test="marketplace-detail-footer"]');
26
+ this.availableElementsHeading = page.getByRole('heading', {
27
+ name: 'Available elements',
28
+ });
29
+ this.importModal = page.locator('[data-test="import-modal"]');
30
+ }
31
+ async isOpen() {
32
+ return await this.searchForConnectorTextbox.isVisible();
33
+ }
34
+ async waitForConnectorSearchResult(connectorName) {
35
+ await this.marketplaceConnectorCard.waitFor({
36
+ state: 'visible',
37
+ timeout: 120000,
38
+ });
39
+ return await this.findConnectorCard(connectorName);
40
+ }
41
+ async importConnectorToProject(connectorName) {
42
+ const connectorCard = await this.waitForConnectorSearchResult(connectorName);
43
+ await connectorCard.click({ timeout: 60000 });
44
+ const browseAllDialog = this.page.getByRole('dialog').filter({
45
+ has: this.page.getByRole('button', { name: 'Back to all results' }),
46
+ });
47
+ await browseAllDialog.waitFor({ state: 'visible', timeout: 60000 });
48
+ const addButton = this.marketplaceDetailFooter.getByRole('button', {
49
+ name: /^(Add to project|Add remaining elements)$/,
50
+ });
51
+ const addedStatus = this.marketplaceDetailFooter.getByText('Added to this project', { exact: true });
52
+ const retryButton = this.marketplaceDetailFooter.getByRole('button', {
53
+ name: 'Try again',
54
+ });
55
+ const detailState = addButton.or(addedStatus).or(retryButton);
56
+ for (let attempt = 1; attempt <= 3; attempt++) {
57
+ await detailState.waitFor({ state: 'visible', timeout: 120000 });
58
+ if (await addedStatus.isVisible()) {
59
+ break;
60
+ }
61
+ if (await retryButton.isVisible()) {
62
+ if (attempt === 3) {
63
+ throw new Error('Browse All could not inspect the Marketplace connector after 3 attempts.');
64
+ }
65
+ await retryButton.click({ timeout: 60000 });
66
+ continue;
67
+ }
68
+ await addButton.click({ timeout: 60000 });
69
+ if (await this.completeImportReviewIfNecessary(retryButton)) {
70
+ break;
71
+ }
72
+ if (attempt === 3) {
73
+ throw new Error('Browse All could not import the Marketplace connector after 3 attempts.');
74
+ }
75
+ }
76
+ await this.availableElementsHeading.waitFor({
77
+ state: 'visible',
78
+ timeout: 120000,
79
+ });
80
+ await browseAllDialog
81
+ .getByRole('button', { name: 'Close' })
82
+ .click({ timeout: 60000 });
83
+ await browseAllDialog.waitFor({ state: 'hidden', timeout: 60000 });
84
+ }
85
+ async completeImportReviewIfNecessary(retryButton) {
86
+ await this.importModal
87
+ .or(this.availableElementsHeading)
88
+ .or(retryButton)
89
+ .waitFor({ state: 'visible', timeout: 120000 });
90
+ if (await retryButton.isVisible()) {
91
+ return false;
92
+ }
93
+ if (!(await this.importModal.isVisible())) {
94
+ return true;
95
+ }
96
+ const confirmButton = this.importModal
97
+ .getByRole('button', {
98
+ name: /^(Add to project|Replace resource|Save as copy)$/,
99
+ })
100
+ .first();
101
+ const confirmationVisible = await confirmButton
102
+ .waitFor({ state: 'visible', timeout: 10000 })
103
+ .then(() => true)
104
+ .catch(() => false);
105
+ if (confirmationVisible) {
106
+ await confirmButton.click({ timeout: 60000 });
107
+ }
108
+ await this.importModal.waitFor({ state: 'hidden', timeout: 120000 });
109
+ await this.availableElementsHeading
110
+ .or(retryButton)
111
+ .waitFor({ state: 'visible', timeout: 120000 });
112
+ return !(await retryButton.isVisible());
113
+ }
114
+ async findConnectorCard(connectorName) {
115
+ if (!connectorName) {
116
+ return this.marketplaceConnectorCard;
117
+ }
118
+ const exactCard = this.marketplaceConnectorList.getByRole('button', {
119
+ name: `View details for ${connectorName} from Marketplace`,
120
+ exact: true,
121
+ });
122
+ const searchWords = connectorName
123
+ .replace(/\s+connector$/i, '')
124
+ .trim()
125
+ .split(/\s+/)
126
+ .map(escapeRegExp)
127
+ .join('.*');
128
+ const relatedCard = this.marketplaceConnectorList
129
+ .getByRole('button', {
130
+ name: new RegExp(`^View details for .*${searchWords}.* from Marketplace$`, 'i'),
131
+ })
132
+ .first();
133
+ if (await exactCard.isVisible()) {
134
+ return exactCard;
135
+ }
136
+ if (await relatedCard.isVisible()) {
137
+ return relatedCard;
138
+ }
139
+ return this.marketplaceConnectorCard;
140
+ }
141
+ }
142
+ exports.BrowseAllConnectorMarketplacePage = BrowseAllConnectorMarketplacePage;
143
+ function escapeRegExp(value) {
144
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
145
+ }
@@ -1,7 +1,7 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class ClusterDetailsPage {
3
3
  private page;
4
- readonly customPropertiesHeading: Locator;
4
+ readonly clusterDetailsHeading: Locator;
5
5
  constructor(page: Page);
6
6
  assertCustomPropertyIsNavigated(linkTitle: string, linkName: string, link: string | RegExp): Promise<void>;
7
7
  }
@@ -4,21 +4,28 @@ exports.ClusterDetailsPage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
5
  class ClusterDetailsPage {
6
6
  page;
7
- customPropertiesHeading;
7
+ // Custom properties no longer have a heading/section of their own — the
8
+ // Environments/Clusters redesign folds them into the "Cluster details"
9
+ // card as rows (camunda-hub's ClusterDetailFields.sm.tsx), so this is the
10
+ // heading that confirms that card — and the properties inside it — rendered.
11
+ clusterDetailsHeading;
8
12
  constructor(page) {
9
13
  this.page = page;
10
- this.customPropertiesHeading = this.page.getByRole('heading', {
11
- name: 'Custom properties',
14
+ this.clusterDetailsHeading = this.page.getByRole('heading', {
15
+ name: 'Cluster details',
12
16
  });
13
17
  }
14
18
  async assertCustomPropertyIsNavigated(linkTitle, linkName, link) {
15
19
  const pageTabPromise = this.page.waitForEvent('popup', {
16
20
  timeout: 60000,
17
21
  });
22
+ // Each custom property is a <dt>/<dd> row (direct child of the details
23
+ // card's <dl>), keyed by the property's own description — not by a
24
+ // generic "Link 1"/"Link 2" row title, which this DOM never had.
18
25
  await this.page
19
- .getByRole('row')
20
- .filter({ hasText: linkTitle })
21
- .getByRole('link', { name: linkName })
26
+ .locator('dl > div')
27
+ .filter({ has: this.page.locator('dt', { hasText: linkTitle }) })
28
+ .getByRole('link', { name: linkName, exact: true })
22
29
  .click({ timeout: 60000 });
23
30
  const camundaPage = await pageTabPromise;
24
31
  await (0, test_1.expect)(camundaPage).toHaveURL(link);
@@ -9,7 +9,9 @@ class ClusterPage {
9
9
  cluster;
10
10
  constructor(page) {
11
11
  this.page = page;
12
- this.clustersList = page.locator('tbody').getByRole('row');
12
+ // The Clusters tab defaults to the card grid (`[data-test="cluster-card"]`),
13
+ // not a table — there is no `tbody` in that view.
14
+ this.clustersList = page.locator('[data-test="cluster-card"]');
13
15
  this.cluster = this.clustersList.getByText(this.defaultClusterName).first();
14
16
  }
15
17
  async clickClusterLink() {
@@ -1,6 +1,8 @@
1
1
  import { Page, Locator } from '@playwright/test';
2
2
  declare class ConnectorMarketplacePage {
3
3
  private page;
4
+ private browseAllConnectorMarketplacePage;
5
+ private connectorName?;
4
6
  readonly searchForConnectorTextbox: Locator;
5
7
  readonly downloadToProjectButton: Locator;
6
8
  readonly addToProjectButton: Locator;
@@ -2,9 +2,12 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ConnectorMarketplacePage = void 0;
4
4
  const test_1 = require("@playwright/test");
5
+ const BrowseAllConnectorMarketplacePage_1 = require("../BrowseAllConnectorMarketplacePage");
5
6
  const sleep_1 = require("../../utils/sleep");
6
7
  class ConnectorMarketplacePage {
7
8
  page;
9
+ browseAllConnectorMarketplacePage;
10
+ connectorName;
8
11
  searchForConnectorTextbox;
9
12
  downloadToProjectButton;
10
13
  addToProjectButton;
@@ -15,7 +18,10 @@ class ConnectorMarketplacePage {
15
18
  noResultsMessage;
16
19
  constructor(page) {
17
20
  this.page = page;
18
- this.searchForConnectorTextbox = page.getByPlaceholder('Search for a connector');
21
+ this.browseAllConnectorMarketplacePage =
22
+ new BrowseAllConnectorMarketplacePage_1.BrowseAllConnectorMarketplacePage(page);
23
+ this.searchForConnectorTextbox =
24
+ this.browseAllConnectorMarketplacePage.searchForConnectorTextbox.or(page.getByPlaceholder('Search for a connector'));
19
25
  this.downloadToProjectButton = page
20
26
  .getByRole('button', { name: 'Download to project' })
21
27
  .first();
@@ -35,6 +41,7 @@ class ConnectorMarketplacePage {
35
41
  await (0, sleep_1.sleep)(2000);
36
42
  }
37
43
  async fillSearchForConnectorTextbox(connectorName) {
44
+ this.connectorName = connectorName;
38
45
  await this.searchForConnectorTextbox.fill(connectorName, { timeout: 60000 });
39
46
  await (0, sleep_1.sleep)(2000);
40
47
  }
@@ -48,6 +55,10 @@ class ConnectorMarketplacePage {
48
55
  await this.replaceResourceButton.click({ timeout: 30000 });
49
56
  }
50
57
  async waitForConnectorSearchResults() {
58
+ if (await this.browseAllConnectorMarketplacePage.isOpen()) {
59
+ await this.browseAllConnectorMarketplacePage.waitForConnectorSearchResult(this.connectorName);
60
+ return;
61
+ }
51
62
  const MAX_RETRIES = 4;
52
63
  for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
53
64
  await (0, test_1.expect)(this.downloadToProjectButton
@@ -72,6 +83,10 @@ class ConnectorMarketplacePage {
72
83
  throw new Error('Marketplace connector search failed after retries due to persistent error or no matching results');
73
84
  }
74
85
  async downloadConnectorToProject() {
86
+ if (await this.browseAllConnectorMarketplacePage.isOpen()) {
87
+ await this.browseAllConnectorMarketplacePage.importConnectorToProject(this.connectorName);
88
+ return;
89
+ }
75
90
  await this.clickDownloadToProjectButton();
76
91
  if (await this.confirmImport()) {
77
92
  return;
@@ -2,6 +2,8 @@ import { Page, Locator } from '@playwright/test';
2
2
  declare class ModelerCreatePage {
3
3
  private page;
4
4
  private processApplicationPage;
5
+ private pendingFileSaves;
6
+ private lastFileSaveActivity;
5
7
  readonly generalPanel: Locator;
6
8
  readonly taskDefinitionPanel: Locator;
7
9
  readonly processIdInput: Locator;
@@ -36,7 +38,6 @@ declare class ModelerCreatePage {
36
38
  readonly restConnectorOption: Locator;
37
39
  readonly changeElementSearchInput: Locator;
38
40
  readonly changeElementResults: Locator;
39
- readonly changeElementTabStrip: Locator;
40
41
  readonly reusableAssetsTab: Locator;
41
42
  readonly marketPlaceButton: Locator;
42
43
  readonly clientIdTextbox: Locator;
@@ -106,6 +107,8 @@ declare class ModelerCreatePage {
106
107
  assertThreeElementsVisible(): Promise<void>;
107
108
  clickForm(name: string): Promise<void>;
108
109
  runProcessInstance(variables?: string, tenant?: string, businessId?: string): Promise<void>;
110
+ private static isFileSave;
111
+ private waitForFileSavesToSettle;
109
112
  private suppressTestModeAnnouncement;
110
113
  switchToPlay(): Promise<void>;
111
114
  clickGeneralPropertiesPanel(): Promise<void>;
@@ -9,6 +9,8 @@ const modelerCluster_1 = require("../../utils/modelerCluster");
9
9
  class ModelerCreatePage {
10
10
  page;
11
11
  processApplicationPage;
12
+ pendingFileSaves = 0;
13
+ lastFileSaveActivity = 0;
12
14
  generalPanel;
13
15
  taskDefinitionPanel;
14
16
  processIdInput;
@@ -43,7 +45,6 @@ class ModelerCreatePage {
43
45
  restConnectorOption;
44
46
  changeElementSearchInput;
45
47
  changeElementResults;
46
- changeElementTabStrip;
47
48
  reusableAssetsTab;
48
49
  marketPlaceButton;
49
50
  clientIdTextbox;
@@ -108,6 +109,23 @@ class ModelerCreatePage {
108
109
  constructor(page) {
109
110
  this.page = page;
110
111
  this.processApplicationPage = new ProcessApplicationPage_1.ProcessApplicationPage(page);
112
+ // Track the modeler's debounced autosave (PATCH /files/{id}) so a deploy
113
+ // can wait until the server holds the latest edits. See
114
+ // waitForFileSavesToSettle().
115
+ page.on('request', (request) => {
116
+ if (ModelerCreatePage.isFileSave(request)) {
117
+ this.pendingFileSaves++;
118
+ this.lastFileSaveActivity = Date.now();
119
+ }
120
+ });
121
+ const settleFileSave = (request) => {
122
+ if (ModelerCreatePage.isFileSave(request)) {
123
+ this.pendingFileSaves = Math.max(0, this.pendingFileSaves - 1);
124
+ this.lastFileSaveActivity = Date.now();
125
+ }
126
+ };
127
+ page.on('requestfinished', settleFileSave);
128
+ page.on('requestfailed', settleFileSave);
111
129
  this.generalPanel = page.locator('[data-group-id="group-general"]');
112
130
  this.taskDefinitionPanel = page
113
131
  .locator('div')
@@ -211,17 +229,17 @@ class ModelerCreatePage {
211
229
  .locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
212
230
  this.changeElementSearchInput = page.locator('.djs-popup-search input');
213
231
  this.changeElementResults = page.locator('.djs-popup-results');
214
- this.changeElementTabStrip = page.locator('.djs-popup-tabs-container');
215
232
  // The tab renders its label as text content, and the product source
216
233
  // spells it both "Reusable Assets" and "Reusable assets".
217
234
  this.reusableAssetsTab = page.getByRole('tab', {
218
235
  name: /reusable assets/i,
219
236
  });
220
- // A title query is ambiguous: when the filtered result list has no connector
221
- // match the popup also renders an empty-state link carrying the same title,
222
- // so the query resolves to two elements and clicking throws in strict mode.
223
- // Target the popup header entry by its stable entry id instead.
224
- this.marketPlaceButton = page.locator('[data-id="browseConnectors"]');
237
+ // Browse All replaces the legacy Marketplace entry only in large menus
238
+ // when the Design System is enabled; small or DS-off menus keep the
239
+ // original entry.
240
+ this.marketPlaceButton = page
241
+ .locator('[data-id="hub-browse-all"]')
242
+ .or(page.locator('[data-id="browseConnectors"]'));
225
243
  this.clientIdTextbox = page.getByLabel('Client ID');
226
244
  this.clientSecretTextbox = page.getByLabel('Client secret');
227
245
  this.rememberCredentialsCheckbox = page.getByText('Remember credentials');
@@ -455,9 +473,13 @@ class ModelerCreatePage {
455
473
  await legacyItem.click({ timeout: 90000 });
456
474
  }
457
475
  async runProcessInstance(variables = '', tenant = '', businessId = '') {
476
+ // ensureStageCluster may navigate away from the diagram, which would drop
477
+ // a save that is still debounced, so settle before and after it.
478
+ await this.waitForFileSavesToSettle();
458
479
  await this.processApplicationPage.ensureStageCluster(process.env.MODELER_CLUSTER_NAME);
459
- await this.page.waitForTimeout(2000); // the canvas autosaves. This can take a second to save.
480
+ await this.waitForFileSavesToSettle();
460
481
  const maxRetries = 3;
482
+ let executeSucceeded = false;
461
483
  for (let attempt = 0; attempt < maxRetries; attempt++) {
462
484
  try {
463
485
  await this.clickDeployAndRunMainButton();
@@ -482,9 +504,16 @@ class ModelerCreatePage {
482
504
  });
483
505
  await this.businessIdInput.fill(businessId);
484
506
  }
485
- await this.clickDeployAndRunSubButton();
486
- await this.instanceStartedAssertion();
487
- return;
507
+ const [executeResponse] = await Promise.all([
508
+ this.page.waitForResponse((response) => response.request().method() === 'POST' &&
509
+ // Workspace-era Hub builds execute via /v2/projects/{id}; older
510
+ // builds used /process-applications/{id}.
511
+ /\/(process-applications|projects)\/[^/]+\/execute$/.test(new URL(response.url()).pathname), { timeout: 30000 }),
512
+ this.clickDeployAndRunSubButton(),
513
+ ]);
514
+ (0, test_1.expect)(executeResponse.ok(), await executeResponse.text()).toBeTruthy();
515
+ executeSucceeded = true;
516
+ break;
488
517
  }
489
518
  catch (error) {
490
519
  if (attempt < maxRetries - 1) {
@@ -497,12 +526,33 @@ class ModelerCreatePage {
497
526
  }
498
527
  }
499
528
  }
529
+ if (executeSucceeded) {
530
+ await this.instanceStartedAssertion();
531
+ }
500
532
  }
501
533
  // Test mode mounts a one-time "A new way to test your processes" modal
502
534
  // (camunda-hub TestAnnouncementModal) that covers the setup steps behind it.
503
535
  // It renders unless localStorage already carries the seen flag, which it
504
536
  // reads on mount; a fresh Playwright context always starts empty, so set the
505
537
  // flag before entering Test mode rather than clicking the modal away.
538
+ static isFileSave(request) {
539
+ return (request.method() === 'PATCH' &&
540
+ /\/api\/internal\/files\/[^/]+$/.test(new URL(request.url()).pathname));
541
+ }
542
+ // Deploy & run deploys the diagram the server holds, not what is on screen.
543
+ // Hub debounces edits for roughly a second before it sends
544
+ // PATCH /files/{id}, and the autosave label keeps showing the previous
545
+ // "Autosaved at ..." time during that window, so the label cannot prove the
546
+ // latest edits are stored. Wait until no file save is in flight and none
547
+ // has started for longer than the debounce, measured from this call, so a
548
+ // save queued by edits made just before it is observed and awaited.
549
+ async waitForFileSavesToSettle(quietMs = 2000, timeout = 60000) {
550
+ const start = Date.now();
551
+ await test_1.expect
552
+ .poll(() => this.pendingFileSaves === 0 &&
553
+ Date.now() - Math.max(start, this.lastFileSaveActivity) >= quietMs, { timeout, intervals: [250] })
554
+ .toBe(true);
555
+ }
506
556
  async suppressTestModeAnnouncement() {
507
557
  await this.page.evaluate(() => localStorage.setItem('play-testAnnouncementSeen', 'true'));
508
558
  }
@@ -996,15 +1046,13 @@ class ModelerCreatePage {
996
1046
  // before bpmn-js attaches its search handler during the popup re-render.
997
1047
  async searchAndClickRestConnector() {
998
1048
  await (0, test_1.expect)(this.changeElementSearchInput).toBeVisible({ timeout: 30000 });
999
- await this.changeElementSearchInput.click();
1000
- await this.changeElementSearchInput.fill('');
1001
1049
  // 'Send REST Request' is the template's current name -- connectors #7917
1002
1050
  // renamed it from 'REST Outbound Connector' on 2026-07-15, and "Outbound"
1003
1051
  // now appears in no searchable field, so the old filter matched nothing.
1004
- await this.changeElementSearchInput.pressSequentially('Send REST Request', {
1005
- delay: 50,
1006
- });
1007
- await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout: 90000 });
1052
+ // Templates live under the popup's "Reusable assets" tab, so searching the
1053
+ // BPMN tab alone never lists them.
1054
+ await this.revealChangeElementTemplate(this.restConnectorOption, 'Send REST Request');
1055
+ await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout: 30000 });
1008
1056
  await this.restConnectorOption.click({ timeout: 90000 });
1009
1057
  }
1010
1058
  async clickRestConnectorOption(recoverSession) {
@@ -1106,38 +1154,10 @@ class ModelerCreatePage {
1106
1154
  name: /^Configure (test case|scenario)$/,
1107
1155
  }))
1108
1156
  .first();
1109
- // Play settles into exactly ONE of three states, and which one is not
1110
- // knowable up front, so race them instead of timing out on each in turn:
1111
- // the setup overlay, the legacy Continue button, or the configure-test
1112
- // panel (meaning setup is already done for this diagram).
1113
- //
1114
- // Waiting only for the first two and reading "neither" as "already ready"
1115
- // was wrong once the diagram moved inside a process application.
1116
- // ConfigurationOverlay renders null until isDeploymentStateValidated, and
1117
- // it sets that flag synchronously ONLY when processApplicationId is
1118
- // undefined -- exactly what a project-root diagram used to give it. With
1119
- // the diagram in a process application the id is defined, so the overlay
1120
- // must first await checkExistingDeployment(). The old 15s probe raced that
1121
- // async call, concluded "already ready" and returned while the overlay was
1122
- // still about to appear. ConfigureTestPanel returns null for as long as the
1123
- // overlay is up, so the Start button never existed and
1124
- // clickStartInstanceButton burned its entire retry budget looking for it.
1125
- // camunda-hub's "Play" -> "Test Studio" rebrand renamed this container's
1126
- // data-test from `play-configuration-overlay` to
1127
- // `test-studio-configuration-overlay`, and a later build dropped both
1128
- // container data-test attributes entirely (neither is present in the
1129
- // failing run's trace DOM), so those two gates now match nothing. The
1130
- // always-present anchor is the panel's heading, which the current build
1131
- // renders as "Set up environment" (verb, two words) -- the old
1132
- // `getByText('Setup environment')` (one word) never matched it and the
1133
- // whole chain timed out. Match the heading with a spelling-tolerant regex
1134
- // so a future flip between the two forms cannot re-break the gate, and keep
1135
- // the legacy data-test attributes for older modeler builds. This locator
1136
- // means "the setup panel is still on screen".
1137
1157
  const configurationOverlay = this.page
1138
1158
  .locator('[data-test="test-studio-configuration-overlay"]')
1139
1159
  .or(this.page.locator('[data-test="play-configuration-overlay"]'))
1140
- .or(this.page.getByText(/set ?up environment/i))
1160
+ .or(this.page.getByText(/set up test run/i))
1141
1161
  .first();
1142
1162
  const configureTestPanel = this.page.getByTestId('configure-test-panel');
1143
1163
  await (0, test_1.expect)(configurationOverlay
@@ -1156,18 +1176,23 @@ class ModelerCreatePage {
1156
1176
  if (isNewFlow) {
1157
1177
  // Step 1: connect cluster if not yet connected (modal with cluster dropdown)
1158
1178
  const connectClusterButton = this.page.getByRole('button', {
1159
- name: 'Connect cluster',
1179
+ name: 'Select environment',
1160
1180
  });
1161
1181
  const needsConnect = await connectClusterButton
1162
1182
  .isVisible({ timeout: 3000 })
1163
1183
  .catch(() => false);
1164
1184
  if (needsConnect) {
1165
1185
  await connectClusterButton.click({ timeout });
1186
+ // camunda-hub's TestModeDeployEndpointConfigModal replaced the
1187
+ // "Configure environment" combobox dialog with a "Choose where to
1188
+ // run" dialog: environments now render as a radio group (one radio
1189
+ // per environment), not a dropdown. The first environment is
1190
+ // auto-selected once the list loads, but click it explicitly rather
1191
+ // than relying on that default.
1166
1192
  const configureEnvDialog = this.page
1167
1193
  .getByRole('dialog')
1168
- .filter({ hasText: 'Configure environment' });
1169
- await configureEnvDialog.getByRole('combobox').click({ timeout });
1170
- await this.page.getByRole('option').first().click({ timeout });
1194
+ .filter({ hasText: 'Choose where to run' });
1195
+ await configureEnvDialog.getByRole('radio').first().click({ timeout });
1171
1196
  await configureEnvDialog
1172
1197
  .getByRole('button', { name: 'Save' })
1173
1198
  .click({ timeout });
@@ -1255,7 +1280,9 @@ class ModelerCreatePage {
1255
1280
  if (await this.isVisibleWithin(option, 5000)) {
1256
1281
  return true;
1257
1282
  }
1258
- if (await this.isVisibleWithin(this.changeElementTabStrip, 5000)) {
1283
+ // The tab strip's container class changed across Hub builds; probe the
1284
+ // "Reusable assets" tab itself instead of its wrapper.
1285
+ if (await this.isVisibleWithin(this.reusableAssetsTab, 5000)) {
1259
1286
  // A failed tab click leaves the search path intact, so do not throw.
1260
1287
  await this.reusableAssetsTab.click({ timeout: 30000 }).catch(() => { });
1261
1288
  if (await this.isVisibleWithin(option, 15000)) {
@@ -1,4 +1,4 @@
1
- import { Page, Locator } from '@playwright/test';
1
+ import { Browser, Page, Locator } from '@playwright/test';
2
2
  declare class ModelerHomePage {
3
3
  private page;
4
4
  private defaultFolderName;
@@ -57,6 +57,7 @@ declare class ModelerHomePage {
57
57
  username: string;
58
58
  password: string;
59
59
  }): Promise<void>;
60
+ assignWorkspaceEnvironmentAsOrgAdmin(browser: Browser): Promise<void>;
60
61
  clickCrossComponentProjectFolder(credentials?: {
61
62
  username: string;
62
63
  password: string;
@@ -349,6 +349,30 @@ class ModelerHomePage {
349
349
  // under flag-on that folder maps to a Workspace.
350
350
  await this.ensureWorkspaceOpen(credentials);
351
351
  }
352
+ // Only an org admin can assign Environments to a workspace (Hub 8.10), so a
353
+ // workspace created by a plain Member has nothing to deploy to until one does.
354
+ // Call while this page is inside that workspace: assigns an Environment as the
355
+ // default Identity first user (an org admin) in a separate browser context,
356
+ // leaving this page's session untouched.
357
+ async assignWorkspaceEnvironmentAsOrgAdmin(browser) {
358
+ const workspaceUrl = this.page.url();
359
+ const context = await browser.newContext({
360
+ baseURL: new URL(workspaceUrl).origin,
361
+ });
362
+ try {
363
+ const adminPage = await context.newPage();
364
+ await new NavigationPage_1.NavigationPage(adminPage).goToModeler();
365
+ await new ProcessApplicationPage_1.ProcessApplicationPage(adminPage).assignEnvironmentToWorkspace(workspaceUrl, process.env.MODELER_CLUSTER_NAME);
366
+ }
367
+ finally {
368
+ await context.close();
369
+ }
370
+ // This page loaded the workspace before the admin assigned the
371
+ // Environment in the other session, so Hub still holds the empty
372
+ // environment list and Deploy & run has no target until it refetches.
373
+ await this.page.reload({ waitUntil: 'domcontentloaded' });
374
+ this.processApplicationPage.markWorkspaceEnvironmentAssignedByAdmin(workspaceUrl);
375
+ }
352
376
  async clickCrossComponentProjectFolder(credentials) {
353
377
  // Kept name: callers use this to (re-)enter the shared Workspace's projects
354
378
  // listing after leaving Modeler. Creates the Workspace if it is somehow
@@ -119,7 +119,14 @@ class OperateHomePage {
119
119
  // yields `"updatedValue""` -- invalid JSON, and Apply rejects it exactly
120
120
  // like the per-character version did. Same recipe as
121
121
  // pages/c8Run-8.10/OperateHomePage.ts.
122
- await this.page.keyboard.press('ControlOrMeta+A');
122
+ //
123
+ // Select with Home + Shift+End rather than ControlOrMeta+A: headless
124
+ // Chromium on macOS does not apply Meta+A as select-all inside Monaco's
125
+ // native edit context, so the insert landed mid-string. The value is a
126
+ // single-line JSON string, so selecting the line selects the document on
127
+ // every platform.
128
+ await this.page.keyboard.press('Home');
129
+ await this.page.keyboard.press('Shift+End');
123
130
  await this.page.keyboard.insertText(`"${newValue}`);
124
131
  }
125
132
  async clickSaveVariableButton() {
@@ -52,10 +52,19 @@ class OptimizeHomePage {
52
52
  }
53
53
  async assertProcessHasBeenImported(processId) {
54
54
  await this.clickDashboardLink();
55
- await this.page.reload();
56
- await (0, test_1.expect)(this.page.getByRole('link', { name: processId })).toBeVisible({
57
- timeout: 180000,
58
- });
55
+ // The process list is paginated, so on a cluster with more than one page
56
+ // of processes the new one is not rendered until the list is filtered by
57
+ // name. Import is asynchronous, so reload and re-filter until it appears.
58
+ const search = this.page
59
+ .getByPlaceholder(/Search for name/i)
60
+ .or(this.page.getByPlaceholder('Filter table'))
61
+ .first();
62
+ const processLink = this.page.getByRole('link', { name: processId });
63
+ await (0, test_1.expect)(async () => {
64
+ await this.page.reload();
65
+ await search.fill(processId, { timeout: 30000 });
66
+ await (0, test_1.expect)(processLink).toBeVisible({ timeout: 15000 });
67
+ }).toPass({ timeout: 180000, intervals: [5000, 10000, 15000] });
59
68
  }
60
69
  async clickOpenUserSettingsButton() {
61
70
  await this.openUserSettingsButton.click();