@cuppet/core 1.0.0
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/LICENSE +201 -0
- package/README.md +183 -0
- package/features/app/appiumManager.js +58 -0
- package/features/app/browserManager.js +54 -0
- package/features/app/hooks.js +95 -0
- package/features/app/stepDefinitions/accessibilitySteps.js +17 -0
- package/features/app/stepDefinitions/apiSteps.js +52 -0
- package/features/app/stepDefinitions/appiumSteps.js +15 -0
- package/features/app/stepDefinitions/generalSteps.js +98 -0
- package/features/app/stepDefinitions/helperSteps.js +79 -0
- package/features/app/stepDefinitions/ifVisibleSteps.js +58 -0
- package/features/app/stepDefinitions/iframeSteps.js +61 -0
- package/features/app/stepDefinitions/lighthouseSteps.js +17 -0
- package/features/app/stepDefinitions/pageElements.js +208 -0
- package/features/app/stepDefinitions/pageElementsConfig.js +42 -0
- package/features/app/stepDefinitions/pageElementsJson.js +49 -0
- package/features/app/stepDefinitions/visualRegressionSteps.js +26 -0
- package/features/app/world.js +15 -0
- package/index.js +43 -0
- package/package.json +57 -0
- package/src/accessibilityTesting.js +44 -0
- package/src/apiFunctions.js +290 -0
- package/src/appiumTesting.js +79 -0
- package/src/dataStorage.js +290 -0
- package/src/elementInteraction.js +1003 -0
- package/src/helperFunctions.js +183 -0
- package/src/lighthouse.js +23 -0
- package/src/mainFunctions.js +288 -0
- package/src/visualRegression.js +67 -0
- package/stepDefinitions.js +17 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module elementInteraction
|
|
3
|
+
* @typedef {import('puppeteer').Page} Page
|
|
4
|
+
* @typedef {import('puppeteer').Browser} Browser
|
|
5
|
+
*/
|
|
6
|
+
const config = require('config');
|
|
7
|
+
const mime = require('mime');
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const helper = require('./helperFunctions');
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
/**
|
|
13
|
+
* Special handling in cases where you want a positive result if an element is missing.
|
|
14
|
+
* To be used in cases where element randomly shows/hides or the step is shared between profiles which have mixed
|
|
15
|
+
* support for that field.
|
|
16
|
+
* @param {Page} page
|
|
17
|
+
* @param selector
|
|
18
|
+
* @param skipFlag
|
|
19
|
+
* @returns {Promise<boolean>}
|
|
20
|
+
*/
|
|
21
|
+
customWaitForSkippableElement: async function (page, selector, skipFlag) {
|
|
22
|
+
try {
|
|
23
|
+
await page.waitForSelector(selector, { visible: true });
|
|
24
|
+
} catch {
|
|
25
|
+
if (skipFlag) {
|
|
26
|
+
// Exit from the function as the step was marked for skipping
|
|
27
|
+
return true;
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error(`Element with selector ${selector} not found!`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Click on an element
|
|
36
|
+
* @param {Page} page
|
|
37
|
+
* @param selector
|
|
38
|
+
* @param skip - flag to skip the element if it is not present in the DOM
|
|
39
|
+
* @returns {Promise<void>}
|
|
40
|
+
*/
|
|
41
|
+
click: async function (page, selector, skip = false) {
|
|
42
|
+
const skipped = await this.customWaitForSkippableElement(page, selector, skip);
|
|
43
|
+
if (skipped) {
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
const objectToCLick = await page.waitForSelector(selector, { visible: true });
|
|
47
|
+
const afterClickPromise = helper.afterClick(page);
|
|
48
|
+
try {
|
|
49
|
+
await objectToCLick.click({ delay: 150 });
|
|
50
|
+
} catch (error) {
|
|
51
|
+
throw new Error(`Could not click on element: ${selector}. Error: ${error}`);
|
|
52
|
+
}
|
|
53
|
+
// Resolve afterClick method
|
|
54
|
+
await afterClickPromise;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Click on multiple elements 1 by 1
|
|
59
|
+
* @param {Page} page
|
|
60
|
+
* @param selector
|
|
61
|
+
* @returns {Promise<void>}
|
|
62
|
+
*/
|
|
63
|
+
clickAllElements: async function (page, selector) {
|
|
64
|
+
await page.waitForSelector(selector);
|
|
65
|
+
const elements = await page.$$(selector);
|
|
66
|
+
for (let element of elements) {
|
|
67
|
+
await new Promise(function (resolve) {
|
|
68
|
+
setTimeout(resolve, 200);
|
|
69
|
+
});
|
|
70
|
+
await element.click({ delay: 300 });
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Press a single key
|
|
76
|
+
* @param {Page} page
|
|
77
|
+
* @param key - Name of key to press, such as ArrowLeft. See KeyInput for a list of all key names.
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
pressKey: async function (page, key) {
|
|
81
|
+
try {
|
|
82
|
+
await page.keyboard.press(key, { delay: 100 });
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error(`Couldn't press key ${key} on the keyboard`);
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Validate text in the page scripts
|
|
90
|
+
* @param {Page} page
|
|
91
|
+
* @param text
|
|
92
|
+
* @returns {Promise<void>}
|
|
93
|
+
*/
|
|
94
|
+
validateTextInScript: async function (page, text) {
|
|
95
|
+
try {
|
|
96
|
+
await page.waitForSelector('xpath/' + `//script[contains(text(),'${text}')]`);
|
|
97
|
+
} catch {
|
|
98
|
+
throw new Error(`Could not find: ${text} in page scripts.`);
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validate that specific text can be found in the page structured data
|
|
104
|
+
* @param {Page} page
|
|
105
|
+
* @param text
|
|
106
|
+
* @returns {Promise<void>}
|
|
107
|
+
*/
|
|
108
|
+
validateTextInSchemaOrg: async function (page, text) {
|
|
109
|
+
try {
|
|
110
|
+
await page.waitForSelector('script[type="application/ld+json"]');
|
|
111
|
+
await page.waitForSelector('xpath/' + `//script[contains(text(),'${text}')]`);
|
|
112
|
+
} catch {
|
|
113
|
+
throw new Error(`Could not find: ${text} in schema org.`);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Validate that specific text is missing in the structured data
|
|
119
|
+
* @param {Page} page
|
|
120
|
+
* @param text
|
|
121
|
+
* @returns {Promise<void>}
|
|
122
|
+
*/
|
|
123
|
+
validateTextNotInSchemaOrg: async function (page, text) {
|
|
124
|
+
await page.waitForSelector('script[type="application/ld+json"]');
|
|
125
|
+
const isTextInSchema = await page.$(
|
|
126
|
+
'xpath/' + `//script[@type="application/ld+json"][contains(text(),'${text}')]`
|
|
127
|
+
);
|
|
128
|
+
if (isTextInSchema) {
|
|
129
|
+
throw new Error(`${text} can be found in the schema org.`);
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Click on element by its text value
|
|
135
|
+
* @param {Page} page
|
|
136
|
+
* @param text
|
|
137
|
+
* @returns {Promise<void>}
|
|
138
|
+
*/
|
|
139
|
+
clickByText: async function (page, text) {
|
|
140
|
+
const objectToClick = await page.waitForSelector('xpath/' + `//body//*[text()[contains(.,'${text}')]]`);
|
|
141
|
+
const afterClickPromise = helper.afterClick(page);
|
|
142
|
+
try {
|
|
143
|
+
await objectToClick.click();
|
|
144
|
+
} catch {
|
|
145
|
+
throw new Error(`Could not click on element with text ${text}`);
|
|
146
|
+
}
|
|
147
|
+
// Resolve afterClick method
|
|
148
|
+
await afterClickPromise;
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Follow link by its name(text value). To be used on target="_self"
|
|
153
|
+
* @param {Page} page
|
|
154
|
+
* @param text
|
|
155
|
+
* @returns {Promise<void>}
|
|
156
|
+
*/
|
|
157
|
+
followLink: async function (page, text) {
|
|
158
|
+
const objectToClick = await page.waitForSelector('xpath/' + `//a[contains(text(), '${text}')]`);
|
|
159
|
+
const navigationPromise = page.waitForNavigation();
|
|
160
|
+
try {
|
|
161
|
+
await objectToClick.click();
|
|
162
|
+
await navigationPromise;
|
|
163
|
+
} catch {
|
|
164
|
+
throw new Error(`Could not click on the element with text: ${text}`);
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Click on the text of a link and expect it to open in a new tab. target="_blank"
|
|
170
|
+
* @param {Browser} browser
|
|
171
|
+
* @param {Page} page
|
|
172
|
+
* @param value - either text or css selector
|
|
173
|
+
* @param xpath - flag, whether to use xpath or not
|
|
174
|
+
* @returns {Promise<Object>}
|
|
175
|
+
*/
|
|
176
|
+
clickLinkOpenNewTab: async function (browser, page, value, xpath = true) {
|
|
177
|
+
let objectToClick;
|
|
178
|
+
if (xpath) {
|
|
179
|
+
const result = await helper.getMultilingualString(value);
|
|
180
|
+
objectToClick = await page.waitForSelector('xpath/' + `//body//*[text()[contains(.,'${result}')]]`);
|
|
181
|
+
} else {
|
|
182
|
+
objectToClick = await page.waitForSelector(value);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
await objectToClick.click();
|
|
187
|
+
} catch (error) {
|
|
188
|
+
throw new Error(`Could not click on the element. Reason: ${error}`);
|
|
189
|
+
}
|
|
190
|
+
// This is made for the standard case of clicking on a link of the first tab and opening second.
|
|
191
|
+
// If you are working on more than two tabs, please use switchToTab() method.
|
|
192
|
+
return await helper.switchToTab(browser, 2);
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Click on element by css selector and follow the popup window
|
|
197
|
+
* @param {Page} page
|
|
198
|
+
* @param selector
|
|
199
|
+
* @returns {Promise<Object>}
|
|
200
|
+
*/
|
|
201
|
+
clickElementOpenPopup: async function (page, selector) {
|
|
202
|
+
const objectToClick = await page.waitForSelector(selector, { visible: true });
|
|
203
|
+
// Set up a listener for the 'popup' event
|
|
204
|
+
const popupPromise = new Promise((resolve) => page.once('popup', resolve));
|
|
205
|
+
try {
|
|
206
|
+
await objectToClick.click();
|
|
207
|
+
} catch {
|
|
208
|
+
throw new Error(`Could not click on element with selector ${selector}`);
|
|
209
|
+
}
|
|
210
|
+
// Return the popup as a new page object
|
|
211
|
+
return popupPromise;
|
|
212
|
+
},
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Find link by text and validate it's href value
|
|
216
|
+
* @param {Page} page
|
|
217
|
+
* @param text
|
|
218
|
+
* @param href
|
|
219
|
+
* @returns {Promise<void>}
|
|
220
|
+
*/
|
|
221
|
+
validateHrefByText: async function (page, text, href) {
|
|
222
|
+
const objectToSelect = await page.waitForSelector('xpath/' + `//a[contains(text(), '${text}')]`);
|
|
223
|
+
const hrefElement = await (await objectToSelect.getProperty('href')).jsonValue();
|
|
224
|
+
if (hrefElement !== href) {
|
|
225
|
+
throw new Error(`The href of the link is ${hrefElement} and it is different from the expected ${href}!`);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Validate that element is rendered and visible by its css selector.
|
|
231
|
+
* Mind that hidden elements will not show (DOM existence is not enough for that step)
|
|
232
|
+
* @param {Page} page
|
|
233
|
+
* @param selector
|
|
234
|
+
* @param {boolean} isVisible - set to false for validating dom existence only
|
|
235
|
+
* @param {int} time
|
|
236
|
+
* @returns {Promise<void>}
|
|
237
|
+
*/
|
|
238
|
+
seeElement: async function (page, selector, isVisible = true, time = 3000) {
|
|
239
|
+
const options = {
|
|
240
|
+
visible: isVisible, // Wait for the element to be visible
|
|
241
|
+
timeout: time, // Maximum time to wait in milliseconds
|
|
242
|
+
};
|
|
243
|
+
try {
|
|
244
|
+
await page.waitForSelector(selector, options);
|
|
245
|
+
} catch {
|
|
246
|
+
throw new Error(`There is no element with selector: ${selector}!`);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Validate specific link attribute value. Find the link using its href value.
|
|
252
|
+
* @param {Page} page - current puppeteer tab
|
|
253
|
+
* @param href - link href value
|
|
254
|
+
* @param attribute - attribute you search for
|
|
255
|
+
* @param value - the expected value of that attribute
|
|
256
|
+
* @param skip - check method customWaitForSkippableElement() for more info
|
|
257
|
+
* @returns {Promise<boolean>}
|
|
258
|
+
*/
|
|
259
|
+
validateValueOfLinkAttributeByHref: async function (page, href, attribute, value, skip = false) {
|
|
260
|
+
const attrValue = await page.$eval(
|
|
261
|
+
`a[href="${href}"]`,
|
|
262
|
+
(el, attribute) => el.getAttribute(attribute),
|
|
263
|
+
attribute
|
|
264
|
+
);
|
|
265
|
+
if (!attrValue && skip === true) {
|
|
266
|
+
// Exit successfully if there is no value and the step is marked to be skipped
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
if (value !== attrValue) {
|
|
270
|
+
throw new Error(`The provided link "${href}" does not have an attribute with value: ${value}.`);
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Validate the value of certain attribute for a generic element by using its css selector to locate it.
|
|
276
|
+
* @param {Page} page
|
|
277
|
+
* @param selector
|
|
278
|
+
* @param attribute
|
|
279
|
+
* @param value
|
|
280
|
+
* @param skip
|
|
281
|
+
* @returns {Promise<boolean>}
|
|
282
|
+
*/
|
|
283
|
+
validateElementWithSelectorHasAttributeWithValue: async function (page, selector, attribute, value, skip = false) {
|
|
284
|
+
const skipped = await this.customWaitForSkippableElement(page, selector, skip);
|
|
285
|
+
if (skipped) {
|
|
286
|
+
return true;
|
|
287
|
+
}
|
|
288
|
+
const attrValue = await page.$eval(selector, (el, attribute) => el.getAttribute(attribute), attribute);
|
|
289
|
+
if (value !== attrValue) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
`The provided element with selector "${selector}" does not have an attribute with value: ${value}.`
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Same as the method above validateElementWithSelectorHasAttributeWithValue(), but using
|
|
298
|
+
* the text of the element to locate it.
|
|
299
|
+
* @param {Page} page
|
|
300
|
+
* @param text
|
|
301
|
+
* @param attribute
|
|
302
|
+
* @param value
|
|
303
|
+
* @returns {Promise<void>}
|
|
304
|
+
*/
|
|
305
|
+
validateValueOfElementAttributeByText: async function (page, text, attribute, value) {
|
|
306
|
+
const result = await helper.getMultilingualString(text);
|
|
307
|
+
const selector = 'xpath/' + `//body//*[text()[contains(.,'${result}')]]`;
|
|
308
|
+
await page.waitForSelector(selector);
|
|
309
|
+
const attrValue = await page.$eval(selector, (el, attribute) => el.getAttribute(attribute), attribute);
|
|
310
|
+
if (value !== attrValue) {
|
|
311
|
+
throw new Error(`The provided text "${result}" doesn't match element which attribute has value: ${value}.`);
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Element should not exist in the page DOM.
|
|
317
|
+
* @param {Page} page
|
|
318
|
+
* @param selector
|
|
319
|
+
* @param time
|
|
320
|
+
* @returns {Promise<void>}
|
|
321
|
+
*/
|
|
322
|
+
notSeeElement: async function (page, selector, time = 5000) {
|
|
323
|
+
const options = {
|
|
324
|
+
hidden: true,
|
|
325
|
+
timeout: time, // Maximum time to wait in milliseconds (default: 30000)
|
|
326
|
+
};
|
|
327
|
+
let isElementInPage = false;
|
|
328
|
+
try {
|
|
329
|
+
isElementInPage = await page.waitForSelector(selector, options);
|
|
330
|
+
} catch {
|
|
331
|
+
throw new Error('Element is visible!');
|
|
332
|
+
}
|
|
333
|
+
if (isElementInPage) {
|
|
334
|
+
throw new Error(`${selector} is hidden but can be found in the page source!`);
|
|
335
|
+
}
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Return the iframe to be used as a page object.
|
|
340
|
+
* @param {Page} page
|
|
341
|
+
* @param selector
|
|
342
|
+
* @returns {Promise<Frame>}
|
|
343
|
+
*/
|
|
344
|
+
getFrameBySelector: async function (page, selector) {
|
|
345
|
+
try {
|
|
346
|
+
await page.waitForSelector(selector);
|
|
347
|
+
const frameHandle = await page.$(selector);
|
|
348
|
+
return frameHandle.contentFrame();
|
|
349
|
+
} catch {
|
|
350
|
+
throw new Error(`iFrame with css selector: ${selector} cannot be found!`);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Validate visibility of text by using xpath to locate it.
|
|
356
|
+
* @param {Page} page
|
|
357
|
+
* @param text
|
|
358
|
+
* @param time
|
|
359
|
+
* @returns {Promise<void>}
|
|
360
|
+
*/
|
|
361
|
+
seeTextByXpath: async function (page, text, time = 6000) {
|
|
362
|
+
let result = await helper.getMultilingualString(text);
|
|
363
|
+
const options = {
|
|
364
|
+
visible: true, // Wait for the element to be visible (default: false)
|
|
365
|
+
timeout: time, // Maximum time to wait in milliseconds (default: 30000)
|
|
366
|
+
};
|
|
367
|
+
if (time > 6000 && !page['_name']) {
|
|
368
|
+
await new Promise(function (resolve) {
|
|
369
|
+
setTimeout(resolve, 500);
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
await page.waitForSelector('xpath/' + `//body//*[text()[contains(.,"${result}")]]`, options);
|
|
374
|
+
} catch (error) {
|
|
375
|
+
throw new Error(`Could not find text : ${result}. The error thrown is: ${error}`);
|
|
376
|
+
}
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Validate text existence in DOM using element textContent value.
|
|
381
|
+
* (can't validate whether you can see it with your eyes or not)
|
|
382
|
+
* @param {Page} page
|
|
383
|
+
* @param selector
|
|
384
|
+
* @param text
|
|
385
|
+
* @returns {Promise<void>}
|
|
386
|
+
*/
|
|
387
|
+
seeTextByElementHandle: async function (page, selector, text) {
|
|
388
|
+
const result = await helper.getMultilingualString(text);
|
|
389
|
+
await page.waitForSelector(selector);
|
|
390
|
+
let textContent = await page.$eval(selector, (element) => element.textContent.trim());
|
|
391
|
+
if (!textContent) {
|
|
392
|
+
textContent = await page.$eval(selector, (element) => element.value.trim());
|
|
393
|
+
}
|
|
394
|
+
if (textContent !== result) {
|
|
395
|
+
throw new Error(`Expected ${result} text, but found ${textContent} instead.`);
|
|
396
|
+
}
|
|
397
|
+
},
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Validate that text is visible in specific region (another element).
|
|
401
|
+
* To be used when multiple renders of the same text are shown on the page.
|
|
402
|
+
* @param {Page} page
|
|
403
|
+
* @param text
|
|
404
|
+
* @param region
|
|
405
|
+
* @returns {Promise<void>}
|
|
406
|
+
*/
|
|
407
|
+
seeTextInRegion: async function (page, text, region) {
|
|
408
|
+
const regionClass = await helper.getRegion(page, region);
|
|
409
|
+
const result = await helper.getMultilingualString(text);
|
|
410
|
+
try {
|
|
411
|
+
await page.waitForSelector(
|
|
412
|
+
'xpath/' + `//*[contains(@class,'${regionClass}') and .//text()[contains(.,"${result}")]]`
|
|
413
|
+
);
|
|
414
|
+
} catch {
|
|
415
|
+
throw new Error(`Cannot find ${result} in ${regionClass}!`);
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Hover element based on text content (useful for text inside spans, paragraphs etc. like menu links)
|
|
421
|
+
* @param {Page} page
|
|
422
|
+
* @param text
|
|
423
|
+
* @param region
|
|
424
|
+
* @returns {Promise<void>}
|
|
425
|
+
*/
|
|
426
|
+
hoverTextInRegion: async function (page, text, region) {
|
|
427
|
+
const regionClass = await helper.getRegion(page, region);
|
|
428
|
+
const result = await helper.getMultilingualString(text);
|
|
429
|
+
const selector = 'xpath/' + `//*[@class='${regionClass}']//*[text()='${result}']`;
|
|
430
|
+
try {
|
|
431
|
+
const element = await page.waitForSelector(selector);
|
|
432
|
+
const parentElementHandle = await page.evaluateHandle((el) => el.parentElement, element);
|
|
433
|
+
await parentElementHandle.hover();
|
|
434
|
+
} catch (error) {
|
|
435
|
+
throw new Error(error);
|
|
436
|
+
}
|
|
437
|
+
},
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Validate that the text is not rendered on the page.
|
|
441
|
+
* @param {Page} page
|
|
442
|
+
* @param text
|
|
443
|
+
* @returns {Promise<void>}
|
|
444
|
+
*/
|
|
445
|
+
notSeeText: async function (page, text) {
|
|
446
|
+
let result = await helper.getMultilingualString(text);
|
|
447
|
+
const isTextInDom = await page.$('xpath/' + `//*[text()[contains(.,'${result}')]]`);
|
|
448
|
+
// isVisible() is used for the cases where the text is in the DOM, but not visible
|
|
449
|
+
// If you need to NOT have it in the DOM - use notSeeElement() or extend this step with flag
|
|
450
|
+
const visibility = await isTextInDom?.isVisible();
|
|
451
|
+
if (visibility) {
|
|
452
|
+
throw new Error(`${text} can be found in the page source.`);
|
|
453
|
+
}
|
|
454
|
+
},
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Validate text value of certain element (input, p, span etc.)
|
|
458
|
+
* @param {Page} page
|
|
459
|
+
* @param text
|
|
460
|
+
* @param selector
|
|
461
|
+
* @returns {Promise<void>}
|
|
462
|
+
*/
|
|
463
|
+
validateTextInField: async function (page, text, selector) {
|
|
464
|
+
let result = await helper.getMultilingualString(text);
|
|
465
|
+
let value = '';
|
|
466
|
+
await page.waitForSelector(selector);
|
|
467
|
+
try {
|
|
468
|
+
const el = await page.$(selector);
|
|
469
|
+
const elementType = await page.evaluate((el) => el.tagName, el);
|
|
470
|
+
if (elementType.toLowerCase() === 'input' || elementType.toLowerCase() === 'textarea') {
|
|
471
|
+
value = await (await page.evaluateHandle((el) => el.value, el)).jsonValue();
|
|
472
|
+
} else {
|
|
473
|
+
value = await (await page.evaluateHandle((el) => el.innerText, el)).jsonValue();
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
throw new Error(error);
|
|
477
|
+
}
|
|
478
|
+
if (value !== result) {
|
|
479
|
+
throw new Error(`Value of element ${value} does not match the text ${result}`);
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Validate that text is actually shown/hidden on closing/opening of an accordion
|
|
485
|
+
* @param {Page} page
|
|
486
|
+
* @param cssSelector
|
|
487
|
+
* @param text
|
|
488
|
+
* @param isVisible
|
|
489
|
+
* @returns {Promise<void>}
|
|
490
|
+
*/
|
|
491
|
+
textVisibilityInAccordion: async function (page, cssSelector, text, isVisible) {
|
|
492
|
+
let result = await helper.getMultilingualString(text);
|
|
493
|
+
const el = await page.$(cssSelector);
|
|
494
|
+
if (el) {
|
|
495
|
+
const isShown = await (await page.evaluateHandle((el) => el.clientHeight, el)).jsonValue();
|
|
496
|
+
if (Boolean(isShown) !== isVisible) {
|
|
497
|
+
throw new Error('Element visibility does not match the requirement!');
|
|
498
|
+
}
|
|
499
|
+
if (isShown) {
|
|
500
|
+
const textValue = await (await page.evaluateHandle((el) => el.textContent.trim(), el)).jsonValue();
|
|
501
|
+
if (isVisible && textValue !== result) {
|
|
502
|
+
throw new Error(`Element text: ${textValue} does not match the expected: ${result}!`);
|
|
503
|
+
} else if (!isVisible && textValue === result) {
|
|
504
|
+
throw new Error(`Element text: ${textValue} is visible but it should not be!`);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
} else if (isVisible) {
|
|
508
|
+
throw new Error(`The element with ${cssSelector} is missing from the DOM tree.`);
|
|
509
|
+
}
|
|
510
|
+
},
|
|
511
|
+
|
|
512
|
+
/**
|
|
513
|
+
* Validate that text disappears in certain time from the page.
|
|
514
|
+
* Can be used for toasts, notifications etc.
|
|
515
|
+
* @param {Page} page
|
|
516
|
+
* @param text
|
|
517
|
+
* @param time
|
|
518
|
+
* @returns {Promise<void>}
|
|
519
|
+
*/
|
|
520
|
+
disappearText: async function (page, text, time) {
|
|
521
|
+
let result = await helper.getMultilingualString(text);
|
|
522
|
+
const options = {
|
|
523
|
+
visible: true, // Wait for the element to be visible (default: false)
|
|
524
|
+
timeout: 250, // 250ms and for that reason time is multiplied by 4 to add up to a full second.
|
|
525
|
+
};
|
|
526
|
+
for (let i = 0; i < time * 4; i++) {
|
|
527
|
+
try {
|
|
528
|
+
await page.waitForSelector('xpath/' + `//*[text()[contains(.,'${result}')]]`, options);
|
|
529
|
+
} catch {
|
|
530
|
+
console.log(`Element disappeared in ${time * 4}.`);
|
|
531
|
+
break;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
},
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Click on an element by its text in a certain region.
|
|
538
|
+
* To be used when there are multiple occurrences of that text.
|
|
539
|
+
* @param {Page} page
|
|
540
|
+
* @param text
|
|
541
|
+
* @param region
|
|
542
|
+
* @returns {Promise<void>}
|
|
543
|
+
*/
|
|
544
|
+
clickTextInRegion: async function (page, text, region) {
|
|
545
|
+
const regionClass = await helper.getRegion(page, region);
|
|
546
|
+
const result = await helper.getMultilingualString(text);
|
|
547
|
+
await page.waitForSelector('xpath/' + `//*[@class='${regionClass}']`);
|
|
548
|
+
const elements =
|
|
549
|
+
(await page.$$('xpath/' + `//*[@class='${regionClass}']//*[text()='${result}']`)) ||
|
|
550
|
+
(await page.$$('xpath/' + `//*[@class='${regionClass}']//*[contains(text(),'${result}')]`));
|
|
551
|
+
|
|
552
|
+
if (!elements?.[0]) {
|
|
553
|
+
throw new Error('Element not found!');
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const afterClickPromise = helper.afterClick(page);
|
|
557
|
+
await elements[0].click();
|
|
558
|
+
await afterClickPromise;
|
|
559
|
+
},
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Standard file upload into normal HTML file upload field
|
|
563
|
+
* @param {Page} page
|
|
564
|
+
* @param fileName
|
|
565
|
+
* @param selector
|
|
566
|
+
* @returns {Promise<void>}
|
|
567
|
+
*/
|
|
568
|
+
uploadFile: async function (page, fileName, selector) {
|
|
569
|
+
await page.waitForSelector(selector);
|
|
570
|
+
const element = await page.$(selector);
|
|
571
|
+
const filePath = config.has('filePath') ? config.get('filePath') : 'files/';
|
|
572
|
+
await element.uploadFile(filePath + fileName);
|
|
573
|
+
// Additional wait as the promise for file upload not always resolve on time when no slowMo is added.
|
|
574
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
575
|
+
},
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Drupal and dropzone specific file upload method.
|
|
579
|
+
* @param {Page} page
|
|
580
|
+
* @param fileName
|
|
581
|
+
* @param selector
|
|
582
|
+
* @returns {Promise<void>}
|
|
583
|
+
*/
|
|
584
|
+
uploadToDropzone: async function (page, fileName, selector) {
|
|
585
|
+
await page.waitForSelector(selector);
|
|
586
|
+
try {
|
|
587
|
+
const element = await page.$(selector);
|
|
588
|
+
const realSelector = await (await element.getProperty('id')).jsonValue();
|
|
589
|
+
const filePath = config.has('filePath') ? config.get('filePath') : 'files/';
|
|
590
|
+
const fullPath = filePath + fileName;
|
|
591
|
+
const mimeType = mime.getType(fullPath);
|
|
592
|
+
const contents = fs.readFileSync(fullPath, { encoding: 'base64' });
|
|
593
|
+
const jsCode = `
|
|
594
|
+
var url = "data:${mimeType};base64,${contents}"
|
|
595
|
+
var file;
|
|
596
|
+
fetch(url)
|
|
597
|
+
.then(response => response.blob())
|
|
598
|
+
.then(file => {
|
|
599
|
+
file.name = "${fileName}";
|
|
600
|
+
drupalSettings.dropzonejs.instances['${realSelector}'].instance.addFile(file)});`;
|
|
601
|
+
await page.evaluate(jsCode);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
throw new Error(error);
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* Put value in a field. It directly places the text like Ctrl+V(Paste) will do it.
|
|
609
|
+
* @param {Page} page
|
|
610
|
+
* @param selector
|
|
611
|
+
* @param data
|
|
612
|
+
* @param skip
|
|
613
|
+
* @returns {Promise<boolean>}
|
|
614
|
+
*/
|
|
615
|
+
fillField: async function (page, selector, data, skip = false) {
|
|
616
|
+
let result = await helper.getMultilingualString(data);
|
|
617
|
+
const skipped = await this.customWaitForSkippableElement(page, selector, skip);
|
|
618
|
+
if (skipped) {
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
try {
|
|
622
|
+
await page.$eval(selector, (el, name) => (el.value = name), result);
|
|
623
|
+
await new Promise(function (resolve) {
|
|
624
|
+
setTimeout(resolve, 500);
|
|
625
|
+
});
|
|
626
|
+
} catch (error) {
|
|
627
|
+
throw new Error(error);
|
|
628
|
+
}
|
|
629
|
+
},
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Simulates typing char by char in a field. Useful for fields which have some auto suggest/autocomplete logic behind it.
|
|
633
|
+
* @param {Page} page
|
|
634
|
+
* @param selector
|
|
635
|
+
* @param text
|
|
636
|
+
* @param skip
|
|
637
|
+
* @returns {Promise<boolean>}
|
|
638
|
+
*/
|
|
639
|
+
typeInField: async function (page, selector, text, skip = false) {
|
|
640
|
+
let result = await helper.getMultilingualString(text);
|
|
641
|
+
const skipped = await this.customWaitForSkippableElement(page, selector, skip);
|
|
642
|
+
if (skipped) {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
const el = await page.$(selector);
|
|
646
|
+
const elementType = await page.evaluate((el) => el.tagName, el);
|
|
647
|
+
if (elementType.toLowerCase() === 'input' || elementType.toLowerCase() === 'textarea') {
|
|
648
|
+
await page.$eval(selector, (input) => (input.value = ''));
|
|
649
|
+
await new Promise(function (resolve) {
|
|
650
|
+
setTimeout(resolve, 150);
|
|
651
|
+
});
|
|
652
|
+
try {
|
|
653
|
+
await page.type(selector, result, { delay: 250 });
|
|
654
|
+
} catch (error) {
|
|
655
|
+
throw new Error(`Cannot type into field due to ${error}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
},
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* Check or uncheck a checkbox. Do nothing if the direction matches the current state.
|
|
662
|
+
* @param {Page} page
|
|
663
|
+
* @param selector
|
|
664
|
+
* @param action
|
|
665
|
+
* @param skip
|
|
666
|
+
* @returns {Promise<boolean>}
|
|
667
|
+
*/
|
|
668
|
+
useCheckbox: async function (page, selector, action, skip = false) {
|
|
669
|
+
const skipped = await this.customWaitForSkippableElement(page, selector, skip);
|
|
670
|
+
if (skipped) {
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
const element = await page.$(selector);
|
|
674
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
675
|
+
const checked = await (await element.getProperty('checked')).jsonValue();
|
|
676
|
+
if ((!checked && action === 'select') || (checked && action === 'deselect')) {
|
|
677
|
+
await element.click();
|
|
678
|
+
} else if ((checked && action === 'select') || (!checked && action === 'deselect')) {
|
|
679
|
+
// Exit successfully when the requested action matches the current state
|
|
680
|
+
return true;
|
|
681
|
+
} else {
|
|
682
|
+
throw new Error(`Action: ${action} does not fit the current state of the checkbox!`);
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Write into CkEditor5 using its API.
|
|
688
|
+
* @param {Page} page
|
|
689
|
+
* @param selector
|
|
690
|
+
* @param text
|
|
691
|
+
* @returns {Promise<*>}
|
|
692
|
+
*/
|
|
693
|
+
writeInCkEditor5: async function (page, selector, text) {
|
|
694
|
+
const textValue = text === 'noText' ? '' : text;
|
|
695
|
+
const options = { hidden: true };
|
|
696
|
+
await page.waitForSelector(selector, options);
|
|
697
|
+
try {
|
|
698
|
+
const elementId = await page.$eval(selector, (el) => el.getAttribute('data-ckeditor5-id'));
|
|
699
|
+
let jsCode = `
|
|
700
|
+
(function () {
|
|
701
|
+
let textEditor = Drupal.CKEditor5Instances.get('${elementId}');
|
|
702
|
+
textEditor.setData('');
|
|
703
|
+
const docFrag = textEditor.model.change(writer => {
|
|
704
|
+
const p1 = writer.createElement('paragraph');
|
|
705
|
+
const docFrag = writer.createDocumentFragment();
|
|
706
|
+
writer.append(p1, docFrag);
|
|
707
|
+
writer.insertText('${textValue}', p1);
|
|
708
|
+
return docFrag;
|
|
709
|
+
}
|
|
710
|
+
);
|
|
711
|
+
textEditor.model.insertContent(docFrag);
|
|
712
|
+
})();
|
|
713
|
+
`;
|
|
714
|
+
return page.evaluate(jsCode);
|
|
715
|
+
} catch (error) {
|
|
716
|
+
throw new Error(`Cannot write into CkEditor5 field due to: ${error}!`);
|
|
717
|
+
}
|
|
718
|
+
},
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* Selects option by its html value.
|
|
722
|
+
* The method supports the skip property.
|
|
723
|
+
* @param {Page} page
|
|
724
|
+
* @param selector
|
|
725
|
+
* @param value
|
|
726
|
+
* @param skip
|
|
727
|
+
* @returns {Promise<boolean|void>}
|
|
728
|
+
*/
|
|
729
|
+
selectOptionByValue: async function (page, selector, value, skip = false) {
|
|
730
|
+
const skipped = await this.customWaitForSkippableElement(page, selector, skip);
|
|
731
|
+
if (skipped) {
|
|
732
|
+
return true;
|
|
733
|
+
}
|
|
734
|
+
const selectedValue = await page.select(selector, value);
|
|
735
|
+
if (selectedValue.length === 0) {
|
|
736
|
+
throw new Error(`The option ${value} is either missing or not selected!`);
|
|
737
|
+
}
|
|
738
|
+
},
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Selects option by its text value
|
|
742
|
+
* @param {Page} page
|
|
743
|
+
* @param selector
|
|
744
|
+
* @param text
|
|
745
|
+
* @returns {Promise<void>}
|
|
746
|
+
*/
|
|
747
|
+
selectOptionByText: async function (page, selector, text) {
|
|
748
|
+
let result = await helper.getMultilingualString(text);
|
|
749
|
+
await page.waitForSelector(selector);
|
|
750
|
+
const objectToSelect = await page.$('xpath/' + `//body//*[contains(text(), '${result}')]`);
|
|
751
|
+
if (objectToSelect) {
|
|
752
|
+
const value = await (await objectToSelect.getProperty('value')).jsonValue();
|
|
753
|
+
await page.select(selector, value);
|
|
754
|
+
} else {
|
|
755
|
+
throw new Error(`Could not find option with text: ${result}`);
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
|
|
759
|
+
/**
|
|
760
|
+
* Selects the first autocomplete option using the keyboard keys
|
|
761
|
+
* from a dropdown with auto-suggest.
|
|
762
|
+
* @param {Page} page
|
|
763
|
+
* @param text
|
|
764
|
+
* @param selector
|
|
765
|
+
* @returns {Promise<void>}
|
|
766
|
+
*/
|
|
767
|
+
selectOptionFirstAutocomplete: async function (page, text, selector) {
|
|
768
|
+
await page.waitForSelector(selector);
|
|
769
|
+
await page.type(selector, text, { delay: 150 });
|
|
770
|
+
await new Promise(function (resolve) {
|
|
771
|
+
setTimeout(resolve, 1000);
|
|
772
|
+
});
|
|
773
|
+
const el = await page.$(selector);
|
|
774
|
+
await el.focus();
|
|
775
|
+
await page.keyboard.press('ArrowDown', { delay: 100 });
|
|
776
|
+
await helper.waitForAjax(page);
|
|
777
|
+
await new Promise(function (resolve) {
|
|
778
|
+
setTimeout(resolve, 1000);
|
|
779
|
+
});
|
|
780
|
+
await page.keyboard.press('Enter', { delay: 100 });
|
|
781
|
+
},
|
|
782
|
+
|
|
783
|
+
/**
|
|
784
|
+
* Selects option from a dropdown using chosen JS field.
|
|
785
|
+
* @param {Page} page
|
|
786
|
+
* @param string
|
|
787
|
+
* @param selector
|
|
788
|
+
* @returns {Promise<void>}
|
|
789
|
+
*/
|
|
790
|
+
selectOptionFromChosen: async function (page, string, selector) {
|
|
791
|
+
await page.waitForSelector(selector);
|
|
792
|
+
const options = await page.$eval(selector, (select) => {
|
|
793
|
+
return Array.from(select.options).map((option) => ({
|
|
794
|
+
value: option.value,
|
|
795
|
+
text: option.text,
|
|
796
|
+
}));
|
|
797
|
+
});
|
|
798
|
+
const result = options.find(({ text }) => text === string);
|
|
799
|
+
const jsCode = `
|
|
800
|
+
jQuery('${selector}').val("${result.value}");
|
|
801
|
+
jQuery('${selector}').trigger("chosen:updated");
|
|
802
|
+
jQuery('${selector}').trigger("change");
|
|
803
|
+
`;
|
|
804
|
+
await page.evaluate(jsCode);
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
/**
|
|
808
|
+
* Method to verify if a dropdown text values are in alphabetical order
|
|
809
|
+
*
|
|
810
|
+
* @param {Object} page
|
|
811
|
+
* @param {string} selector
|
|
812
|
+
* @param {boolean} flag
|
|
813
|
+
* @returns {Promise<void>}
|
|
814
|
+
*/
|
|
815
|
+
iCheckIfDropdownOptionsAreInAlphabeticalOrder: async function (page, selector, flag) {
|
|
816
|
+
await page.waitForSelector(selector);
|
|
817
|
+
const options = await page.$eval(selector, (select) => {
|
|
818
|
+
return Array.from(select.options).map((option) => ({
|
|
819
|
+
value: option.value,
|
|
820
|
+
text: option.text,
|
|
821
|
+
}));
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
// Remove fist element if it's none (can be extended for other placeholders)
|
|
825
|
+
if (options[0].value === '_none') {
|
|
826
|
+
options.shift();
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
const isArraySorted = helper.isArraySorted(options, 'text');
|
|
830
|
+
|
|
831
|
+
if (Boolean(isArraySorted) !== flag) {
|
|
832
|
+
throw new Error(`Dropdown options are not sorted as expected`);
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
|
|
836
|
+
/**
|
|
837
|
+
* Method to verify if the checkbox text values are in alphabetical order
|
|
838
|
+
*
|
|
839
|
+
* @param {Object} page
|
|
840
|
+
* @param {string} selector
|
|
841
|
+
* @param {boolean} flag
|
|
842
|
+
* @returns {Promise<void>}
|
|
843
|
+
*/
|
|
844
|
+
iCheckIfCheckboxOptionsAreInAlphabeticalOrder: async function (page, selector, flag) {
|
|
845
|
+
await page.waitForSelector(selector);
|
|
846
|
+
const elements = await page.$$(selector);
|
|
847
|
+
|
|
848
|
+
const texts = await Promise.all(
|
|
849
|
+
elements.map((element) =>
|
|
850
|
+
element.getProperty('textContent').then((propertyHandle) => propertyHandle.jsonValue())
|
|
851
|
+
)
|
|
852
|
+
);
|
|
853
|
+
|
|
854
|
+
const isArraySorted = helper.isArraySorted(texts, 0);
|
|
855
|
+
|
|
856
|
+
if (Boolean(isArraySorted) !== flag) {
|
|
857
|
+
throw new Error(`The checkboxes are not sorted as expected`);
|
|
858
|
+
}
|
|
859
|
+
},
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* Sets date in a https://flatpickr.js.org/ based field.
|
|
863
|
+
* @param {Page} page
|
|
864
|
+
* @param selector
|
|
865
|
+
* @param value
|
|
866
|
+
* @returns {Promise<void>}
|
|
867
|
+
*/
|
|
868
|
+
setDateFlatpickr: async function (page, selector, value) {
|
|
869
|
+
await page.waitForSelector(selector);
|
|
870
|
+
try {
|
|
871
|
+
await page.$eval(selector, (el, date) => el._flatpickr.setDate(`${date}`, true), value);
|
|
872
|
+
} catch (error) {
|
|
873
|
+
throw new Error(`Cannot set date due to ${error}!`);
|
|
874
|
+
}
|
|
875
|
+
},
|
|
876
|
+
|
|
877
|
+
/**
|
|
878
|
+
* Scrolls element to the top of the page using cssSelector
|
|
879
|
+
* @param {Page} page
|
|
880
|
+
* @param cssSelector
|
|
881
|
+
* @returns {Promise<void>}
|
|
882
|
+
*/
|
|
883
|
+
scrollElementToTop: async function (page, cssSelector) {
|
|
884
|
+
await page.waitForSelector(cssSelector);
|
|
885
|
+
try {
|
|
886
|
+
const el = await page.$(cssSelector);
|
|
887
|
+
await page.evaluate((el) => el.scrollIntoView(true), el);
|
|
888
|
+
} catch (error) {
|
|
889
|
+
throw new Error(error);
|
|
890
|
+
}
|
|
891
|
+
},
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Sets value into codemirror field
|
|
895
|
+
* @param {Page} page
|
|
896
|
+
* @param cssSelector
|
|
897
|
+
* @param value
|
|
898
|
+
* @returns {Promise<void>}
|
|
899
|
+
*/
|
|
900
|
+
setValueInCodeMirrorField: async function (page, cssSelector, value) {
|
|
901
|
+
let result = await helper.getMultilingualString(value);
|
|
902
|
+
await page.waitForSelector(cssSelector);
|
|
903
|
+
try {
|
|
904
|
+
const jsCode = `
|
|
905
|
+
(function () {
|
|
906
|
+
const textArea = document.querySelector('${cssSelector}');
|
|
907
|
+
let editor = CodeMirror.fromTextArea(textArea);
|
|
908
|
+
editor.getDoc().setValue("${result}");
|
|
909
|
+
})();
|
|
910
|
+
`;
|
|
911
|
+
await page.evaluate(jsCode);
|
|
912
|
+
} catch (error) {
|
|
913
|
+
throw new Error(error);
|
|
914
|
+
}
|
|
915
|
+
},
|
|
916
|
+
|
|
917
|
+
/**
|
|
918
|
+
* Sets value in a datetime picker field and triggers the change event
|
|
919
|
+
* @param {Page} page
|
|
920
|
+
* @param {string} selector - CSS selector for the datetime picker input
|
|
921
|
+
* @param {string} value - Datetime value in format 'YYYY/MM/DD HH:mm'
|
|
922
|
+
* @returns {Promise<void>}
|
|
923
|
+
*/
|
|
924
|
+
setDateTimePickerValue: async function (page, selector, value) {
|
|
925
|
+
await page.waitForSelector(selector);
|
|
926
|
+
try {
|
|
927
|
+
const jsCode = `
|
|
928
|
+
(function () {
|
|
929
|
+
const input = document.querySelector('${selector}');
|
|
930
|
+
input.value = '${value}';
|
|
931
|
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
932
|
+
})();
|
|
933
|
+
`;
|
|
934
|
+
await page.evaluate(jsCode);
|
|
935
|
+
} catch (error) {
|
|
936
|
+
throw new Error(`Cannot set datetime picker value due to: ${error}`);
|
|
937
|
+
}
|
|
938
|
+
},
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* Helper function to format date with specified format and relative range
|
|
942
|
+
* @param {string} format - Date format (e.g. 'd-m-Y H:i', 'Y-M-d H')
|
|
943
|
+
* @param {string} range - Relative date range (e.g. '+2 days', '+5 days')
|
|
944
|
+
* @returns {string} - Formatted date string
|
|
945
|
+
* @example
|
|
946
|
+
* formatDateWithRange('d-m-Y H:i', '+2 days') // returns "15-03-2024 14:30"
|
|
947
|
+
* formatDateWithRange('Y-M-d H', '+5 days') // returns "2024-03-18 14"
|
|
948
|
+
*/
|
|
949
|
+
formatDateWithRange: function (format, range) {
|
|
950
|
+
// Parse relative date
|
|
951
|
+
const match = range.match(/^\+(\d+)\s*(day|days|hour|hours|minute|minutes)$/i);
|
|
952
|
+
if (!match) {
|
|
953
|
+
throw new Error(`Invalid range format. Expected format: '+N days/hours/minutes'`);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const amount = parseInt(match[1]);
|
|
957
|
+
const unit = match[2].toLowerCase();
|
|
958
|
+
|
|
959
|
+
const date = new Date();
|
|
960
|
+
|
|
961
|
+
switch (unit) {
|
|
962
|
+
case 'day':
|
|
963
|
+
case 'days':
|
|
964
|
+
date.setDate(date.getDate() + amount);
|
|
965
|
+
break;
|
|
966
|
+
case 'hour':
|
|
967
|
+
case 'hours':
|
|
968
|
+
date.setHours(date.getHours() + amount);
|
|
969
|
+
break;
|
|
970
|
+
case 'minute':
|
|
971
|
+
case 'minutes':
|
|
972
|
+
date.setMinutes(date.getMinutes() + amount);
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Format the date according to the specified format
|
|
977
|
+
const formatMap = {
|
|
978
|
+
d: String(date.getDate()).padStart(2, '0'),
|
|
979
|
+
m: String(date.getMonth() + 1).padStart(2, '0'),
|
|
980
|
+
Y: date.getFullYear(),
|
|
981
|
+
H: String(date.getHours()).padStart(2, '0'),
|
|
982
|
+
i: String(date.getMinutes()).padStart(2, '0'),
|
|
983
|
+
M: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][date.getMonth()],
|
|
984
|
+
};
|
|
985
|
+
|
|
986
|
+
return format.replace(/[dmyYHMi]/g, (match) => formatMap[match] || match);
|
|
987
|
+
},
|
|
988
|
+
|
|
989
|
+
/**
|
|
990
|
+
* Set value in datetime picker with specified format and relative range
|
|
991
|
+
* @param {Page} page
|
|
992
|
+
* @param {string} selector - CSS selector for the datetime picker
|
|
993
|
+
* @param {string} format - Date format (e.g. 'd-m-Y H:i', 'Y-M-d H')
|
|
994
|
+
* @param {string} range - Relative date range (e.g. '+2 days', '+5 days')
|
|
995
|
+
* @returns {Promise<void>}
|
|
996
|
+
* @example
|
|
997
|
+
* await setDateTimePickerWithFormat(page, '#start_date', 'd-m-Y H:i', '+2 days')
|
|
998
|
+
*/
|
|
999
|
+
setDateTimePickerWithFormat: async function (page, selector, format, range) {
|
|
1000
|
+
const formattedDate = this.formatDateWithRange(format, range);
|
|
1001
|
+
await this.setDateTimePickerValue(page, selector, formattedDate);
|
|
1002
|
+
},
|
|
1003
|
+
};
|