@camunda/e2e-test-suite 0.0.970 → 0.0.972

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.
@@ -25,6 +25,9 @@ declare class ModelerCreatePage {
25
25
  readonly additionalActionsButton: Locator;
26
26
  readonly cancelButton: Locator;
27
27
  readonly restConnectorOption: Locator;
28
+ readonly changeElementSearchInput: Locator;
29
+ readonly changeElementTabStrip: Locator;
30
+ readonly reusableAssetsTab: Locator;
28
31
  readonly webhookMessageStartEventConnectorOption: Locator;
29
32
  readonly webhookTab: Locator;
30
33
  readonly webhookIsActiveButton: Locator;
@@ -156,6 +159,8 @@ declare class ModelerCreatePage {
156
159
  clickNewForm(): Promise<void>;
157
160
  clickDeploySubButton(): Promise<void>;
158
161
  clickCancelButton(): Promise<void>;
162
+ private waitForVisible;
163
+ revealChangeElementTemplate(option: Locator, searchTerm: string): Promise<void>;
159
164
  clickRestConnectorOption(): Promise<void>;
160
165
  clickWebhookStartEventConnectorOption(): Promise<void>;
161
166
  clickWebhookTab(): Promise<void>;
@@ -31,6 +31,9 @@ class ModelerCreatePage {
31
31
  additionalActionsButton;
32
32
  cancelButton;
33
33
  restConnectorOption;
34
+ changeElementSearchInput;
35
+ changeElementTabStrip;
36
+ reusableAssetsTab;
34
37
  webhookMessageStartEventConnectorOption;
35
38
  webhookTab;
36
39
  webhookIsActiveButton;
@@ -170,6 +173,17 @@ class ModelerCreatePage {
170
173
  this.deploySubButton = page.locator('[data-test="deploy-action"]');
171
174
  this.cancelButton = page.getByRole('button', { name: 'Cancel' });
172
175
  this.restConnectorOption = page.locator('[data-id="replace.template-io.camunda.connectors.HttpJson.v2"]');
176
+ this.changeElementSearchInput = page.locator('.djs-popup-search input');
177
+ this.changeElementTabStrip = page.locator('.djs-popup-tabs-container');
178
+ // The tab renders its label as text content, which wins over its `title`
179
+ // attribute in the accessible-name computation. Match case-insensitively:
180
+ // the label is capitalised differently across bpmn-js releases. Scope the
181
+ // lookup to the popup's own tab strip -- the properties panel renders its
182
+ // own tablist (see webhookTab below), so an unscoped tab query is
183
+ // ambiguous.
184
+ this.reusableAssetsTab = this.changeElementTabStrip.getByRole('tab', {
185
+ name: /reusable assets/i,
186
+ });
173
187
  // bpmn-js popup entries are `li[role="option"]` inside a `role="listbox"`,
174
188
  // so getByRole('listitem') no longer matches. Their accessible name also
175
189
  // includes the entry description, and three entries share the label
@@ -734,11 +748,53 @@ class ModelerCreatePage {
734
748
  async clickCancelButton() {
735
749
  await this.cancelButton.click({ timeout: 60000 });
736
750
  }
751
+ // `waitFor` actually blocks up to the timeout; `isVisible()` would return
752
+ // immediately and report false while the popup is still rendering.
753
+ async waitForVisible(locator, timeout) {
754
+ return locator
755
+ .first()
756
+ .waitFor({ state: 'visible', timeout })
757
+ .then(() => true)
758
+ .catch(() => false);
759
+ }
760
+ // The change-element popup is now tabbed: every `replace.template-*` entry
761
+ // is assigned to the "Reusable Assets" tab, so connector templates are not
762
+ // in the DOM at all while the default "BPMN" tab is selected - which is why
763
+ // the template locators time out with `<element(s) not found>`.
764
+ // Opening that tab is version-tolerant: on an untabbed popup the strip is
765
+ // absent and this is a no-op.
766
+ // Search is the fallback, because it spans the full entry set across every
767
+ // tab. It must be typed with `pressSequentially`: the popup's search input
768
+ // is uncontrolled and its state is written by a `keyup` handler only, so
769
+ // `fill()` (which dispatches `input` alone) never applies the filter.
770
+ async revealChangeElementTemplate(option, searchTerm) {
771
+ if (await this.waitForVisible(option, 5000)) {
772
+ return;
773
+ }
774
+ if (await this.waitForVisible(this.changeElementTabStrip, 5000)) {
775
+ // Do not throw: a failed tab click leaves the search fallback below
776
+ // intact, and search alone is enough on an untabbed popup.
777
+ await this.reusableAssetsTab.click({ timeout: 30000 }).catch(() => { });
778
+ if (await this.waitForVisible(option, 15000)) {
779
+ return;
780
+ }
781
+ }
782
+ if (!(await this.waitForVisible(this.changeElementSearchInput, 5000))) {
783
+ return;
784
+ }
785
+ await this.changeElementSearchInput.click();
786
+ await this.changeElementSearchInput.fill('');
787
+ await this.changeElementSearchInput.pressSequentially(searchTerm, {
788
+ delay: 50,
789
+ });
790
+ }
737
791
  async clickRestConnectorOption() {
738
- await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout: 15000 });
792
+ await this.revealChangeElementTemplate(this.restConnectorOption, 'REST');
793
+ await (0, test_1.expect)(this.restConnectorOption).toBeVisible({ timeout: 90000 });
739
794
  await this.restConnectorOption.click();
740
795
  }
741
796
  async clickWebhookStartEventConnectorOption() {
797
+ await this.revealChangeElementTemplate(this.webhookMessageStartEventConnectorOption, 'Webhook');
742
798
  await this.webhookMessageStartEventConnectorOption.click({ timeout: 60000 });
743
799
  }
744
800
  async clickWebhookTab() {
@@ -991,6 +1047,7 @@ class ModelerCreatePage {
991
1047
  await this.intermediateBoundaryEvent.click({ timeout: 60000 });
992
1048
  }
993
1049
  async clickIntermediateWebhookConnectorOption() {
1050
+ await this.revealChangeElementTemplate(this.intermediateWebhookConnectorOption, 'Webhook');
994
1051
  await this.intermediateWebhookConnectorOption.click({ timeout: 60000 });
995
1052
  }
996
1053
  async clickCorrelationKeyProcessInput() {
@@ -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
  });
@@ -132,6 +132,9 @@ _8_10_1.test.describe('Connectors User Flow Tests @tasklistV2', () => {
132
132
  await modelerCreatePage.enterDiagramName(processName);
133
133
  await (0, sleep_1.sleep)(10000);
134
134
  await modelerCreatePage.clickChangeTypeButton();
135
+ // Connector templates now sit behind the change-element popup's
136
+ // "Reusable Assets" tab, so surface the entry before asserting on it.
137
+ await modelerCreatePage.revealChangeElementTemplate(modelerCreatePage.webhookMessageStartEventConnectorOption, 'Webhook');
135
138
  await (0, test_1.expect)(modelerCreatePage.webhookMessageStartEventConnectorOption).toBeVisible({
136
139
  timeout: 30000,
137
140
  });
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.972",
4
4
  "description": "End-to-end test helpers for Camunda 8",
5
5
  "repository": {
6
6
  "type": "git",