@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.
@@ -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;
@@ -509,6 +509,14 @@ async function modelAndRunConnectorsTimerEventDiagram(modelerCreatePage, modeler
509
509
  timeout: 60000,
510
510
  });
511
511
  await connectorMarketplacePage.downloadConnectorToProject();
512
+ // Both lookups are container-relative: clickProjectBreadcrumb() returns to
513
+ // this test's process application, which is where the uploaded diagram
514
+ // lives. The downloaded element template is the open question -- hub
515
+ // publishes element templates to the workspace, but the marketplace download
516
+ // is initiated from inside the container, so it should land there. If a
517
+ // trace shows the template at workspace root instead, this call becomes
518
+ // clickWorkspaceBreadcrumb() followed by returnToResourceContainer() before
519
+ // the diagram is reopened.
512
520
  await modelerHomePage.clickProjectBreadcrumb();
513
521
  await modelerHomePage.clickProcessDiagram('REST Outbound Connector');
514
522
  await connectorTemplatePage.publishConnector();
@@ -98,7 +98,9 @@ _8_7_1.test.describe('IDP User Flow Tests', () => {
98
98
  await modelerHomePage.clickCrossComponentProjectFolder();
99
99
  });
100
100
  await _8_7_1.test.step('Add an IDP application to the project', async () => {
101
- await modelerHomePage.clickDiagramTypeDropdown();
101
+ // Workspace-level menu: an IDP application can only be created at
102
+ // workspace root, never inside a process application.
103
+ await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
102
104
  await modelerHomePage.clickIdpApplicationTemplateOption();
103
105
  await modelerHomePage.enterIdpApplicationName(idpApplicationName);
104
106
  await modelerHomePage.selectCluster(clusterName);
@@ -129,7 +129,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
129
129
  await modelerCreatePage.clickAppendEndEventButton();
130
130
  await (0, sleep_1.sleep)(20000);
131
131
  await modelerCreatePage.runProcessInstance(clusterName);
132
- await (0, test_1.expect)(page.getByText('The diagram failed to deploy & run')).toBeVisible({ timeout: 60000 });
132
+ await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).toBeVisible({ timeout: 60000 });
133
133
  await modelerCreatePage.clickCloseButton();
134
134
  });
135
135
  });
@@ -219,7 +219,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
219
219
  await modelerCreatePage.clickAppendEndEventButton();
220
220
  await (0, sleep_1.sleep)(20000);
221
221
  await modelerCreatePage.runProcessInstance(clusterName, '', {}, '', true);
222
- await (0, test_1.expect)(page.getByText('The diagram failed to deploy & run')).toBeVisible({ timeout: 60000 });
222
+ await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).toBeVisible({ timeout: 60000 });
223
223
  await modelerCreatePage.clickCloseButton();
224
224
  });
225
225
  });
@@ -314,7 +314,7 @@ _8_8_1.test.describe('User Roles User Flow', () => {
314
314
  await modelerCreatePage.clickAppendEndEventButton();
315
315
  await (0, sleep_1.sleep)(20000);
316
316
  await modelerCreatePage.runProcessInstance(clusterName);
317
- await (0, test_1.expect)(page.getByText('The diagram failed to deploy & run')).not.toBeVisible({ timeout: 60000 });
317
+ await (0, test_1.expect)(page.getByText(/The (diagram|application) failed to deploy & run/)).not.toBeVisible({ timeout: 60000 });
318
318
  await (0, test_1.expect)(modelerCreatePage.viewProcessInstanceLink).toBeVisible({
319
319
  timeout: 120000,
320
320
  });
@@ -87,7 +87,9 @@ _8_8_1.test.describe('IDP User Flow Tests', () => {
87
87
  await modelerHomePage.clickCrossComponentProjectFolder();
88
88
  });
89
89
  await _8_8_1.test.step('Add an IDP application to the project', async () => {
90
- await modelerHomePage.clickDiagramTypeDropdown();
90
+ // Workspace-level menu: an IDP application can only be created at
91
+ // workspace root, never inside a process application.
92
+ await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
91
93
  await modelerHomePage.clickIdpApplicationTemplateOption();
92
94
  });
93
95
  await _8_8_1.test.step('Creates a document extraction template', async () => {
@@ -159,7 +161,9 @@ _8_8_1.test.describe('IDP User Flow Tests', () => {
159
161
  await modelerHomePage.clickCrossComponentProjectFolder();
160
162
  });
161
163
  await _8_8_1.test.step('Add an IDP application to the project', async () => {
162
- await modelerHomePage.clickDiagramTypeDropdown();
164
+ // Workspace-level menu: an IDP application can only be created at
165
+ // workspace root, never inside a process application.
166
+ await modelerHomePage.clickWorkspaceDiagramTypeDropdown();
163
167
  await modelerHomePage.clickIdpApplicationTemplateOption();
164
168
  });
165
169
  await _8_8_1.test.step('Creates a document extraction template', async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1014",
3
+ "version": "0.0.1016",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",