@camunda/e2e-test-suite 0.0.1027 → 0.0.1029

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.
@@ -194,7 +194,26 @@ class FormJsPage {
194
194
  await (0, test_1.expect)(this.dialog).toBeVisible({
195
195
  timeout: 30000,
196
196
  });
197
- await this.selectCluster(clusterName);
197
+ // Same recovery as ModelerCreatePage.runProcessInstance: selectCluster
198
+ // clears the stage binding when the dialog does not offer the cluster, so
199
+ // ensureStageCluster rebinds here and reports that it navigated. Any other
200
+ // failure leaves the binding in place, it returns false, and the original
201
+ // error stands. This path has no surrounding retry loop, hence the one
202
+ // explicit second attempt.
203
+ try {
204
+ await this.selectCluster(clusterName);
205
+ }
206
+ catch (error) {
207
+ const rebound = await this.processApplicationPage.ensureStageCluster(clusterName);
208
+ if (!rebound) {
209
+ throw error;
210
+ }
211
+ await this.clickDeployButton();
212
+ await (0, test_1.expect)(this.dialog).toBeVisible({
213
+ timeout: 30000,
214
+ });
215
+ await this.selectCluster(clusterName);
216
+ }
198
217
  await this.selectOnlyThisResource();
199
218
  await this.clickDeploySubButton();
200
219
  await (0, test_1.expect)(this.deploySuccessNotification).toBeVisible({ timeout: 30000 });
@@ -98,7 +98,10 @@ class ModelerHomePage {
98
98
  this.uploadFilesButton = page.getByRole('menuitem', { name: 'Upload files' });
99
99
  this.dialog = page.getByRole('dialog');
100
100
  this.messageBanner = page.locator('[data-test="close-top-banner"]');
101
- this.closeButton = page.getByRole('button', { name: 'Got it - Dismiss' });
101
+ // Matched on the shared "Got it" prefix so it covers both announcement
102
+ // dialogs hub currently ships: the release notes one ("Got it - Dismiss")
103
+ // and the process-application restructure one ("Got it").
104
+ this.closeButton = page.getByRole('button', { name: 'Got it' });
102
105
  this.optionsButton = page.getByRole('button', { name: 'Options' });
103
106
  this.formNameBreadcrumb = (formName) => page.locator('button[data-test="breadcrumb-form"]', {
104
107
  hasText: formName,
@@ -106,16 +109,31 @@ class ModelerHomePage {
106
109
  this.formBreadcrumbButton = page.locator('button[data-test="breadcrumb-form"]');
107
110
  this.renameFormMenuItem = page.locator('[data-test="rename-form"]');
108
111
  }
112
+ // The release-announcement dialog ("Check out our new release") re-mounts
113
+ // after the projects list re-renders, so one dismissal pass is not enough.
114
+ // Repeat while a pass still finds something to dismiss; a pass that finds
115
+ // nothing returns immediately, so the cost is unchanged when no overlay is up.
109
116
  async dismissOverlays() {
110
- for (const locator of [
111
- this.closeButton,
112
- this.page.getByRole('button', { name: 'Close' }).first(),
113
- this.messageBanner,
114
- ]) {
115
- await locator
116
- .waitFor({ state: 'visible', timeout: 3000 })
117
- .then(() => locator.click())
118
- .catch(() => { });
117
+ for (let pass = 0; pass < 3; pass++) {
118
+ let dismissed = false;
119
+ for (const locator of [
120
+ this.closeButton,
121
+ this.page.getByRole('button', { name: 'Close' }).first(),
122
+ this.messageBanner,
123
+ ]) {
124
+ const visible = await locator
125
+ .waitFor({ state: 'visible', timeout: 3000 })
126
+ .then(() => true)
127
+ .catch(() => false);
128
+ if (!visible) {
129
+ continue;
130
+ }
131
+ await locator.click({ timeout: 10000 }).catch(() => { });
132
+ dismissed = true;
133
+ }
134
+ if (!dismissed) {
135
+ return;
136
+ }
119
137
  }
120
138
  }
121
139
  async clickCreateNewProjectButton() {
@@ -127,10 +145,39 @@ class ModelerHomePage {
127
145
  visibilityTimeout: 10000,
128
146
  });
129
147
  }
148
+ // The rename runs on a freshly created project, which is exactly when an
149
+ // announcement dialog mounts on top of it: it takes focus, the inline editor
150
+ // unmounts, and a single click on `[data-test="editable-input"]` then waits
151
+ // out its whole timeout on an element that never comes back — leaving the
152
+ // project named "New project". Dismiss overlays and re-enter the name until
153
+ // it commits.
154
+ //
155
+ // The committed name renders differently per page: the project page puts it
156
+ // in the breadcrumb menu button, and only the projects list carries a
157
+ // `[title="<name>"]` link. Accept either, and once the rename has been
158
+ // submitted treat the editor unmounting as done — hub keeps rendering the
159
+ // pre-rename fetch until a reload, which the caller's Home-page assertion
160
+ // already performs.
130
161
  async enterNewProjectName(name) {
131
- await this.projectNameInput.click({ timeout: 60000 });
132
- await this.projectNameInput.fill(name);
133
- await this.projectNameInput.press('Enter');
162
+ const committedName = this.page
163
+ .locator('[data-test="breadcrumb-project-menu"]', { hasText: name })
164
+ .or(this.page.getByTitle(name, { exact: true }))
165
+ .first();
166
+ let submitted = false;
167
+ await (0, test_1.expect)(async () => {
168
+ if (await committedName.isVisible()) {
169
+ return;
170
+ }
171
+ if (submitted && !(await this.projectNameInput.isVisible())) {
172
+ return;
173
+ }
174
+ await this.dismissOverlays();
175
+ await this.projectNameInput.click({ timeout: 15000 });
176
+ await this.projectNameInput.fill(name);
177
+ submitted = true;
178
+ await this.projectNameInput.press('Enter');
179
+ await (0, test_1.expect)(committedName).toBeVisible({ timeout: 10000 });
180
+ }).toPass({ timeout: 120000 });
134
181
  }
135
182
  async enterIdpApplicationName(name) {
136
183
  await this.idpApplicationNameInput.click({ timeout: 60000 });
@@ -378,12 +425,17 @@ class ModelerHomePage {
378
425
  await this.page.getByText('Home', { exact: true }).first().click();
379
426
  }
380
427
  }
428
+ // An announcement dialog covering the Modeler header makes every click on the
429
+ // org button bounce off the modal layer ("intercepts pointer events") until
430
+ // the timeout, so dismiss overlays before each attempt.
381
431
  async clickOpenOrganizationsButton() {
382
- await (0, sleep_1.sleep)(5000);
383
- await (0, test_1.expect)(this.openOrganizationsButton).toBeVisible({
384
- timeout: 10000,
432
+ await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.openOrganizationsButton, {
433
+ preAction: async () => {
434
+ await this.dismissOverlays();
435
+ },
436
+ totalTimeout: 120000,
437
+ visibilityTimeout: 30000,
385
438
  });
386
- await this.openOrganizationsButton.click({ timeout: 30000 });
387
439
  }
388
440
  async clickCrossComponentProjectFolder() {
389
441
  await (0, clickLocatorWithRetry_1.clickLocatorWithRetry)(this.page, this.crossComponentProjectFolder, {
@@ -160,6 +160,19 @@ class ProcessApplicationPage {
160
160
  .filter({ hasNotText: 'Select a development cluster' })
161
161
  .first();
162
162
  await option.click({ timeout: 30000 });
163
+ // The field defaults to the first cluster alphabetically and re-mounts as
164
+ // the cluster list settles, so an option click can be dropped and Save
165
+ // then persists that default. Nothing downstream re-reads the binding --
166
+ // ensureStageCluster records the requested cluster as fact -- so the
167
+ // symptom surfaces much later, as a deploy dialog offering a cluster the
168
+ // test never asked for. Confirm the field carries the chosen cluster
169
+ // before saving; ensureStageCluster's retry re-navigates and binds again
170
+ // when it does not.
171
+ if (clusterName) {
172
+ await (0, test_1.expect)(this.developmentClusterSelect).toContainText(clusterName, {
173
+ timeout: 15000,
174
+ });
175
+ }
163
176
  await this.defineStagesSaveButton.click({ timeout: 30000 });
164
177
  await (0, test_1.expect)(this.defineStagesDialog).toBeHidden({ timeout: 60000 });
165
178
  }
@@ -159,7 +159,10 @@ declare class ModelerCreatePage {
159
159
  clickMarketPlaceButton(): Promise<void>;
160
160
  completeDeploymentEndpointConfiguration(): Promise<void>;
161
161
  completePlayConfiguration(): Promise<void>;
162
- filterChangeElementPopup(searchTerm: string): Promise<void>;
162
+ revealChangeElementTemplate(option: Locator, searchTerm: string): Promise<boolean>;
163
+ private isVisibleWithin;
164
+ private openChangeElementPopup;
165
+ private clickChangeElementTemplateOption;
163
166
  clickWebhookMessageStartEventConnectorOption(): Promise<void>;
164
167
  clickWebhookIdInput(): Promise<void>;
165
168
  clearWebhookIdInput(): Promise<void>;
@@ -1128,76 +1128,93 @@ class ModelerCreatePage {
1128
1128
  }
1129
1129
  }
1130
1130
  }
1131
- // Connector templates now live in the popup's "Reusable assets" tab, not
1132
- // the default "BPMN" one, so they do not render until either that tab is
1133
- // selected or a search is active (search spans every tab and flattens the
1134
- // result list).
1131
+ // Two independent paths put a connector template into the popup's queryable
1132
+ // DOM slice, and neither is reliable alone: selecting the "Reusable assets"
1133
+ // tab renders the template entries directly, while searching flattens the
1134
+ // result list. Probe the option after each step and stop at the first one
1135
+ // that worked, then keep re-issuing the filter while the popup stays open:
1136
+ // the project's connector-template list is fetched asynchronously, so a
1137
+ // filter typed before it lands renders "0 results found" and a single typed
1138
+ // filter never picks the template up afterwards.
1135
1139
  // Use pressSequentially (not fill): the popup syncs its search state from a
1136
1140
  // keyup listener only, and fill() dispatches just an input event, so the
1137
1141
  // filter never applies and the unfiltered BPMN tab keeps rendering.
1138
- async filterChangeElementPopup(searchTerm) {
1139
- // Two independent paths put the template in the DOM, and neither is
1140
- // reliable alone: selecting the tab renders the template entries
1141
- // directly, while searching spans every tab and flattens the result
1142
- // list. Run 32243588212 showed the search registering (the tab strip
1143
- // hid) with the template still absent from .djs-popup-results, so the
1144
- // tab click runs first and the search backs it up.
1145
- const hasTabs = await this.changeElementTabStrip
1146
- .isVisible({ timeout: 5000 })
1147
- .catch(() => false);
1148
- if (hasTabs) {
1142
+ async revealChangeElementTemplate(option, searchTerm) {
1143
+ if (await this.isVisibleWithin(option, 5000)) {
1144
+ return true;
1145
+ }
1146
+ if (await this.isVisibleWithin(this.changeElementTabStrip, 5000)) {
1149
1147
  // A failed tab click leaves the search path intact, so do not throw.
1150
1148
  await this.reusableAssetsTab.click({ timeout: 30000 }).catch(() => { });
1149
+ if (await this.isVisibleWithin(option, 15000)) {
1150
+ return true;
1151
+ }
1151
1152
  }
1152
- const hasSearch = await this.changeElementSearchInput
1153
- .isVisible({ timeout: 5000 })
1153
+ if (!(await this.isVisibleWithin(this.changeElementSearchInput, 5000))) {
1154
+ return false;
1155
+ }
1156
+ const deadline = Date.now() + 90000;
1157
+ do {
1158
+ await this.changeElementSearchInput.click();
1159
+ await this.changeElementSearchInput.fill('');
1160
+ await this.changeElementSearchInput.pressSequentially(searchTerm, {
1161
+ delay: 50,
1162
+ });
1163
+ if (await this.isVisibleWithin(option, 15000)) {
1164
+ return true;
1165
+ }
1166
+ } while (Date.now() < deadline);
1167
+ return false;
1168
+ }
1169
+ async isVisibleWithin(locator, timeout) {
1170
+ return locator
1171
+ .waitFor({ state: 'visible', timeout })
1172
+ .then(() => true)
1154
1173
  .catch(() => false);
1155
- if (!hasSearch) {
1174
+ }
1175
+ // The specs reach the Marketplace through this popup's own empty-state link,
1176
+ // and the import modal closes the popup behind it, so the popup is routinely
1177
+ // gone by the time a template is clicked. Reopen it against the element being
1178
+ // replaced first: filtering a closed popup finds no search input and the
1179
+ // option assertion then times out against a popup that was never on screen.
1180
+ async openChangeElementPopup(element) {
1181
+ if (await this.isVisibleWithin(this.changeElementSearchInput, 5000)) {
1156
1182
  return;
1157
1183
  }
1158
- await this.changeElementSearchInput.click();
1159
- await this.changeElementSearchInput.fill('');
1160
- await this.changeElementSearchInput.pressSequentially(searchTerm, {
1161
- delay: 50,
1162
- });
1163
- // The popup hides its tab strip while searching. Wait on that as a
1164
- // readiness signal, but do not assert it: with the tab already selected
1165
- // the template is reachable whether or not the search registered.
1166
- await (0, test_1.expect)(this.changeElementTabStrip)
1167
- .toBeHidden({ timeout: 30000 })
1168
- .catch(() => { });
1184
+ // The context pad only renders for a selected element, so "Change element"
1185
+ // would otherwise never appear.
1186
+ await (0, test_1.expect)(element).toBeVisible({ timeout: 60000 });
1187
+ await element.click({ force: true, timeout: 30000 });
1188
+ await this.changeTypeButton.click({ force: true, timeout: 30000 });
1169
1189
  }
1170
- async clickWebhookMessageStartEventConnectorOption() {
1190
+ async clickChangeElementTemplateOption(option, element, searchTerm) {
1171
1191
  const maxRetries = 3;
1192
+ let lastError;
1172
1193
  for (let retries = 0; retries < maxRetries; retries++) {
1173
1194
  try {
1174
- if (retries === 0) {
1175
- await this.filterChangeElementPopup('Webhook Message Start Event Connector');
1176
- await this.webhookMessageStartEventConnectorOption.click({
1177
- timeout: 30000,
1178
- });
1179
- }
1180
- else {
1195
+ if (retries > 0) {
1181
1196
  await this.page.reload();
1182
- // A reload drops the selection, so re-select the start event before
1183
- // reopening the popup -- the context pad only renders for a selected
1184
- // element and "Change element" would otherwise never appear.
1185
- await (0, test_1.expect)(this.startEventElement).toBeVisible({ timeout: 60000 });
1186
- await this.startEventElement.click({ force: true, timeout: 30000 });
1187
- await this.changeTypeButton.click({ force: true, timeout: 30000 });
1188
- await this.filterChangeElementPopup('Webhook Message Start Event Connector');
1189
- await this.webhookMessageStartEventConnectorOption.click({
1190
- timeout: 90000,
1191
- });
1197
+ // domcontentloaded is more reliable than networkidle for Modeler,
1198
+ // which maintains persistent background connections (SSE/WebSocket).
1199
+ await this.page.waitForLoadState('domcontentloaded');
1192
1200
  }
1201
+ await this.openChangeElementPopup(element);
1202
+ if (!(await this.revealChangeElementTemplate(option, searchTerm))) {
1203
+ throw new Error('The connector template never appeared in the change-element popup.');
1204
+ }
1205
+ await option.click({ timeout: 30000 });
1193
1206
  return;
1194
1207
  }
1195
1208
  catch (error) {
1209
+ lastError = error;
1196
1210
  console.error(`Click attempt ${retries + 1} failed: ${error}`);
1197
- await new Promise((resolve) => setTimeout(resolve, 10000));
1211
+ await (0, sleep_1.sleep)(10000);
1198
1212
  }
1199
1213
  }
1200
- throw new Error(`Failed to click the button after ${maxRetries} attempts.`);
1214
+ throw new Error(`Failed to click the connector template after ${maxRetries} attempts: ${lastError}`);
1215
+ }
1216
+ async clickWebhookMessageStartEventConnectorOption() {
1217
+ await this.clickChangeElementTemplateOption(this.webhookMessageStartEventConnectorOption, this.startEventElement, 'Webhook');
1201
1218
  }
1202
1219
  async clickWebhookIdInput() {
1203
1220
  await this.webhookIdInput.click();
@@ -1354,33 +1371,7 @@ class ModelerCreatePage {
1354
1371
  await this.appendViaPopup('append-none-intermediate-throwing', ['append-events', 'append-intermediate-throw-events'], 'Intermediate throw event');
1355
1372
  }
1356
1373
  async clickIntermediateWebhookConnectorOption() {
1357
- const max = 3;
1358
- for (let i = 0; i < max; i++) {
1359
- try {
1360
- // Filter the popup to surface the webhook connector: it sits in the
1361
- // "Reusable assets" tab, which the popup only flattens into the result
1362
- // list while a search is active.
1363
- await this.filterChangeElementPopup('Webhook');
1364
- await (0, test_1.expect)(this.intermediateWebhookConnectorOption).toBeVisible({
1365
- timeout: 30000,
1366
- });
1367
- await this.intermediateWebhookConnectorOption.click({ timeout: 30000 });
1368
- return;
1369
- }
1370
- catch (err) {
1371
- console.error(`clickIntermediateWebhookConnectorOption attempt ${i + 1} failed: ${err}`);
1372
- if (i === max - 1)
1373
- throw err;
1374
- await this.page.reload();
1375
- // domcontentloaded is more reliable than networkidle for Modeler
1376
- // which maintains persistent background connections (SSE/WebSocket).
1377
- await this.page.waitForLoadState('domcontentloaded');
1378
- // Wait for modeler canvas to render after reload before re-selecting
1379
- await (0, test_1.expect)(this.secondElement).toBeVisible({ timeout: 60000 });
1380
- await this.secondElement.click({ force: true, timeout: 30000 });
1381
- await this.changeTypeButton.click({ force: true, timeout: 30000 });
1382
- }
1383
- }
1374
+ await this.clickChangeElementTemplateOption(this.intermediateWebhookConnectorOption, this.secondElement, 'Webhook');
1384
1375
  }
1385
1376
  async clickCorrelationKeyProcessInput() {
1386
1377
  await this.correlationKeyProcessInput.click({ timeout: 60000 });
@@ -98,7 +98,7 @@ SM_8_10_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
98
98
  await modelerCreatePage.clickStartEventElement();
99
99
  await modelerCreatePage.clickChangeTypeButton();
100
100
  try {
101
- await modelerCreatePage.filterChangeElementPopup('Webhook Message Start Event Connector');
101
+ await modelerCreatePage.revealChangeElementTemplate(modelerCreatePage.webhookMessageStartEventConnectorOption, 'Webhook');
102
102
  await (0, test_1.expect)(modelerCreatePage.webhookMessageStartEventConnectorOption).toBeVisible({
103
103
  timeout: 15000,
104
104
  });
@@ -212,7 +212,9 @@ SM_8_10_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
212
212
  // Probe for the template this flow needs: an intermediate event's
213
213
  // change-element list never offers the message start event connector,
214
214
  // so probing for that one always fell through to the marketplace.
215
- await modelerCreatePage.filterChangeElementPopup('Webhook Intermediate Event Connector');
215
+ // 'Webhook' is the filter both webhook flows share; the entry is then
216
+ // picked by its template id, so a broader term cannot mis-select.
217
+ await modelerCreatePage.revealChangeElementTemplate(modelerCreatePage.intermediateWebhookConnectorOption, 'Webhook');
216
218
  await (0, test_1.expect)(modelerCreatePage.intermediateWebhookConnectorOption).toBeVisible({
217
219
  timeout: 15000,
218
220
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.1027",
3
+ "version": "0.0.1029",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",