@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.
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
969
|
-
//
|
|
970
|
-
//
|
|
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
|
-
|
|
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
|
|
1172
|
-
//
|
|
1173
|
-
//
|
|
1174
|
-
|
|
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
|
});
|