@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.
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @module helperFunctions
3
+ * @typedef {import('puppeteer').Page} Page
4
+ * @typedef {import('puppeteer').Browser} Browser
5
+ */
6
+ const config = require('config');
7
+ const strings = require('../features/app/multilingualStrings/multilingualStrings');
8
+ module.exports = {
9
+ /**
10
+ * Waits for a keypress event to continue the test execution.
11
+ * Please use only for local execution as it doesn't resolve automatically.
12
+ *
13
+ * @returns {Promise<void>} - A promise that resolves when a keypress event occurs.
14
+ */
15
+ waitForKeypress: async function () {
16
+ process.stdin.setRawMode(true);
17
+ return new Promise((resolve) =>
18
+ process.stdin.once('data', () => {
19
+ process.stdin.setRawMode(false);
20
+ resolve();
21
+ })
22
+ );
23
+ },
24
+
25
+ /**
26
+ * Generate random string with custom length
27
+ * @param length
28
+ * @returns {string}
29
+ */
30
+ generateRandomString: function (length) {
31
+ return Math.random()
32
+ .toString(36)
33
+ .substring(2, length + 2);
34
+ },
35
+
36
+ /**
37
+ * Wait until AJAX request is completed
38
+ * @param {Page} page
39
+ * @returns {Promise<void>}
40
+ */
41
+ waitForAjax: async function (page) {
42
+ const jsCode = "typeof jQuery === 'undefined' || (jQuery.active === 0 && jQuery(':animated').length === 0)";
43
+ await page.waitForFunction(jsCode);
44
+ },
45
+
46
+ /**
47
+ * Returns the translated variant of the inputted text or the text itself if there isn't a translation.
48
+ * @param text
49
+ * @returns {Promise<string>}
50
+ */
51
+ getMultilingualString: async function (text) {
52
+ const lang = config.has('language') ? config.get('language') : null;
53
+ let result;
54
+ if (lang) {
55
+ let string = strings.multilingualStrings(lang, text);
56
+ result = string ?? text;
57
+ } else {
58
+ result = text;
59
+ }
60
+ return result;
61
+ },
62
+
63
+ /**
64
+ * Retrieves the class name of an element based on a property in the config json.
65
+ * You can set directly the full class, partial or ID, but mind that it always resolves to
66
+ * the full className of that element.
67
+ *
68
+ * @param {Object} page - The page object representing the web page.
69
+ * @param {string} region - The region of the page to search for the element.
70
+ * @returns {Promise<string>} - A promise that resolves to the class name of the element.
71
+ */
72
+ getRegion: async function (page, region) {
73
+ const regionMap = config.get('regionMap');
74
+ const el = await page.waitForSelector(regionMap[region]);
75
+ return await (await el.getProperty('className')).jsonValue();
76
+ },
77
+
78
+ /**
79
+ * Assert that array is in alphabetical order
80
+ *
81
+ * @param {Array} arr
82
+ * @param {string|number} propKey - json property when element items are objects or array key for simple arrays
83
+ * @returns {boolean}
84
+ */
85
+ isArraySorted: function (arr, propKey) {
86
+ let sortedArr = arr;
87
+ sortedArr.sort((a, b) => a[propKey].localeCompare(b[propKey]));
88
+
89
+ return JSON.stringify(arr) === JSON.stringify(sortedArr);
90
+ },
91
+
92
+ /**
93
+ * Method which checks whether it's an AJAX request or normal page(doc) request.
94
+ * If it's an AJAX it adds hardcoded 2s wait to allow for element rendering, otherwise
95
+ * the next step waitForSelector is triggered before the AJAX completes, and it will never find the element
96
+ * because it uses the old page state.
97
+ * If it's a non-interactive(dropdown, checkbox, autocomplete option etc.) element please use the corresponding step.
98
+ * @param {Page} page
99
+ * @returns {Promise<void>}
100
+ */
101
+ afterClick: async function (page) {
102
+ // Listen for page or ajax requests
103
+ async function handleRequest(request) {
104
+ try {
105
+ if (['xhr', 'fetch'].includes(request.resourceType())) {
106
+ await page.waitForResponse(() => true, { timeout: 10000 });
107
+ return 'AJAX';
108
+ } else if (request.resourceType() === 'document') {
109
+ await page.waitForNavigation({ timeout: 10000 });
110
+ return 'Document';
111
+ } else {
112
+ // Simple wait for cases where the click was over element which changes via CSS/JS not request.
113
+ await new Promise((resolve) => setTimeout(resolve, 200));
114
+ return true;
115
+ }
116
+ } catch {
117
+ return 'Timeout';
118
+ }
119
+ }
120
+ page.once('request', async (request) => {
121
+ const isAjax = await handleRequest(request);
122
+ if (isAjax === 'AJAX') {
123
+ // Add wait after AJAX so that there is enough time to render the response from it
124
+ await new Promise((resolve) => setTimeout(resolve, 200));
125
+ }
126
+ });
127
+ },
128
+
129
+ /**
130
+ * Go back to original page (first tab)
131
+ * To be used when you have more than one tabs open, and you want to go back to the first.
132
+ * @param {Browser} browser
133
+ * @returns {Promise<Object>}
134
+ */
135
+ openOriginalTab: async function (browser) {
136
+ const pages = await browser.pages();
137
+ // Switch to the original/initial tab - [0]
138
+ // For complex handling of more than 2 tabs use switchToTab() method.
139
+ await pages[0].bringToFront(); // Switch to the original tab
140
+ return pages[0];
141
+ },
142
+
143
+ /**
144
+ * Switching between open tabs
145
+ * @param {Browser} browser
146
+ * @param {number} tabIndex - the number of the tab (first tab is 1, not 0 for better UX)
147
+ * @returns {Promise<Object>}
148
+ */
149
+ switchToTab: async function (browser, tabIndex) {
150
+ let pages = await browser.pages();
151
+ let tabNumber = Number(tabIndex);
152
+ if (tabNumber < 1) {
153
+ throw new Error('Please provide a valid tab number - 1,2,3 etc.');
154
+ }
155
+
156
+ let attempts = 0;
157
+ // Pages is an array, thus tab 1 is 0, tab 2 is 1 etc.
158
+ // We need this subtraction by 1 for both the loop and the return of the new page object.
159
+ const num = tabNumber - 1;
160
+ while (pages.length <= num && attempts < 40) {
161
+ await new Promise((resolve) => setTimeout(resolve, 100)); // wait for 100ms before checking again
162
+ pages = await browser.pages();
163
+ attempts++;
164
+ }
165
+
166
+ if (tabNumber > pages.length) {
167
+ throw new Error(`The opened tabs are ${pages.length}, you entered ${tabNumber}`);
168
+ }
169
+
170
+ await pages[num].bringToFront();
171
+ return pages[num];
172
+ },
173
+ /**
174
+ * Replace the incompatible chars from a URL with _ so that the string can be used in a filename.
175
+ * @param path
176
+ * @returns {Promise<string>}
177
+ */
178
+ prepareFileNameFromUrl: async function (path) {
179
+ const newUrl = new URL(path);
180
+ let pathName = newUrl.pathname;
181
+ return pathName.replace(/[^a-z0-9]/gi, '_').toLowerCase();
182
+ },
183
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * @module lighthouse
3
+ * @typedef {import('puppeteer').Page} Page
4
+ */
5
+ const dataStorage = require('./dataStorage');
6
+ const helper = require('./helperFunctions');
7
+
8
+ module.exports = {
9
+ /**
10
+ *
11
+ * @param {Page} page
12
+ * @param path
13
+ * @param scenarioName
14
+ * @returns {Promise<void>}
15
+ */
16
+ validatePageSpeed: async function (page, path, scenarioName) {
17
+ const { default: lighthouse, generateReport: report } = await import('lighthouse');
18
+ const { lhr } = await lighthouse(path, undefined, undefined, page);
19
+ const reportHtml = report(lhr, 'html');
20
+ const fileName = await helper.prepareFileNameFromUrl(path);
21
+ await dataStorage.createHtmlReport('LightHouse-' + scenarioName.slice(0, -1) + fileName, reportHtml);
22
+ },
23
+ };
@@ -0,0 +1,288 @@
1
+ /**
2
+ * @module mainFunctions
3
+ * @typedef {import('puppeteer').Page} Page
4
+ * @typedef {import('puppeteer').Browser} Browser
5
+ */
6
+ const config = require('config');
7
+
8
+ module.exports = {
9
+ /**
10
+ * Prepare the URL using the config to get the domain and then
11
+ * based on the path variable to either generate a full path or replace it
12
+ * when path is absolute URL.
13
+ * @param path
14
+ * @returns {Promise<string>}
15
+ */
16
+ prepareUrl: async function (path) {
17
+ if (path.startsWith('http')) {
18
+ return path;
19
+ }
20
+ let baseUrl = config.get('credentials.baseUrl').toString();
21
+ if (baseUrl.endsWith('/')) {
22
+ baseUrl = baseUrl.slice(0, -1);
23
+ }
24
+ if (path === '/' || path === 'homepage' || path === 'home') {
25
+ return baseUrl;
26
+ }
27
+ return baseUrl + path;
28
+ },
29
+
30
+ /**
31
+ * Return current page URL - absolute or relative
32
+ * @param {Page} page
33
+ * @param {boolean} absolute - set to true if you want to extract the full path (with domain)
34
+ * @returns {string}
35
+ */
36
+ extractPath: function (page, absolute = false) {
37
+ const url = new URL(page.url());
38
+ const href = url.href;
39
+ const origin = url.origin;
40
+ if (absolute) {
41
+ return href;
42
+ }
43
+ return href.replace(origin, '');
44
+ },
45
+
46
+ /**
47
+ * Visit a certain URL. Can be relative or absolute.
48
+ * The step also supports auto select of a cookie consent if configured.
49
+ * @param {Page} page
50
+ * @param path
51
+ * @returns {Promise<void>}
52
+ */
53
+ visitPath: async function (page, path) {
54
+ const url = await this.prepareUrl(path);
55
+ try {
56
+ await page.goto(url, { waitUntil: 'domcontentloaded' });
57
+ } catch (error) {
58
+ throw new Error(`The requested page cannot be opened!: ${error}`);
59
+ }
60
+ if (config.has('blockingCookie')) {
61
+ const selector = config.get('blockingCookie');
62
+ await new Promise(function (resolve) {
63
+ setTimeout(resolve, 1000);
64
+ });
65
+ const cookie = await page.$(selector);
66
+ if (cookie) {
67
+ await cookie.click();
68
+ }
69
+ }
70
+ },
71
+
72
+ /**
73
+ * Visit current page concatenated with another path.
74
+ * Example localhost.com/listing -> localhost.com/listing/page-1
75
+ * @param {Page} page
76
+ * @param plus
77
+ * @returns {Promise<void>}
78
+ */
79
+ visitCurrentPathPlus: async function (page, plus) {
80
+ if (!plus.startsWith('/')) {
81
+ throw new Error('The path alias must start with a slash!');
82
+ }
83
+ let path = page.url();
84
+ if (path.endsWith('/')) {
85
+ path = path.slice(0, -1);
86
+ }
87
+ await page.goto(path + plus, { waitUntil: 'domcontentloaded' });
88
+ },
89
+
90
+ /**
91
+ * Reload the current page
92
+ * @param {Page} page
93
+ * @returns {Promise<void>}
94
+ */
95
+ reloadPage: async function (page) {
96
+ await page.reload({ waitUntil: 'domcontentloaded' });
97
+ },
98
+
99
+ /**
100
+ * Reload the current page and add get params
101
+ * @param {Page} page
102
+ * @param params
103
+ * @returns {Promise<void>}
104
+ */
105
+ reloadPageWithParams: async function (page, params) {
106
+ const currentUrl = this.extractPath(page);
107
+ if (!params.startsWith('?')) {
108
+ throw new Error("Invalid get param provided. Use '?' as first character.");
109
+ }
110
+ const newPath = currentUrl + params;
111
+ await this.visitPath(page, newPath);
112
+ },
113
+
114
+ /**
115
+ * Validate whether the current page is the one you should be on
116
+ * @param {Page} page
117
+ * @param path
118
+ * @returns {void}
119
+ */
120
+ validatePath: function (page, path) {
121
+ const pathAlias = this.extractPath(page);
122
+ if (pathAlias !== path) {
123
+ throw new Error(`The current path ${pathAlias} does not match the expected: ${path}!`);
124
+ }
125
+ },
126
+
127
+ /**
128
+ * Validate the last path in an alias
129
+ * @param {Page} page
130
+ * @param path
131
+ * @returns {void}
132
+ */
133
+ validatePathEnding: function (page, path) {
134
+ let pathAlias = this.extractPath(page);
135
+ if (pathAlias.endsWith('/')) {
136
+ pathAlias = pathAlias.slice(0, -1);
137
+ }
138
+ const splitAlias = pathAlias.split('/');
139
+ let lastElement;
140
+ if (Array.isArray(splitAlias)) {
141
+ lastElement = splitAlias[splitAlias.length - 1];
142
+ }
143
+ if (lastElement !== path) {
144
+ throw new Error(`The last path alias of ${pathAlias} does not match the expected: ${path}!`);
145
+ }
146
+ },
147
+
148
+ /**
149
+ * Validate http response code
150
+ * @param {Page} page
151
+ * @param code
152
+ * @param path
153
+ * @returns {Promise<void>}
154
+ */
155
+ validateStatusCode: async function (page, code, path) {
156
+ let baseUrl = config.get('credentials.baseUrl');
157
+ if (baseUrl.endsWith('/')) {
158
+ baseUrl = baseUrl.slice(0, -1);
159
+ }
160
+ const response = await page.goto(baseUrl + path);
161
+ const statusCode = response.status();
162
+ if (statusCode !== Number(code)) {
163
+ throw new Error(`The status code of the response: ${statusCode} does not match the expected!`);
164
+ }
165
+ },
166
+
167
+ /**
168
+ * Open new tab and switch to it (manually open tab and load a page)
169
+ * @param {Browser} browser
170
+ * @param url
171
+ * @returns {Promise<Object>}
172
+ */
173
+ openNewTab: async function (browser, url) {
174
+ const page = await browser.newPage();
175
+ await this.visitPath(page, url);
176
+ // Get all pages
177
+ const pages = await browser.pages();
178
+ // Switch to the new tab
179
+ return pages[pages.length - 1];
180
+ },
181
+
182
+ /**
183
+ * Validate current page response headers.
184
+ * @param {Page} page
185
+ * @param header
186
+ * @param value
187
+ * @returns {Promise<void>}
188
+ */
189
+ validatePageResponseHeaders: async function (page, header, value) {
190
+ const refreshPage = await page.reload({ waitUntil: 'domcontentloaded' });
191
+ const responseHeaders = refreshPage.headers();
192
+ if (responseHeaders[header.toLowerCase()] !== value) {
193
+ throw new Error('Response headers do not match the requirement!');
194
+ }
195
+ },
196
+
197
+ /**
198
+ * Verify cookie existence by name
199
+ * @param {object} page
200
+ * @param {string} cookieName
201
+ * @param {boolean} presence
202
+ * @returns {Promise<void>}
203
+ */
204
+ verifyCookiePresence: async function (page, cookieName, presence) {
205
+ let result;
206
+ const jsCode = `(function (name) {
207
+ var dc = document.cookie;
208
+ var prefix = name + '=';
209
+ var begin = dc.indexOf('; ' + prefix);
210
+ if (begin == -1) {
211
+ begin = dc.indexOf(prefix);
212
+ if (begin != 0) return null;
213
+ }
214
+ else
215
+ {
216
+ begin += 2;
217
+ var end = document.cookie.indexOf(';', begin);
218
+ if (end == -1) {
219
+ end = dc.length;
220
+ }
221
+ }
222
+ return decodeURI(dc.substring(begin + prefix.length, end));
223
+ })('${cookieName}');`;
224
+ try {
225
+ result = await page.evaluate(jsCode);
226
+ } catch (error) {
227
+ throw new Error(`There was an error when evaluating the code. ${error}`);
228
+ }
229
+
230
+ if (result) {
231
+ if (!presence) {
232
+ throw new Error(`The cookie ${cookieName} is present, but it shouldn't!`);
233
+ }
234
+ } else if (!result) {
235
+ if (presence) {
236
+ throw new Error(`The cookie ${cookieName} is not present, but it should!`);
237
+ }
238
+ }
239
+ },
240
+ /**
241
+ * Sets the viewport of the given page to match the specified device's dimensions.
242
+ *
243
+ * @param {Object} page - The page object where the viewport should be set.
244
+ * @param {string} device - The name of the device whose viewport dimensions should be applied.
245
+ * @throws {Error} Throws an error if the specified device is not defined in the configuration.
246
+ */
247
+ setViewport: async function (page, device) {
248
+ const viewport = config.get('viewport');
249
+
250
+ if (!viewport[device]) {
251
+ throw new Error(
252
+ `Viewport for device "${device}" is not defined in config.\nAvailable devices are: ${Object.keys(viewport).join(', ')}`
253
+ );
254
+ }
255
+ await page.setViewport({
256
+ width: viewport[device]['width'],
257
+ height: viewport[device]['height'],
258
+ });
259
+ },
260
+ /**
261
+ * Handle browser alert dialog and validate its text
262
+ * @param {Page} page
263
+ * @param {boolean} accept - true to accept/confirm, false to dismiss/cancel
264
+ * @param {string} expectedText - the expected text in the dialog
265
+ * @returns {Promise<void>}
266
+ */
267
+ handleAlert: async function (page, accept, expectedText = null) {
268
+ try {
269
+ page.on('dialog', async (dialog) => {
270
+ if (expectedText !== null) {
271
+ const actualText = dialog.message();
272
+ if (actualText !== expectedText) {
273
+ throw new Error(
274
+ `Alert dialog text mismatch. Expected: "${expectedText}", Got: "${actualText}"`
275
+ );
276
+ }
277
+ }
278
+ if (accept) {
279
+ await dialog.accept();
280
+ } else {
281
+ await dialog.dismiss();
282
+ }
283
+ });
284
+ } catch (error) {
285
+ throw new Error(`Could not handle alert dialog: ${error}`);
286
+ }
287
+ },
288
+ };
@@ -0,0 +1,67 @@
1
+ const config = require('config');
2
+ const backStop = require('backstopjs');
3
+ const backStopConfig = require('../backStopData/backStopConfig.json');
4
+
5
+ module.exports = {
6
+ /**
7
+ *
8
+ * @returns {Object} - the backstop configuration object
9
+ */
10
+ backstopConfigPrepare: function () {
11
+ let newConfig = backStopConfig;
12
+ newConfig.id = process.env.NODE_CONFIG_ENV;
13
+ newConfig.viewports[0].width = Number(config.get('viewport.width'));
14
+ newConfig.viewports[0].height = Number(config.get('viewport.height'));
15
+ newConfig.engineOptions.args = config.get('args');
16
+ return newConfig;
17
+ },
18
+
19
+ /**
20
+ *
21
+ * @param command
22
+ * @param configObject
23
+ * @returns {Promise<void>}
24
+ */
25
+ runBackStop: async function (command, configObject) {
26
+ await backStop(command, { config: configObject })
27
+ .then(() => {
28
+ console.log(`${command} backstop run executed successfully!`);
29
+ // test successful
30
+ })
31
+ .catch((error) => {
32
+ throw new Error(error);
33
+ });
34
+ },
35
+
36
+ /**
37
+ *
38
+ * @param scenarioName
39
+ * @param path
40
+ * @param testCommand
41
+ * @returns {Promise<void>}
42
+ */
43
+ runBackStopSingleScenario: async function (scenarioName, path, testCommand) {
44
+ const newConfig = this.backstopConfigPrepare();
45
+ newConfig.scenarios[0].label = scenarioName;
46
+ newConfig.scenarios[0].url = path;
47
+ await this.runBackStop(testCommand, newConfig);
48
+ },
49
+ /**
50
+ *
51
+ * @param pages
52
+ * @param testCommand
53
+ * @returns {Promise<void>}
54
+ */
55
+ runBackstopMultiplePages: async function (pages, testCommand) {
56
+ const newConfig = this.backstopConfigPrepare();
57
+ newConfig.scenarios = [];
58
+ pages.forEach((page) => {
59
+ newConfig.scenarios.push({
60
+ label: page.label,
61
+ url: page.url,
62
+ // Add other scenario properties as needed...
63
+ });
64
+ });
65
+ await this.runBackStop(testCommand, newConfig);
66
+ },
67
+ };
@@ -0,0 +1,17 @@
1
+ // Cuppet Core Step Definitions
2
+ // This file exports all step definitions for use in consuming projects
3
+
4
+ module.exports = {
5
+ accessibilitySteps: require('./features/app/stepDefinitions/accessibilitySteps'),
6
+ apiSteps: require('./features/app/stepDefinitions/apiSteps'),
7
+ appiumSteps: require('./features/app/stepDefinitions/appiumSteps'),
8
+ generalSteps: require('./features/app/stepDefinitions/generalSteps'),
9
+ helperSteps: require('./features/app/stepDefinitions/helperSteps'),
10
+ iframeSteps: require('./features/app/stepDefinitions/iframeSteps'),
11
+ ifVisibleSteps: require('./features/app/stepDefinitions/ifVisibleSteps'),
12
+ lighthouseSteps: require('./features/app/stepDefinitions/lighthouseSteps'),
13
+ pageElements: require('./features/app/stepDefinitions/pageElements'),
14
+ pageElementsConfig: require('./features/app/stepDefinitions/pageElementsConfig'),
15
+ pageElementsJson: require('./features/app/stepDefinitions/pageElementsJson'),
16
+ visualRegressionSteps: require('./features/app/stepDefinitions/visualRegressionSteps')
17
+ };