@capillarytech/cap-ui-utils 3.1.0 → 3.1.2
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/e2e/constants/common.js +22 -0
- package/e2e/index.js +65 -0
- package/e2e/pages/common/base.page.js +13 -0
- package/e2e/pages/common/constant.js +5 -0
- package/e2e/pages/common/login.page.js +95 -0
- package/e2e/services/{lockService.ts → lockService.js} +71 -70
- package/e2e/services/locks/.gitkeep +0 -0
- package/e2e/utils/{antdVersionUtil.ts → antdVersionUtil.js} +24 -32
- package/e2e/utils/{automationBypassUtil.ts → automationBypassUtil.js} +11 -8
- package/e2e/utils/{debugModeUtil.ts → debugModeUtil.js} +9 -7
- package/e2e/utils/deletionRegistry.js +16 -0
- package/e2e/utils/{elementUtil.ts → elementUtil.js} +226 -298
- package/e2e/utils/expectUtil.js +40 -0
- package/e2e/utils/featureFlagUtil.js +25 -0
- package/e2e/utils/garudaDropdownUtil.js +34 -0
- package/e2e/utils/htmlEditorUtil.js +59 -0
- package/e2e/utils/logCollectorUtil.js +65 -0
- package/e2e/utils/{mockResponse.ts → mockResponse.js} +5 -2
- package/e2e/utils/{reportUploader.ts → reportUploader.js} +69 -66
- package/e2e/utils/{requestRecorderUtil.ts → requestRecorderUtil.js} +69 -97
- package/e2e/utils/{screenshotRecorderUtil.ts → screenshotRecorderUtil.js} +61 -82
- package/e2e/utils/setupMock.js +18 -0
- package/e2e/utils/unmatchedBracesUtil.js +87 -0
- package/e2e/utils/uploaders/fileServiceUploader.js +113 -0
- package/e2e/utils/uploaders/uploader.js +2 -0
- package/e2e/utils/virtualListUtil.js +74 -0
- package/package.json +1 -1
- package/e2e/constants/common.ts +0 -23
- package/e2e/index.ts +0 -30
- package/e2e/pages/common/base.page.ts +0 -11
- package/e2e/pages/common/constant.ts +0 -2
- package/e2e/pages/common/login.page.ts +0 -95
- package/e2e/services/locks/beeTemplateReady.signal +0 -1
- package/e2e/utils/deletionRegistry.ts +0 -15
- package/e2e/utils/expectUtil.ts +0 -19
- package/e2e/utils/featureFlagUtil.ts +0 -30
- package/e2e/utils/garudaDropdownUtil.ts +0 -60
- package/e2e/utils/htmlEditorUtil.ts +0 -56
- package/e2e/utils/logCollectorUtil.ts +0 -52
- package/e2e/utils/setupMock.ts +0 -29
- package/e2e/utils/unmatchedBracesUtil.ts +0 -101
- package/e2e/utils/uploaders/fileServiceUploader.ts +0 -84
- package/e2e/utils/uploaders/uploader.ts +0 -20
- package/e2e/utils/virtualListUtil.ts +0 -115
|
@@ -1,28 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const assert_1 = require("assert");
|
|
3
4
|
class ElementUtils {
|
|
4
|
-
async hoverToReveal(element
|
|
5
|
+
async hoverToReveal(element) {
|
|
5
6
|
await element.waitForDisplayed({ timeout: 60000 });
|
|
6
7
|
await element.moveTo();
|
|
7
8
|
await browser.execute((el) => {
|
|
8
|
-
['mouseenter','mouseover','mousemove'].forEach((t) =>
|
|
9
|
-
el.dispatchEvent(new MouseEvent(t, { bubbles: true })));
|
|
9
|
+
['mouseenter', 'mouseover', 'mousemove'].forEach((t) => el.dispatchEvent(new MouseEvent(t, { bubbles: true })));
|
|
10
10
|
}, element);
|
|
11
11
|
await browser.pause(400);
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
await
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
await element.click()
|
|
13
|
+
async elementClick(element) {
|
|
14
|
+
await browser.pause(2000);
|
|
15
|
+
await element.waitForDisplayed({ timeout: 90000 });
|
|
16
|
+
try {
|
|
17
|
+
await element.waitForClickable({ timeout: 60000, interval: 1000, timeoutMsg: "element is still not clickable" });
|
|
18
|
+
await element.click();
|
|
20
19
|
}
|
|
21
|
-
catch(error){
|
|
22
|
-
|
|
20
|
+
catch (error) {
|
|
21
|
+
await this.forceClick(element);
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
|
-
|
|
26
24
|
/**
|
|
27
25
|
* Scrolls the element to viewport center and clicks it via a dispatched
|
|
28
26
|
* pointer/mouse-event sequence — bypasses WDIO's isClickable coverage check.
|
|
@@ -30,31 +28,28 @@ class ElementUtils {
|
|
|
30
28
|
* overlay (info banner, lingering popover) sits over its centre point, or
|
|
31
29
|
* antd selects that open on mousedown rather than click.
|
|
32
30
|
*/
|
|
33
|
-
async forceClick(element
|
|
31
|
+
async forceClick(element) {
|
|
34
32
|
const fresh = await this.waitForDisplayedStaleSafe(element, 90000);
|
|
35
|
-
await browser.execute((el
|
|
33
|
+
await browser.execute((el) => {
|
|
36
34
|
el.scrollIntoView({ block: "center", inline: "center" });
|
|
37
|
-
['pointerdown','mousedown','pointerup','mouseup','click'].forEach((t) =>
|
|
38
|
-
el.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, view: window })));
|
|
35
|
+
['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click'].forEach((t) => el.dispatchEvent(new MouseEvent(t, { bubbles: true, cancelable: true, view: window })));
|
|
39
36
|
}, fresh);
|
|
40
37
|
await browser.pause(500);
|
|
41
38
|
}
|
|
42
|
-
|
|
43
39
|
/**
|
|
44
40
|
* Re-fetches a fresh element from its original selector. A detached ("stale")
|
|
45
41
|
* handle — left behind when React/antd re-renders the node — is replaced by a
|
|
46
42
|
* live one. Falls back to the original reference for elements without a plain
|
|
47
43
|
* string selector (e.g. chained/derived elements).
|
|
48
44
|
*/
|
|
49
|
-
|
|
45
|
+
async refetch(element) {
|
|
50
46
|
// `.selector` on an un-awaited ChainablePromiseElement (e.g. a page getter
|
|
51
47
|
// passed straight through, as every current caller does) resolves lazily —
|
|
52
48
|
// reading it as a promise, not a sync property, prevents this from always
|
|
53
49
|
// seeing a Promise object and silently skipping the re-query.
|
|
54
|
-
const selector = await (element
|
|
50
|
+
const selector = await (element === null || element === void 0 ? void 0 : element.selector);
|
|
55
51
|
return typeof selector === "string" ? await $(selector) : element;
|
|
56
52
|
}
|
|
57
|
-
|
|
58
53
|
/**
|
|
59
54
|
* waitForDisplayed that survives the target re-rendering mid-wait, returning
|
|
60
55
|
* the live handle it last confirmed. WDIO's built-in waitForDisplayed polls
|
|
@@ -66,7 +61,7 @@ class ElementUtils {
|
|
|
66
61
|
* get a fresh, currently-attached element instead of one that already went
|
|
67
62
|
* stale during the wait.
|
|
68
63
|
*/
|
|
69
|
-
|
|
64
|
+
async waitForDisplayedStaleSafe(element, timeout = 90000) {
|
|
70
65
|
const start = Date.now();
|
|
71
66
|
while (Date.now() - start < timeout) {
|
|
72
67
|
const el = await this.refetch(element);
|
|
@@ -74,18 +69,20 @@ class ElementUtils {
|
|
|
74
69
|
const outcome = await el
|
|
75
70
|
.waitForDisplayed({ timeout: remaining, interval: 500 })
|
|
76
71
|
.then(() => "displayed")
|
|
77
|
-
.catch((error) =>
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (outcome === "
|
|
72
|
+
.catch((error) => {
|
|
73
|
+
var _a;
|
|
74
|
+
return String((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error).includes("stale element reference")
|
|
75
|
+
? "stale"
|
|
76
|
+
: "timeout";
|
|
77
|
+
});
|
|
78
|
+
if (outcome === "displayed")
|
|
79
|
+
return el;
|
|
80
|
+
if (outcome === "timeout")
|
|
81
|
+
throw new Error("Element not displayed within timeout");
|
|
84
82
|
await browser.pause(300); // stale → re-query and keep waiting
|
|
85
83
|
}
|
|
86
84
|
throw new Error("Element kept going stale while waiting for it to be displayed");
|
|
87
85
|
}
|
|
88
|
-
|
|
89
86
|
/**
|
|
90
87
|
* Clicks an element, retrying against a freshly re-queried node whenever the
|
|
91
88
|
* DOM re-renders mid-interaction and throws "stale element reference" (antd
|
|
@@ -93,7 +90,7 @@ class ElementUtils {
|
|
|
93
90
|
* failure it falls back to a forced click. Throws only if nothing succeeds
|
|
94
91
|
* within `timeout`.
|
|
95
92
|
*/
|
|
96
|
-
|
|
93
|
+
async clickWithStaleRetry(element, timeout = 40000) {
|
|
97
94
|
const start = Date.now();
|
|
98
95
|
// Uses promise .catch() rather than nested try/catch blocks: a genuine
|
|
99
96
|
// failure still propagates cleanly to WDIO so the failure screenshot is
|
|
@@ -104,14 +101,17 @@ class ElementUtils {
|
|
|
104
101
|
.waitForClickable({ timeout: 10000, interval: 500 })
|
|
105
102
|
.then(() => el.click())
|
|
106
103
|
.then(() => "clicked")
|
|
107
|
-
.catch((error) =>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (outcome === "
|
|
114
|
-
|
|
104
|
+
.catch((error) => {
|
|
105
|
+
var _a;
|
|
106
|
+
return String((_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : error).includes("stale element reference")
|
|
107
|
+
? "stale"
|
|
108
|
+
: "unclickable";
|
|
109
|
+
});
|
|
110
|
+
if (outcome === "clicked")
|
|
111
|
+
return;
|
|
112
|
+
if (outcome === "unclickable")
|
|
113
|
+
break; // genuine (non-stale) failure → forced click
|
|
114
|
+
await browser.pause(500); // stale → re-query and retry
|
|
115
115
|
}
|
|
116
116
|
// Single fallback: forceClick re-fetches and stale-waits internally, so
|
|
117
117
|
// it survives the target still churning after the loop above gave up.
|
|
@@ -119,7 +119,6 @@ class ElementUtils {
|
|
|
119
119
|
// real error to the test (and its screenshot).
|
|
120
120
|
await this.forceClick(element);
|
|
121
121
|
}
|
|
122
|
-
|
|
123
122
|
/**
|
|
124
123
|
* Expands an antd Collapse (accordion) left-nav section by its header label,
|
|
125
124
|
* then confirms `confirmChild` became visible.
|
|
@@ -137,18 +136,16 @@ class ElementUtils {
|
|
|
137
136
|
* User segments, …). `label` is the visible header text; `confirmChild` is any
|
|
138
137
|
* element that only becomes visible once the section is expanded.
|
|
139
138
|
*/
|
|
140
|
-
async expandSideNavSection(label
|
|
139
|
+
async expandSideNavSection(label, confirmChild, attempts = 3) {
|
|
141
140
|
// The header label may be a <span> (top-level sections) or a <div> (nested
|
|
142
141
|
// sub-accordions like "Status"), so match any element by its text.
|
|
143
|
-
const header = await $(
|
|
144
|
-
`//div[@role="tab" and contains(@class,"ant-collapse-header")][.//*[normalize-space(text())="${label}"]]`
|
|
145
|
-
);
|
|
142
|
+
const header = await $(`//div[@role="tab" and contains(@class,"ant-collapse-header")][.//*[normalize-space(text())="${label}"]]`);
|
|
146
143
|
await header.waitForDisplayed({ timeout: 60000 });
|
|
147
144
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
148
145
|
const expanded = (await header.getAttribute("aria-expanded")) === "true";
|
|
149
146
|
if (!expanded) {
|
|
150
147
|
await header.waitForClickable({ timeout: 40000 });
|
|
151
|
-
await browser.execute((el
|
|
148
|
+
await browser.execute((el) => el.click(), header);
|
|
152
149
|
console.log(`Expanded side-nav "${label}" (attempt ${attempt})`);
|
|
153
150
|
}
|
|
154
151
|
const visible = await confirmChild
|
|
@@ -162,7 +159,6 @@ class ElementUtils {
|
|
|
162
159
|
}
|
|
163
160
|
throw new Error(`Side-nav section "${label}" did not expand to reveal its content`);
|
|
164
161
|
}
|
|
165
|
-
|
|
166
162
|
/**
|
|
167
163
|
* Opens a leaf item that lives inside a left-nav antd Collapse section:
|
|
168
164
|
* expands the section (idempotent) via expandSideNavSection, then clicks the
|
|
@@ -172,8 +168,8 @@ class ElementUtils {
|
|
|
172
168
|
* attempt. Use for the "expand section -> click child" pattern
|
|
173
169
|
* (Library -> Charts, Data connectors -> Export schedules, User segments -> Status).
|
|
174
170
|
*/
|
|
175
|
-
async openSideNavItem(sectionLabel
|
|
176
|
-
let lastError
|
|
171
|
+
async openSideNavItem(sectionLabel, leaf, attempts = 3) {
|
|
172
|
+
let lastError;
|
|
177
173
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
178
174
|
try {
|
|
179
175
|
await this.expandSideNavSection(sectionLabel, leaf);
|
|
@@ -181,7 +177,8 @@ class ElementUtils {
|
|
|
181
177
|
await leaf.click();
|
|
182
178
|
console.log(`Opened "${sectionLabel}" item on attempt ${attempt}`);
|
|
183
179
|
return;
|
|
184
|
-
}
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
185
182
|
lastError = error;
|
|
186
183
|
console.log(`openSideNavItem "${sectionLabel}": attempt ${attempt}/${attempts} failed (${error}) — re-expanding`);
|
|
187
184
|
await browser.pause(2000);
|
|
@@ -189,28 +186,25 @@ class ElementUtils {
|
|
|
189
186
|
}
|
|
190
187
|
throw new Error(`openSideNavItem: could not open item under "${sectionLabel}" after ${attempts} attempts. Last error: ${lastError}`);
|
|
191
188
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
await element.setValue(textToEnter)
|
|
189
|
+
async enterText(element, textToEnter) {
|
|
190
|
+
await browser.pause(1000);
|
|
191
|
+
// browser.execute('arguments[0].style.outline = "#f00 solid 4px";', element);
|
|
192
|
+
await element.waitForEnabled({ timeout: 40000, interval: 1000, timeoutMsg: "element is still not editable" });
|
|
193
|
+
await element.setValue(textToEnter);
|
|
198
194
|
}
|
|
199
|
-
|
|
200
195
|
/**
|
|
201
196
|
* Types text using addValue (key events). Use for React controlled inputs
|
|
202
197
|
* where setValue does not update state.
|
|
203
198
|
*/
|
|
204
|
-
async typeText(element
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
}
|
|
213
|
-
|
|
199
|
+
async typeText(element, textToEnter) {
|
|
200
|
+
await browser.pause(1000);
|
|
201
|
+
await element.waitForEnabled({
|
|
202
|
+
timeout: 40000,
|
|
203
|
+
interval: 1000,
|
|
204
|
+
timeoutMsg: "element is still not editable"
|
|
205
|
+
});
|
|
206
|
+
await element.addValue(textToEnter);
|
|
207
|
+
}
|
|
214
208
|
/**
|
|
215
209
|
* Selects a date by clicking through the antd calendar UI instead of
|
|
216
210
|
* typing into the input. Some DatePicker instances only accept a value
|
|
@@ -228,33 +222,29 @@ class ElementUtils {
|
|
|
228
222
|
* (via the next/prev-month buttons) until the target month is showing,
|
|
229
223
|
* then clicks the day cell there.
|
|
230
224
|
*/
|
|
231
|
-
async selectCalendarDate(dateInput
|
|
225
|
+
async selectCalendarDate(dateInput, targetDate, label = "date field") {
|
|
232
226
|
const isoDate = `${targetDate.getFullYear()}-${String(targetDate.getMonth() + 1).padStart(2, "0")}-${String(targetDate.getDate()).padStart(2, "0")}`;
|
|
233
227
|
const expectedDisplay = targetDate.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric" });
|
|
234
|
-
|
|
235
228
|
await this.retryStep(async () => {
|
|
236
229
|
const input = await this.refetch(dateInput);
|
|
237
230
|
await input.waitForClickable({ timeout: 40000 });
|
|
238
231
|
await input.click();
|
|
239
|
-
|
|
240
232
|
const panel = await $('//div[contains(@class,"ant-picker-dropdown") and not(contains(@class,"ant-picker-dropdown-hidden"))]');
|
|
241
233
|
await panel.waitForDisplayed({ timeout: 10000 });
|
|
242
|
-
|
|
243
234
|
let dayCell = await panel.$(`.//td[@title="${isoDate}"]`);
|
|
244
235
|
const alreadyVisible = await dayCell.isDisplayed().catch(() => false);
|
|
245
|
-
|
|
246
236
|
if (!alreadyVisible) {
|
|
247
237
|
const headerView = await panel.$(".ant-picker-header-view");
|
|
248
238
|
const nextBtn = await panel.$(".ant-picker-header-next-btn");
|
|
249
239
|
const prevBtn = await panel.$(".ant-picker-header-prev-btn");
|
|
250
240
|
const targetMonths = targetDate.getFullYear() * 12 + targetDate.getMonth();
|
|
251
|
-
|
|
252
241
|
for (let attempt = 0; attempt < 24; attempt++) {
|
|
253
242
|
const shownText = (await headerView.getText()).trim(); // e.g. "Jul 2026"
|
|
254
243
|
const shownDate = new Date(`1 ${shownText}`);
|
|
255
244
|
const shownMonths = shownDate.getFullYear() * 12 + shownDate.getMonth();
|
|
256
245
|
const diff = targetMonths - shownMonths;
|
|
257
|
-
if (diff === 0)
|
|
246
|
+
if (diff === 0)
|
|
247
|
+
break;
|
|
258
248
|
if (attempt === 23) {
|
|
259
249
|
throw new Error(`${label}: could not navigate calendar to target month (stuck on "${shownText}")`);
|
|
260
250
|
}
|
|
@@ -263,12 +253,9 @@ class ElementUtils {
|
|
|
263
253
|
}
|
|
264
254
|
dayCell = await panel.$(`.//td[@title="${isoDate}"]`);
|
|
265
255
|
}
|
|
266
|
-
|
|
267
256
|
await dayCell.waitForClickable({ timeout: 10000 });
|
|
268
257
|
await dayCell.click();
|
|
269
|
-
|
|
270
258
|
await panel.waitForDisplayed({ timeout: 5000, reverse: true });
|
|
271
|
-
|
|
272
259
|
const committed = await input.getValue();
|
|
273
260
|
if (committed !== expectedDisplay) {
|
|
274
261
|
throw new Error(`${label} did not commit: expected "${expectedDisplay}", got "${committed}"`);
|
|
@@ -282,7 +269,6 @@ class ElementUtils {
|
|
|
282
269
|
},
|
|
283
270
|
});
|
|
284
271
|
}
|
|
285
|
-
|
|
286
272
|
/**
|
|
287
273
|
* Writes via the native HTMLInputElement value setter, then dispatches an
|
|
288
274
|
* `input` event. Bypasses React's `_valueTracker`, so React commits the
|
|
@@ -298,91 +284,79 @@ class ElementUtils {
|
|
|
298
284
|
* (validation, commit-on-blur, etc). With an element, it first waits for the
|
|
299
285
|
* field to be editable, then dispatches `change` + blur after `input`.
|
|
300
286
|
*/
|
|
301
|
-
async setReactInputValue(
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
287
|
+
async setReactInputValue(target, value, alsoDispatch = []) {
|
|
288
|
+
if (typeof target === "string") {
|
|
289
|
+
await browser.execute((sel, val, extras) => {
|
|
290
|
+
const input = document.querySelector(sel);
|
|
291
|
+
if (!input) {
|
|
292
|
+
throw new Error(`setReactInputValue: no element matches selector "${sel}"`);
|
|
293
|
+
}
|
|
294
|
+
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set;
|
|
295
|
+
setter.call(input, val);
|
|
296
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
297
|
+
for (const name of extras) {
|
|
298
|
+
input.dispatchEvent(new Event(name, { bubbles: true }));
|
|
299
|
+
}
|
|
300
|
+
}, target, value, alsoDispatch);
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
await browser.pause(1000);
|
|
304
|
+
await target.waitForEnabled({
|
|
305
|
+
timeout: 40000,
|
|
306
|
+
interval: 1000,
|
|
307
|
+
timeoutMsg: "element is still not editable"
|
|
308
|
+
});
|
|
309
|
+
await browser.execute((el, val) => {
|
|
310
|
+
const desc = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value");
|
|
311
|
+
if (desc && desc.set) {
|
|
312
|
+
desc.set.call(el, val);
|
|
312
313
|
}
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
316
|
-
for (const name of extras) {
|
|
317
|
-
input.dispatchEvent(new Event(name, { bubbles: true }));
|
|
314
|
+
else {
|
|
315
|
+
el.value = val;
|
|
318
316
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
317
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
318
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
319
|
+
el.blur();
|
|
320
|
+
}, target, value);
|
|
321
|
+
}
|
|
322
|
+
async switchToUrl(url) {
|
|
323
|
+
await browser.switchWindow(url);
|
|
324
|
+
}
|
|
325
|
+
async waitUntilDisplayed(element, awaitedText, timeout = 30000) {
|
|
326
|
+
try {
|
|
327
|
+
// First wait for element to be displayed
|
|
328
|
+
await element.waitForDisplayed({ timeout: timeout, timeoutMsg: `Element not displayed within ${timeout} ms` });
|
|
329
|
+
// Then wait for the correct text
|
|
330
|
+
await element.waitUntil(async function () {
|
|
331
|
+
const currentText = await this.getText();
|
|
332
|
+
console.log(`Waiting for text: '${awaitedText}', Current text: '${currentText}'`);
|
|
333
|
+
return currentText.includes(awaitedText) || currentText === awaitedText;
|
|
334
|
+
}, {
|
|
335
|
+
timeout,
|
|
336
|
+
timeoutMsg: `Unable to locate the awaited text: '${awaitedText}' within ${timeout} ms`
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
console.log(`🔴 Error in waitUntilDisplayed: ${error}`);
|
|
341
|
+
throw error;
|
|
338
342
|
}
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
el.blur();
|
|
342
|
-
}, target, value);
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
async switchToUrl(url: string){
|
|
346
|
-
await browser.switchWindow('url')
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
async waitUntilDisplayed(element:WebdriverIO.Element, awaitedText, timeout=30000){
|
|
350
|
-
try {
|
|
351
|
-
// First wait for element to be displayed
|
|
352
|
-
await element.waitForDisplayed({ timeout: timeout, timeoutMsg: `Element not displayed within ${timeout} ms` });
|
|
353
|
-
|
|
354
|
-
// Then wait for the correct text
|
|
343
|
+
}
|
|
344
|
+
async waitUntilEnabled(element) {
|
|
355
345
|
await element.waitUntil(async function () {
|
|
356
|
-
|
|
357
|
-
console.log(`Waiting for text: '${awaitedText}', Current text: '${currentText}'`);
|
|
358
|
-
return currentText.includes(awaitedText) || currentText === awaitedText;
|
|
346
|
+
return await element.isEnabled();
|
|
359
347
|
}, {
|
|
360
|
-
timeout,
|
|
361
|
-
timeoutMsg:
|
|
348
|
+
timeout: 90000,
|
|
349
|
+
timeoutMsg: 'Element not enabled'
|
|
362
350
|
});
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
throw error;
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
async waitUntilEnabled(element:WebdriverIO.Element){
|
|
370
|
-
await element.waitUntil(async function () {
|
|
371
|
-
return await element.isEnabled();
|
|
372
|
-
}, {
|
|
373
|
-
timeout: 90000,
|
|
374
|
-
timeoutMsg: 'Element not enabled'
|
|
375
|
-
});
|
|
376
|
-
}
|
|
377
|
-
async checkElemetExistsAndElementClick(element:WebdriverIO.Element){
|
|
351
|
+
}
|
|
352
|
+
async checkElemetExistsAndElementClick(element) {
|
|
378
353
|
await browser.pause(1000);
|
|
379
354
|
let isDisplayed = await element.isDisplayed();
|
|
380
355
|
if (isDisplayed) {
|
|
381
|
-
|
|
382
|
-
|
|
356
|
+
await element.waitForClickable({ timeout: 40000, interval: 1000, timeoutMsg: "element is still not clickable" });
|
|
357
|
+
await element.click();
|
|
383
358
|
}
|
|
384
359
|
}
|
|
385
|
-
|
|
386
360
|
/**
|
|
387
361
|
* Stale-safe variant of checkElemetExistsAndElementClick. Opt-in helper for
|
|
388
362
|
* optional buttons that live inside a re-rendering container — e.g. the
|
|
@@ -391,7 +365,7 @@ class ElementUtils {
|
|
|
391
365
|
* element reference". Kept as a separate method so the widely-used shared
|
|
392
366
|
* helpers stay untouched and other modules are unaffected.
|
|
393
367
|
*/
|
|
394
|
-
async checkElementExistsAndClickStaleSafe(element
|
|
368
|
+
async checkElementExistsAndClickStaleSafe(element, appearTimeout = 3000) {
|
|
395
369
|
// Presence must be *waited* for, not sampled. A fixed pause + one
|
|
396
370
|
// isDisplayed() snapshot loses the race against an antd modal's
|
|
397
371
|
// mount + zoom transition: the helper returns "not there", the modal
|
|
@@ -405,10 +379,9 @@ class ElementUtils {
|
|
|
405
379
|
.waitForDisplayed({ timeout: appearTimeout, interval: 250 })
|
|
406
380
|
.then(() => true)
|
|
407
381
|
.catch(() => false);
|
|
408
|
-
if (!appeared)
|
|
409
|
-
|
|
382
|
+
if (!appeared)
|
|
383
|
+
return false;
|
|
410
384
|
await this.clickWithStaleRetry(element, 40000);
|
|
411
|
-
|
|
412
385
|
// Confirm the modal actually went away: if it is still up, everything
|
|
413
386
|
// downstream is about to fail confusingly, so fail here instead.
|
|
414
387
|
const dismissed = await (await this.refetch(element))
|
|
@@ -420,118 +393,104 @@ class ElementUtils {
|
|
|
420
393
|
}
|
|
421
394
|
return true;
|
|
422
395
|
}
|
|
423
|
-
|
|
424
396
|
/**
|
|
425
397
|
* Use this when the element/input is optional (not mandatory).
|
|
426
398
|
* Does not throw if the element is not displayed; only logs and skips entering text.
|
|
427
399
|
*/
|
|
428
|
-
async checkElemetExistsAndEnterText(element
|
|
400
|
+
async checkElemetExistsAndEnterText(element, textToEnter) {
|
|
429
401
|
const isDisplayed = await element.isDisplayed();
|
|
430
402
|
if (isDisplayed) {
|
|
431
403
|
await element.waitForEnabled({
|
|
432
404
|
timeout: 40000,
|
|
433
405
|
interval: 1000,
|
|
434
|
-
timeoutMsg:
|
|
406
|
+
timeoutMsg: "element is still not editable"
|
|
435
407
|
});
|
|
436
408
|
await element.setValue(textToEnter);
|
|
437
409
|
}
|
|
438
410
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
await browser.pause(1000)
|
|
411
|
+
async isElementDisplayed(element) {
|
|
412
|
+
await browser.pause(1000);
|
|
442
413
|
let isDisplayed = await element.isDisplayed();
|
|
443
414
|
return isDisplayed;
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
async elementHover(element: WebdriverIO.Element) {
|
|
415
|
+
}
|
|
416
|
+
async elementHover(element) {
|
|
447
417
|
try {
|
|
448
418
|
// Pause for 2 seconds before starting (optional)
|
|
449
419
|
await browser.pause(2000);
|
|
450
|
-
|
|
451
420
|
// Wait for the element to be displayed
|
|
452
421
|
await element.waitForDisplayed({ timeout: 90000 });
|
|
453
|
-
|
|
454
422
|
// Wait for the element to be interactable/clickable (often ensures it's not obscured)
|
|
455
|
-
await element.waitForClickable({
|
|
456
|
-
timeout: 40000,
|
|
457
|
-
interval: 1000,
|
|
458
|
-
timeoutMsg:
|
|
423
|
+
await element.waitForClickable({
|
|
424
|
+
timeout: 40000,
|
|
425
|
+
interval: 1000,
|
|
426
|
+
timeoutMsg: "element is still not interactable for hover"
|
|
459
427
|
});
|
|
460
|
-
|
|
461
428
|
// Perform the hover action
|
|
462
429
|
await element.moveTo();
|
|
463
430
|
console.log('Move to action performed');
|
|
464
|
-
}
|
|
431
|
+
}
|
|
432
|
+
catch (error) {
|
|
465
433
|
throw new Error(error);
|
|
466
434
|
}
|
|
467
435
|
}
|
|
468
|
-
|
|
469
436
|
/**
|
|
470
437
|
* Click the button only if it becomes clickable within the given timeout.
|
|
471
438
|
* Uses a short timeout (default 1s) to avoid waiting when the button stays disabled (e.g. after validation failure).
|
|
472
439
|
* Use during unmatched-braces checks: if Done/Save becomes clickable we click it; otherwise we skip and assert the error.
|
|
473
440
|
*/
|
|
474
|
-
async clickIfClickable(button
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
441
|
+
async clickIfClickable(button, timeoutMs = 1000) {
|
|
442
|
+
try {
|
|
443
|
+
await button.waitForClickable({ timeout: timeoutMs });
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
447
|
+
if (/timeout|not clickable/i.test(message)) {
|
|
448
|
+
// Button stayed disabled within the timeout; caller will assert validation error.
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
throw error;
|
|
482
452
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
// waitForClickable succeeded — click errors propagate normally.
|
|
487
|
-
await button.click();
|
|
453
|
+
// waitForClickable succeeded — click errors propagate normally.
|
|
454
|
+
await button.click();
|
|
488
455
|
}
|
|
489
|
-
|
|
490
456
|
/**
|
|
491
457
|
* Clear the input's current value then type new text.
|
|
492
458
|
* Delegates the clear step to selectAllAndClear (select-all + Backspace) so
|
|
493
459
|
* React-controlled inputs see the onChange event. Only calls setValue when
|
|
494
460
|
* textToEnter is non-empty.
|
|
495
461
|
*/
|
|
496
|
-
async clearAndEnterText(element
|
|
462
|
+
async clearAndEnterText(element, textToEnter) {
|
|
497
463
|
await this.selectAllAndClear(element);
|
|
498
464
|
if (textToEnter !== '') {
|
|
499
465
|
await element.setValue(textToEnter);
|
|
500
466
|
}
|
|
501
467
|
}
|
|
502
|
-
|
|
503
468
|
/**
|
|
504
469
|
* Clear text by sending Backspace key N times (N = textToRemove.length).
|
|
505
470
|
* If element is provided, focus it first (click) so backspaces apply to that field.
|
|
506
471
|
* Use for clearing content in inputs/editors (e.g. unmatched-braces recovery in Viber, Email CodeMirror).
|
|
507
472
|
*/
|
|
508
|
-
async clearByBackspace(textToRemove
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
473
|
+
async clearByBackspace(textToRemove, element) {
|
|
474
|
+
if (element) {
|
|
475
|
+
await element.waitForDisplayed({ timeout: 10000 });
|
|
476
|
+
await element.click();
|
|
477
|
+
}
|
|
478
|
+
const count = textToRemove.length;
|
|
479
|
+
for (let i = 0; i < count; i++) {
|
|
480
|
+
await browser.keys(["Backspace"]);
|
|
481
|
+
}
|
|
517
482
|
}
|
|
518
|
-
|
|
519
483
|
/**
|
|
520
484
|
* Wait for a spinner to appear (optional) then disappear.
|
|
521
485
|
* Mirrors the pattern in deleteTemplate: catches if spinner never appears (loaded too fast),
|
|
522
486
|
* then waits for it to vanish before proceeding.
|
|
523
487
|
*/
|
|
524
|
-
async waitForSpinnerCycle(
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
console.log("Spinner did not appear — may have loaded too fast, proceeding");
|
|
531
|
-
});
|
|
532
|
-
await spinner.waitForDisplayed({ timeout: disappearTimeoutMs, reverse: true });
|
|
533
|
-
}
|
|
534
|
-
|
|
488
|
+
async waitForSpinnerCycle(spinner, appearTimeoutMs = 5000, disappearTimeoutMs = 180000) {
|
|
489
|
+
await spinner.waitForDisplayed({ timeout: appearTimeoutMs }).catch(() => {
|
|
490
|
+
console.log("Spinner did not appear — may have loaded too fast, proceeding");
|
|
491
|
+
});
|
|
492
|
+
await spinner.waitForDisplayed({ timeout: disappearTimeoutMs, reverse: true });
|
|
493
|
+
}
|
|
535
494
|
/**
|
|
536
495
|
* Best-effort loader "settle" for the blank-screen -> loader -> content
|
|
537
496
|
* lifecycle. Waits (advisory) for a loader to APPEAR, then to DISAPPEAR, both
|
|
@@ -542,18 +501,14 @@ class ElementUtils {
|
|
|
542
501
|
* hard guarantee should follow this with a positive element check (e.g.
|
|
543
502
|
* waitForStableVisible on real content).
|
|
544
503
|
*/
|
|
545
|
-
async waitForLoaderSettle(
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
.waitForDisplayed({ timeout: disappearTimeoutMs, reverse: true })
|
|
554
|
-
.catch(() => console.log(`Loader still present after ${disappearTimeoutMs}ms — proceeding (positive check gates)`));
|
|
555
|
-
}
|
|
556
|
-
|
|
504
|
+
async waitForLoaderSettle(loader, { appearTimeoutMs = 20000, disappearTimeoutMs = 180000 } = {}) {
|
|
505
|
+
await loader
|
|
506
|
+
.waitForDisplayed({ timeout: appearTimeoutMs })
|
|
507
|
+
.catch(() => console.log(`Loader did not appear within ${appearTimeoutMs}ms — proceeding`));
|
|
508
|
+
await loader
|
|
509
|
+
.waitForDisplayed({ timeout: disappearTimeoutMs, reverse: true })
|
|
510
|
+
.catch(() => console.log(`Loader still present after ${disappearTimeoutMs}ms — proceeding (positive check gates)`));
|
|
511
|
+
}
|
|
557
512
|
/**
|
|
558
513
|
* Reusable try-catch handler function for waiting and verifying elements
|
|
559
514
|
*/
|
|
@@ -561,66 +516,50 @@ class ElementUtils {
|
|
|
561
516
|
try {
|
|
562
517
|
await element.waitForDisplayed({ timeout });
|
|
563
518
|
console.log(`${label} found in search results:`, name);
|
|
564
|
-
}
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
565
521
|
console.error(`${label} not found in search results:`, name);
|
|
566
522
|
throw new Error(`${label} "${name}" not found in search results`);
|
|
567
523
|
}
|
|
568
524
|
}
|
|
569
|
-
|
|
570
525
|
/**
|
|
571
526
|
* Counts visible elements in a `$$` collection and asserts it equals `expected`.
|
|
572
527
|
* antd v6 keeps duplicate hidden DOM nodes around, so plain `.length` isn't reliable.
|
|
573
528
|
*/
|
|
574
|
-
async assertVisibleCount(
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
assert.equal(
|
|
584
|
-
visibleCount,
|
|
585
|
-
expected,
|
|
586
|
-
`Expected ${expected} visible ${label} but found ${visibleCount}`
|
|
587
|
-
);
|
|
588
|
-
console.log(`🟢 Verified visible ${label} count = ${visibleCount}`);
|
|
589
|
-
}
|
|
590
|
-
|
|
529
|
+
async assertVisibleCount(elements, expected, label = "elements") {
|
|
530
|
+
let visibleCount = 0;
|
|
531
|
+
for (const el of elements) {
|
|
532
|
+
if (await el.isDisplayed().catch(() => false))
|
|
533
|
+
visibleCount++;
|
|
534
|
+
}
|
|
535
|
+
assert_1.strict.equal(visibleCount, expected, `Expected ${expected} visible ${label} but found ${visibleCount}`);
|
|
536
|
+
console.log(`🟢 Verified visible ${label} count = ${visibleCount}`);
|
|
537
|
+
}
|
|
591
538
|
/**
|
|
592
539
|
* Polls `$$(selector)` until one match is displayed, skipping hidden duplicates
|
|
593
540
|
* (e.g. antd keeps inactive tab panes mounted in the DOM).
|
|
594
541
|
*/
|
|
595
|
-
async firstVisibleElement(selector
|
|
596
|
-
let found
|
|
597
|
-
await browser.waitUntil(
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
return true;
|
|
604
|
-
}
|
|
542
|
+
async firstVisibleElement(selector, timeout = 90000) {
|
|
543
|
+
let found;
|
|
544
|
+
await browser.waitUntil(async () => {
|
|
545
|
+
const elements = await $$(selector);
|
|
546
|
+
for (const element of elements) {
|
|
547
|
+
if (await element.isDisplayed()) {
|
|
548
|
+
found = element;
|
|
549
|
+
return true;
|
|
605
550
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
return found as WebdriverIO.Element;
|
|
551
|
+
}
|
|
552
|
+
return false;
|
|
553
|
+
}, { timeout, timeoutMsg: `No visible element matched selector: ${selector}` });
|
|
554
|
+
return found;
|
|
611
555
|
}
|
|
612
|
-
|
|
613
556
|
/**
|
|
614
557
|
* Races candidates and confirms the winner is stably displayed.
|
|
615
558
|
* After a candidate wins the race, waits stableMs then re-checks.
|
|
616
559
|
* If the winner disappeared, retries the full race with remaining timeout.
|
|
617
560
|
* Only throws when the total timeout is exhausted.
|
|
618
561
|
*/
|
|
619
|
-
async waitForStableVisible(
|
|
620
|
-
candidates: WebdriverIO.Element[],
|
|
621
|
-
timeout: number,
|
|
622
|
-
stableMs: number = 5000,
|
|
623
|
-
): Promise<WebdriverIO.Element> {
|
|
562
|
+
async waitForStableVisible(candidates, timeout, stableMs = 5000) {
|
|
624
563
|
const deadline = Date.now() + timeout;
|
|
625
564
|
while (true) {
|
|
626
565
|
const remaining = deadline - Date.now();
|
|
@@ -631,14 +570,14 @@ class ElementUtils {
|
|
|
631
570
|
for (const el of candidates) {
|
|
632
571
|
if (await el.isDisplayed()) {
|
|
633
572
|
await browser.pause(stableMs);
|
|
634
|
-
if (await el.isDisplayed())
|
|
573
|
+
if (await el.isDisplayed())
|
|
574
|
+
return el;
|
|
635
575
|
console.log(`waitForStableVisible: candidate "${el.selector}" disappeared — retrying race`);
|
|
636
576
|
break;
|
|
637
577
|
}
|
|
638
578
|
}
|
|
639
579
|
}
|
|
640
580
|
}
|
|
641
|
-
|
|
642
581
|
/**
|
|
643
582
|
* Retries an async step until it completes without throwing, up to `maxAttempts`
|
|
644
583
|
* times (default 3). The flow only moves ahead once the step is acknowledged
|
|
@@ -649,24 +588,17 @@ class ElementUtils {
|
|
|
649
588
|
* (e.g. refresh / re-trigger a search) before the next attempt. The last error is
|
|
650
589
|
* only re-thrown after every attempt is exhausted.
|
|
651
590
|
*/
|
|
652
|
-
async retryStep
|
|
653
|
-
action: () => Promise<T>,
|
|
654
|
-
options: {
|
|
655
|
-
maxAttempts?: number;
|
|
656
|
-
delayMs?: number;
|
|
657
|
-
label?: string;
|
|
658
|
-
beforeRetry?: (attempt: number) => Promise<void>;
|
|
659
|
-
} = {}
|
|
660
|
-
): Promise<T> {
|
|
591
|
+
async retryStep(action, options = {}) {
|
|
661
592
|
const { maxAttempts = 3, delayMs = 3000, label = "step", beforeRetry } = options;
|
|
662
|
-
let lastError
|
|
593
|
+
let lastError;
|
|
663
594
|
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
664
595
|
try {
|
|
665
596
|
console.log(`[retry] Attempt ${attempt}/${maxAttempts} for: '${label}'`);
|
|
666
597
|
const result = await action();
|
|
667
598
|
console.log(`[retry] '${label}' acknowledged on attempt ${attempt}/${maxAttempts}`);
|
|
668
599
|
return result;
|
|
669
|
-
}
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
670
602
|
lastError = error;
|
|
671
603
|
console.log(`[retry] '${label}' failed on attempt ${attempt}/${maxAttempts}: ${error}`);
|
|
672
604
|
if (attempt < maxAttempts) {
|
|
@@ -679,7 +611,6 @@ class ElementUtils {
|
|
|
679
611
|
}
|
|
680
612
|
throw new Error(`[retry] '${label}' failed after ${maxAttempts} attempts. Last error: ${lastError}`);
|
|
681
613
|
}
|
|
682
|
-
|
|
683
614
|
/**
|
|
684
615
|
* Guards a step's precondition: the element that the PREVIOUS step is expected to
|
|
685
616
|
* have produced must be present before the current step runs. If it is not displayed
|
|
@@ -688,36 +619,34 @@ class ElementUtils {
|
|
|
688
619
|
* its result. Returns true when the precondition holds (use the return value inside
|
|
689
620
|
* a retry's beforeRetry hook to decide whether re-confirmation is needed).
|
|
690
621
|
*/
|
|
691
|
-
async ensurePrecondition(element
|
|
622
|
+
async ensurePrecondition(element, label, timeout = 15000) {
|
|
692
623
|
try {
|
|
693
624
|
await element.waitForDisplayed({ timeout, interval: 500 });
|
|
694
625
|
console.log(`[precondition] OK — previous step output present: '${label}'`);
|
|
695
626
|
return true;
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
`The current step will not run. Underlying: ${error}`
|
|
700
|
-
);
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
throw new Error(`[precondition] FAILED — expected output from the previous step is not present: '${label}'. ` +
|
|
630
|
+
`The current step will not run. Underlying: ${error}`);
|
|
701
631
|
}
|
|
702
632
|
}
|
|
703
|
-
|
|
704
633
|
/**
|
|
705
634
|
* Selects all text in a React controlled input and clears it via Backspace,
|
|
706
635
|
* triggering React's onChange. Use instead of clearValue() for React inputs.
|
|
707
636
|
*/
|
|
708
|
-
async selectAllAndClear(element
|
|
637
|
+
async selectAllAndClear(element) {
|
|
709
638
|
await element.waitForEnabled({ timeout: 40000, interval: 1000 });
|
|
710
|
-
await browser.execute((el
|
|
639
|
+
await browser.execute((el) => el.select(), element);
|
|
711
640
|
await browser.keys("Backspace");
|
|
712
641
|
}
|
|
713
|
-
|
|
714
|
-
async clearByKeysAndType(element: WebdriverIO.Element, textToEnter: string) {
|
|
642
|
+
async clearByKeysAndType(element, textToEnter) {
|
|
715
643
|
await element.waitForDisplayed({ timeout: 20000 });
|
|
716
644
|
await element.waitForEnabled({ timeout: 40000, interval: 1000 });
|
|
717
645
|
await element.click();
|
|
718
646
|
for (let attempt = 0; attempt < 10; attempt++) {
|
|
719
647
|
const current = await element.getValue();
|
|
720
|
-
if (!current)
|
|
648
|
+
if (!current)
|
|
649
|
+
break;
|
|
721
650
|
await browser.keys("End");
|
|
722
651
|
await browser.keys(new Array(current.length + 2).fill("Backspace"));
|
|
723
652
|
}
|
|
@@ -725,7 +654,6 @@ class ElementUtils {
|
|
|
725
654
|
await element.addValue(textToEnter);
|
|
726
655
|
}
|
|
727
656
|
}
|
|
728
|
-
|
|
729
657
|
/**
|
|
730
658
|
* Moves the cursor to the absolute end of the currently focused contenteditable element
|
|
731
659
|
* (e.g. TinyMCE inline editor) using the DOM Selection API.
|
|
@@ -738,20 +666,20 @@ class ElementUtils {
|
|
|
738
666
|
* Runs in the current WebDriver frame context — call from inside the BeFree iframe
|
|
739
667
|
* after switchToIframe() so document refers to the iframe document.
|
|
740
668
|
*/
|
|
741
|
-
async moveCursorToEndOfContentEditable()
|
|
669
|
+
async moveCursorToEndOfContentEditable() {
|
|
742
670
|
await browser.execute(() => {
|
|
743
|
-
const el = document.activeElement
|
|
744
|
-
if (!el || !el.isContentEditable)
|
|
671
|
+
const el = document.activeElement;
|
|
672
|
+
if (!el || !el.isContentEditable)
|
|
673
|
+
return;
|
|
745
674
|
const range = document.createRange();
|
|
746
675
|
const sel = window.getSelection();
|
|
747
|
-
if (!sel)
|
|
676
|
+
if (!sel)
|
|
677
|
+
return;
|
|
748
678
|
range.selectNodeContents(el);
|
|
749
679
|
range.collapse(false); // collapse to end
|
|
750
680
|
sel.removeAllRanges();
|
|
751
681
|
sel.addRange(range);
|
|
752
682
|
});
|
|
753
683
|
}
|
|
754
|
-
|
|
755
684
|
}
|
|
756
|
-
|
|
757
|
-
export default new ElementUtils()
|
|
685
|
+
exports.default = new ElementUtils();
|