@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,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
|
+
};
|