@camunda/e2e-test-suite 0.0.969 → 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.
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.d.ts +1 -0
- package/dist/pages/SM-8.10/ConnectorMarketplacePage.js +43 -5
- package/dist/pages/SM-8.10/ModelerCreatePage.d.ts +2 -0
- package/dist/pages/SM-8.10/ModelerCreatePage.js +50 -13
- package/dist/pages/SM-8.8/NavigationPage.d.ts +5 -0
- package/dist/pages/SM-8.8/NavigationPage.js +30 -20
- package/dist/tests/SM-8.8/identity-user-flows.spec.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
});
|
|
@@ -30,10 +30,15 @@ declare class NavigationPage {
|
|
|
30
30
|
username: string;
|
|
31
31
|
password: string;
|
|
32
32
|
}): Promise<void>;
|
|
33
|
+
private goToWithoutAccess;
|
|
33
34
|
goToOptimizeWithoutAccess({ username, password, }?: {
|
|
34
35
|
username?: string;
|
|
35
36
|
password?: string;
|
|
36
37
|
}): Promise<void>;
|
|
38
|
+
goToModelerWithoutAccess({ username, password, }?: {
|
|
39
|
+
username?: string;
|
|
40
|
+
password?: string;
|
|
41
|
+
}): Promise<void>;
|
|
37
42
|
goToKeycloak(): Promise<void>;
|
|
38
43
|
goToOCIdentity(sleepTimeout?: number, credentials?: {
|
|
39
44
|
username: string;
|
|
@@ -5,6 +5,7 @@ const test_1 = require("@playwright/test");
|
|
|
5
5
|
const LoginPage_1 = require("./LoginPage");
|
|
6
6
|
const sleep_1 = require("../../utils/sleep");
|
|
7
7
|
const constants_1 = require("../../utils/constants");
|
|
8
|
+
const GATEWAY_ERROR_STATUSES = [502, 503, 504];
|
|
8
9
|
class NavigationPage {
|
|
9
10
|
page;
|
|
10
11
|
modelerPageBanner;
|
|
@@ -118,18 +119,34 @@ class NavigationPage {
|
|
|
118
119
|
async goToOptimize(sleepTimeout, credentials) {
|
|
119
120
|
await this.goTo('/optimize', this.optimizePageBanner, sleepTimeout, credentials);
|
|
120
121
|
}
|
|
121
|
-
// `goTo` ends by asserting the app banner, so it cannot be used to reach
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
122
|
+
// `goTo` ends by asserting the app banner, so it cannot be used to reach a
|
|
123
|
+
// component the user has no role for: the denied response carries neither
|
|
124
|
+
// the banner nor a login form, and every retry burns a full 60s timeout.
|
|
125
|
+
// Navigate and log in only; the caller asserts what the denied page shows.
|
|
126
|
+
// The ingress answers 5xx while a component is still rolling out (upgrade
|
|
127
|
+
// scenarios), so retry the request until the component itself responds.
|
|
128
|
+
async goToWithoutAccess(url, username, password) {
|
|
129
|
+
await (0, test_1.expect)(async () => {
|
|
130
|
+
const response = await this.page.goto(url, {
|
|
131
|
+
timeout: 60000,
|
|
132
|
+
waitUntil: 'domcontentloaded',
|
|
133
|
+
});
|
|
134
|
+
(0, test_1.expect)(GATEWAY_ERROR_STATUSES).not.toContain(response?.status());
|
|
135
|
+
}).toPass({ timeout: 120000, intervals: [5000, 10000, 15000] });
|
|
136
|
+
await new LoginPage_1.LoginPage(this.page).login(username, password);
|
|
137
|
+
}
|
|
138
|
+
// Optimize denies a user without the Optimize role with a `text/plain` 403
|
|
139
|
+
// page.
|
|
126
140
|
async goToOptimizeWithoutAccess({ username = 'demo', password = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_PASSWORD ??
|
|
127
141
|
'demo', } = {}) {
|
|
128
|
-
await this.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
await this.goToWithoutAccess('/optimize', username, password);
|
|
143
|
+
}
|
|
144
|
+
// Web Modeler denies a user without the Web Modeler role with a blank page.
|
|
145
|
+
// It briefly renders its banner before dropping it, so `goTo`'s closing
|
|
146
|
+
// banner assertion either fails outright or passes by winning that race.
|
|
147
|
+
async goToModelerWithoutAccess({ username = 'demo', password = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_PASSWORD ??
|
|
148
|
+
'demo', } = {}) {
|
|
149
|
+
await this.goToWithoutAccess('/modeler', username, password);
|
|
133
150
|
}
|
|
134
151
|
async goToKeycloak() {
|
|
135
152
|
const keycloakUrl = process.env.KEYCLOAK_URL
|
|
@@ -147,18 +164,11 @@ class NavigationPage {
|
|
|
147
164
|
async goToConsole(sleepTimeout, credentials) {
|
|
148
165
|
await this.goTo('/console', this.consolePageBanner, sleepTimeout, credentials);
|
|
149
166
|
}
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
// Console's own "Something went wrong" error page, which has neither the
|
|
153
|
-
// banner nor a login form, and every retry burns a full 60s timeout.
|
|
154
|
-
// Navigate and log in only; the caller asserts what the denied page shows.
|
|
167
|
+
// Console denies a user without the Console role with its own "Something
|
|
168
|
+
// went wrong" error page.
|
|
155
169
|
async goToConsoleWithoutAccess({ username = 'demo', password = process.env.DISTRO_QA_E2E_TESTS_IDENTITY_FIRSTUSER_PASSWORD ??
|
|
156
170
|
'demo', } = {}) {
|
|
157
|
-
await this.
|
|
158
|
-
timeout: 60000,
|
|
159
|
-
waitUntil: 'domcontentloaded',
|
|
160
|
-
});
|
|
161
|
-
await new LoginPage_1.LoginPage(this.page).login(username, password);
|
|
171
|
+
await this.goToWithoutAccess('/console', username, password);
|
|
162
172
|
}
|
|
163
173
|
async goToManagementIdentity(sleepTimeout, credentials) {
|
|
164
174
|
await this.goTo('/identity', this.managementIdentityPageBanner, sleepTimeout, credentials);
|
|
@@ -74,7 +74,7 @@ SM_8_8_1.test.describe('Identity User Flow Tests', () => {
|
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
await SM_8_8_1.test.step('Ensure Modeler is not Accessible', async () => {
|
|
77
|
-
await navigationPage.
|
|
77
|
+
await navigationPage.goToModelerWithoutAccess(credentials);
|
|
78
78
|
await (0, test_1.expect)(modelerHomePage.modelerPageBanner).not.toBeVisible({
|
|
79
79
|
timeout: 60000,
|
|
80
80
|
});
|