@camunda/e2e-test-suite 0.0.970 → 0.0.971

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.
@@ -18,5 +18,6 @@ declare class ConnectorMarketplacePage {
18
18
  waitForConnectorSearchResults(): Promise<void>;
19
19
  downloadConnectorToProject(): Promise<void>;
20
20
  private confirmImport;
21
+ private waitForImportToSettle;
21
22
  }
22
23
  export { ConnectorMarketplacePage };
@@ -85,6 +85,9 @@ class ConnectorMarketplacePage {
85
85
  await this.cancelButton.click({ timeout: 20000 });
86
86
  throw new Error('Connector import modal offered no import action ("Add to project", "Replace resource" or "Save as copy"); the import was abandoned.');
87
87
  }
88
+ // Hub imported without asking for confirmation, so the write is in flight
89
+ // here too.
90
+ await this.waitForImportToSettle();
88
91
  }
89
92
  // Hub's import modal labels its primary button after the project state:
90
93
  // "Add to project" when the template is new to the project, "Replace
@@ -99,18 +102,53 @@ class ConnectorMarketplacePage {
99
102
  this.replaceResourceButton,
100
103
  this.saveAsCopyButton,
101
104
  ];
102
- const deadline = Date.now() + 20000;
105
+ // Carbon animates the modal in, so a click issued mid-transition is
106
+ // rejected ("element is not stable") and by the next Playwright retry the
107
+ // button can already be gone; keep probing the labels until one click
108
+ // actually lands instead of burning the whole budget on a single button.
109
+ const deadline = Date.now() + 45000;
103
110
  do {
104
111
  for (const button of importButtons) {
105
- if (await button.isVisible().catch(() => false)) {
106
- await button.click({ timeout: 20000 });
107
- await (0, test_1.expect)(button).toBeHidden({ timeout: 60000 });
108
- return true;
112
+ if (!(await button.isVisible().catch(() => false))) {
113
+ continue;
109
114
  }
115
+ // Register the waiter before the click: the write can be acknowledged
116
+ // before the modal finishes unmounting.
117
+ const templateWritten = this.page
118
+ .waitForResponse((response) => response.request().method() === 'POST' &&
119
+ response.url().includes('/internal/files'), { timeout: 30000 })
120
+ .catch(() => null);
121
+ const clicked = await button
122
+ .click({ timeout: 5000 })
123
+ .then(() => true)
124
+ .catch(() => false);
125
+ if (!clicked) {
126
+ continue;
127
+ }
128
+ await (0, test_1.expect)(button).toBeHidden({ timeout: 60000 });
129
+ const written = await templateWritten;
130
+ // A rejected write leaves the project without the template just like
131
+ // an aborted one, so surface the status here instead of letting it
132
+ // resurface 60s later as a missing "Authentication" group in
133
+ // ModelerCreatePage.
134
+ if (written && !written.ok()) {
135
+ throw new Error(`Connector template import failed: writing it into the project returned HTTP ${written.status()}.`);
136
+ }
137
+ await this.waitForImportToSettle();
138
+ return true;
110
139
  }
111
140
  await (0, sleep_1.sleep)(1000);
112
141
  } while (Date.now() < deadline);
113
142
  return false;
114
143
  }
144
+ // Hub unmounts the ImportModal as soon as the click is handled, but writes the
145
+ // template into the project with a POST that is still in flight at that point.
146
+ // Returning right after the button hides let the caller's `page.reload()`
147
+ // abort that write ~20ms later, leaving the project's connector-template list
148
+ // empty and every templated task on the diagram at "Template: Not found" --
149
+ // the properties panel then never renders the "Authentication" group.
150
+ async waitForImportToSettle() {
151
+ await this.page.waitForLoadState('networkidle').catch(() => { });
152
+ }
115
153
  }
116
154
  exports.ConnectorMarketplacePage = ConnectorMarketplacePage;
@@ -30,6 +30,8 @@ declare class ModelerCreatePage {
30
30
  readonly cancelButton: Locator;
31
31
  readonly restConnectorOption: Locator;
32
32
  readonly changeElementSearchInput: Locator;
33
+ readonly changeElementTabStrip: Locator;
34
+ readonly reusableAssetsTab: Locator;
33
35
  readonly marketPlaceButton: Locator;
34
36
  readonly clientIdTextbox: Locator;
35
37
  readonly clientSecretTextbox: Locator;
@@ -35,6 +35,8 @@ class ModelerCreatePage {
35
35
  cancelButton;
36
36
  restConnectorOption;
37
37
  changeElementSearchInput;
38
+ changeElementTabStrip;
39
+ reusableAssetsTab;
38
40
  marketPlaceButton;
39
41
  clientIdTextbox;
40
42
  clientSecretTextbox;
@@ -156,6 +158,12 @@ class ModelerCreatePage {
156
158
  .locator('.djs-popup-results')
157
159
  .locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
158
160
  this.changeElementSearchInput = page.locator('.djs-popup-search input');
161
+ this.changeElementTabStrip = page.locator('.djs-popup-tabs-container');
162
+ // The tab renders its label as text content, and the product source
163
+ // spells it both "Reusable Assets" and "Reusable assets".
164
+ this.reusableAssetsTab = page.getByRole('tab', {
165
+ name: /reusable assets/i,
166
+ });
159
167
  // A title query is ambiguous: when the filtered result list has no connector
160
168
  // match the popup also renders an empty-state link carrying the same title,
161
169
  // so the query resolves to two elements and clicking throws in strict mode.
@@ -965,16 +973,44 @@ class ModelerCreatePage {
965
973
  }
966
974
  }
967
975
  }
968
- // Connector templates nest under the change-element popup's "Templates"
969
- // category and every entry now renders a description, so filter the popup by
970
- // name to surface the entry as a flat search result before clicking it.
976
+ // Connector templates now live in the popup's "Reusable assets" tab, not
977
+ // the default "BPMN" one, so they do not render until either that tab is
978
+ // selected or a search is active (search spans every tab and flattens the
979
+ // result list).
980
+ // Use pressSequentially (not fill): the popup syncs its search state from a
981
+ // keyup listener only, and fill() dispatches just an input event, so the
982
+ // filter never applies and the unfiltered BPMN tab keeps rendering.
971
983
  async filterChangeElementPopup(searchTerm) {
984
+ // Two independent paths put the template in the DOM, and neither is
985
+ // reliable alone: selecting the tab renders the template entries
986
+ // directly, while searching spans every tab and flattens the result
987
+ // list. Run 32243588212 showed the search registering (the tab strip
988
+ // hid) with the template still absent from .djs-popup-results, so the
989
+ // tab click runs first and the search backs it up.
990
+ const hasTabs = await this.changeElementTabStrip
991
+ .isVisible({ timeout: 5000 })
992
+ .catch(() => false);
993
+ if (hasTabs) {
994
+ // A failed tab click leaves the search path intact, so do not throw.
995
+ await this.reusableAssetsTab.click({ timeout: 30000 }).catch(() => { });
996
+ }
972
997
  const hasSearch = await this.changeElementSearchInput
973
998
  .isVisible({ timeout: 5000 })
974
999
  .catch(() => false);
975
- if (hasSearch) {
976
- await this.changeElementSearchInput.fill(searchTerm);
1000
+ if (!hasSearch) {
1001
+ return;
977
1002
  }
1003
+ await this.changeElementSearchInput.click();
1004
+ await this.changeElementSearchInput.fill('');
1005
+ await this.changeElementSearchInput.pressSequentially(searchTerm, {
1006
+ delay: 50,
1007
+ });
1008
+ // The popup hides its tab strip while searching. Wait on that as a
1009
+ // readiness signal, but do not assert it: with the tab already selected
1010
+ // the template is reachable whether or not the search registered.
1011
+ await (0, test_1.expect)(this.changeElementTabStrip)
1012
+ .toBeHidden({ timeout: 30000 })
1013
+ .catch(() => { });
978
1014
  }
979
1015
  async clickWebhookMessageStartEventConnectorOption() {
980
1016
  const maxRetries = 3;
@@ -988,6 +1024,11 @@ class ModelerCreatePage {
988
1024
  }
989
1025
  else {
990
1026
  await this.page.reload();
1027
+ // A reload drops the selection, so re-select the start event before
1028
+ // reopening the popup -- the context pad only renders for a selected
1029
+ // element and "Change element" would otherwise never appear.
1030
+ await (0, test_1.expect)(this.startEventElement).toBeVisible({ timeout: 60000 });
1031
+ await this.startEventElement.click({ force: true, timeout: 30000 });
991
1032
  await this.changeTypeButton.click({ force: true, timeout: 30000 });
992
1033
  await this.filterChangeElementPopup('Webhook Message Start Event Connector');
993
1034
  await this.webhookMessageStartEventConnectorOption.click({
@@ -1168,14 +1209,10 @@ class ModelerCreatePage {
1168
1209
  const max = 3;
1169
1210
  for (let i = 0; i < max; i++) {
1170
1211
  try {
1171
- // Filter the popup to surface the webhook connector. The full connector
1172
- // list can be long and the specific entry may be below the viewport or
1173
- // not yet populated when the popup first opens.
1174
- if (await this.changeElementSearchInput
1175
- .isVisible({ timeout: 3000 })
1176
- .catch(() => false)) {
1177
- await this.changeElementSearchInput.fill('Webhook');
1178
- }
1212
+ // Filter the popup to surface the webhook connector: it sits in the
1213
+ // "Reusable assets" tab, which the popup only flattens into the result
1214
+ // list while a search is active.
1215
+ await this.filterChangeElementPopup('Webhook');
1179
1216
  await (0, test_1.expect)(this.intermediateWebhookConnectorOption).toBeVisible({
1180
1217
  timeout: 30000,
1181
1218
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camunda/e2e-test-suite",
3
- "version": "0.0.970",
3
+ "version": "0.0.971",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",