@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/index.js ADDED
@@ -0,0 +1,43 @@
1
+ // Core Cuppet Framework Components
2
+ // This package provides the core testing framework functionality
3
+
4
+ // Export main function modules
5
+ const elementInteraction = require('./src/elementInteraction');
6
+ const dataStorage = require('./src/dataStorage');
7
+ const mainFunctions = require('./src/mainFunctions');
8
+ const helperFunctions = require('./src/helperFunctions');
9
+ const apiFunctions = require('./src/apiFunctions');
10
+ const appiumTesting = require('./src/appiumTesting');
11
+ const accessibilityTesting = require('./src/accessibilityTesting');
12
+ const lighthouse = require('./src/lighthouse');
13
+ const visualRegression = require('./src/visualRegression');
14
+
15
+ // Export managers
16
+ const BrowserManager = require('./features/app/browserManager');
17
+ const AppiumManager = require('./features/app/appiumManager');
18
+
19
+ // Export step definitions
20
+ const stepDefinitions = require('./stepDefinitions');
21
+
22
+ module.exports = {
23
+ // Core functions
24
+ elementInteraction,
25
+ dataStorage,
26
+ mainFunctions,
27
+ helperFunctions,
28
+ apiFunctions,
29
+ appiumTesting,
30
+ accessibilityTesting,
31
+ lighthouse,
32
+ visualRegression,
33
+
34
+ // Managers
35
+ BrowserManager,
36
+ AppiumManager,
37
+
38
+ // Step definitions
39
+ stepDefinitions,
40
+
41
+ // Version info
42
+ version: require('./package.json').version
43
+ };
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@cuppet/core",
3
+ "version": "1.0.0",
4
+ "description": "Core testing framework components for Cuppet - BDD framework based on Cucumber and Puppeteer",
5
+ "main": "index.js",
6
+ "files": [
7
+ "src/",
8
+ "features/",
9
+ "index.js",
10
+ "stepDefinitions.js"
11
+ ],
12
+ "keywords": [
13
+ "testing",
14
+ "bdd",
15
+ "cucumber",
16
+ "puppeteer",
17
+ "appium",
18
+ "automation",
19
+ "e2e"
20
+ ],
21
+ "author": "Miroslav Rusev",
22
+ "license": "ISC",
23
+ "peerDependencies": {
24
+ "@cucumber/cucumber": "^11.0.0",
25
+ "puppeteer": "^24.0.1",
26
+ "config": "^3.3.9"
27
+ },
28
+ "dependencies": {
29
+ "@supercharge/strings": "^2.0.0",
30
+ "axios": "^1.8.2",
31
+ "backstopjs": "^6.3.23",
32
+ "chai": "^4.3.7",
33
+ "lighthouse": "^12.1.0",
34
+ "mime": "^3.0.0",
35
+ "moment": "^2.30.1",
36
+ "pa11y": "^8.0.0",
37
+ "pa11y-reporter-html": "^2.0.0",
38
+ "xml2js": "^0.6.2"
39
+ },
40
+ "devDependencies": {
41
+ "@wdio/globals": "^9.14.0",
42
+ "appium": "^2.18.0",
43
+ "appium-uiautomator2-driver": "^4.2.3",
44
+ "webdriverio": "9.12.7"
45
+ },
46
+ "scripts": {
47
+ "test": "echo \"Error: no test specified\" && exit 1"
48
+ },
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/MiroslavRusev/cuppet-core.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/MiroslavRusev/cuppet-core/issues"
55
+ },
56
+ "homepage": "https://github.com/MiroslavRusev/cuppet-core#readme"
57
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @module accessibilityTesting
3
+ * @typedef {import('puppeteer').Page} Page
4
+ * @typedef {import('puppeteer').Browser} Browser
5
+ */
6
+ const config = require('config');
7
+ const storage = require('./dataStorage');
8
+ const helper = require('./helperFunctions');
9
+ const pa11y = require('pa11y');
10
+ const htmlReporter = require('pa11y-reporter-html');
11
+
12
+ module.exports = {
13
+ /**
14
+ * Method to validate if certain path meets the criteria from the config.
15
+ * Please use the config json files to set options.
16
+ * You can find more info at - https://github.com/pa11y/pa11y#configuration
17
+ * @param {Browser} browser - puppeteer browser object
18
+ * @param {Page} page - puppeteer page object
19
+ * @param scenarioName - the current scenario name
20
+ * @param path - the path of the page which accessibility will be tested
21
+ * @throws Error
22
+ * @returns {Promise<void>}
23
+ */
24
+ validatePageAccessibility: async function (browser, page, scenarioName, path) {
25
+ const pa11yConfig = config.get('pa11yConfig');
26
+ const configOptions = {
27
+ ...pa11yConfig,
28
+ browser: browser,
29
+ page: page,
30
+ };
31
+ if (!path.startsWith('http')) {
32
+ throw new Error('Only absolute paths are allowed!');
33
+ }
34
+
35
+ const results = await pa11y(path, configOptions);
36
+ const fileName = await helper.prepareFileNameFromUrl(path);
37
+ // make the URL ready for filepath usage
38
+ if (results.issues) {
39
+ const html = await htmlReporter.results(results);
40
+ await storage.createHtmlReport('Pa11y-' + scenarioName.slice(0, -1) + fileName, html);
41
+ throw new Error(`${path} page has accessibility issues. HTML report has been generated!`);
42
+ }
43
+ },
44
+ };
@@ -0,0 +1,290 @@
1
+ const axios = require('axios');
2
+ const config = require('config');
3
+ const storage = require('./dataStorage');
4
+ const xml2js = require('xml2js');
5
+ const assert = require('chai').assert;
6
+
7
+ module.exports = {
8
+ /** @type {object} */
9
+ response: null,
10
+ /** @type {object} */
11
+ request: null,
12
+
13
+ /**
14
+ * Prepare path for API test usage
15
+ * @param url - It can be absolute/relative path or even placeholder for saved variable
16
+ * @returns {Promise<*>} - Returns a working path
17
+ */
18
+ prepareUrl: async function (url) {
19
+ const path = await storage.checkForMultipleVariables(url);
20
+ if (!path.startsWith('http') && config.has('api.baseApiUrl')) {
21
+ return config.get('api.baseApiUrl') + path;
22
+ }
23
+ return path;
24
+ },
25
+
26
+ /**
27
+ * Function used to generate the needed headers for each request
28
+ * @async
29
+ * @function setHeaders
30
+ * @param headers
31
+ * @param {boolean} defaultHeadersFlag
32
+ * @returns {Promise<Object>} - Returns an object with the headers
33
+ */
34
+ setHeaders: async function (headers = {}, defaultHeadersFlag = false) {
35
+ if (!defaultHeadersFlag) {
36
+ return headers;
37
+ }
38
+
39
+ let defaultHeaders = {
40
+ 'Content-Type': 'application/json',
41
+ Accept: 'application/json',
42
+ };
43
+ if (config.has('api.x-api-key')) {
44
+ defaultHeaders['X-Api-Key'] = config.get('api.x-api-key');
45
+ }
46
+ if (config.has('api.Authorization')) {
47
+ defaultHeaders['X-Api-Key'] = config.get('api.Authorization');
48
+ }
49
+ if (headers && defaultHeaders) {
50
+ defaultHeaders = {
51
+ ...defaultHeaders,
52
+ ...headers,
53
+ };
54
+ }
55
+ return defaultHeaders;
56
+ },
57
+
58
+ /**
59
+ * Prepare and set the basic auth (if needed).
60
+ * This method supports if the API and the website have different basic auth.
61
+ * @async
62
+ * @function setHeaders
63
+ * @returns {Promise<{Object}>}
64
+ */
65
+ setBasicAuth: async function () {
66
+ let basicAuth = {};
67
+ if (config.has('api.authUser')) {
68
+ basicAuth = {
69
+ username: config.get('api.authUser'),
70
+ password: config.get('api.authPass'),
71
+ };
72
+ } else if (config.has('basicAuth.authUser')) {
73
+ basicAuth = {
74
+ username: config.get('basicAuth.authUser'),
75
+ password: config.get('basicAuth.authPass'),
76
+ };
77
+ }
78
+ return basicAuth;
79
+ },
80
+
81
+ /**
82
+ * Sends an HTTP request using axios.
83
+ *
84
+ * @async
85
+ * @function sendRequest
86
+ * @param {string} method - The HTTP method to use for the request.
87
+ * @param {string} [url="/"] - The URL to send the request to. Defaults to "/".
88
+ * @param {Object} [headers={}] - An object containing HTTP headers to include with the request. Defaults to an empty object.
89
+ * @param {Object} [data={}] - An object containing data to send in the body of the request. Defaults to an empty object.
90
+ * @returns {Promise<Object>} Returns a Promise that resolves to the response from the server.
91
+ * @throws {Error} Throws an error if the request fails.
92
+ */
93
+ sendRequest: async function (method, url = '/', headers = {}, data = {}) {
94
+ const apiUrl = await this.prepareUrl(url);
95
+ const requestHeaders = await this.setHeaders(headers, true);
96
+ const auth = await this.setBasicAuth();
97
+ if (this.request) {
98
+ data = this.request;
99
+ }
100
+ try {
101
+ this.response = await axios.request({
102
+ url: apiUrl,
103
+ method: method.toLowerCase(),
104
+ ...(Object.keys(auth).length && { auth }),
105
+ // The data is conditionally added to the request, because it's not used with GET requests and creates conflict.
106
+ // The following checks if data object is not empty, returns data object if not empty or skip if empty.
107
+ ...(Object.keys(data).length && { data }),
108
+ headers: requestHeaders,
109
+ });
110
+ return this.response;
111
+ } catch (error) {
112
+ throw new Error(`Request failed with: ${error}`);
113
+ }
114
+ },
115
+
116
+ /**
117
+ * Replace placeholders of type %var% and prepare request body
118
+ * @async
119
+ * @function prepareRequestBody
120
+ * @param body - the request body needs to be passed in string format
121
+ * @returns {Promise<Object>} - returns the request body object
122
+ */
123
+ prepareRequestBody: async function (body) {
124
+ const preparedBody = await storage.checkForMultipleVariables(body);
125
+ this.request = JSON.parse(preparedBody);
126
+ return this.request;
127
+ },
128
+
129
+ /**
130
+ * Put values in request 1 by 1
131
+ * Example object: {
132
+ * property: value
133
+ * }
134
+ * @async
135
+ * @function prepareRequestBody
136
+ * @param value - the value of the new property
137
+ * @param property - the name of the property
138
+ * @param object - parent object name
139
+ * @returns {Promise<Object>} - returns the request body object
140
+ */
141
+ iPutValuesInRequestBody: async function (value, property, object) {
142
+ const preparedValue = await storage.checkForVariable(value);
143
+ if (!this.request) {
144
+ this.request = {};
145
+ }
146
+ this.request[object][property] = preparedValue;
147
+ return this.request;
148
+ },
149
+
150
+ /**
151
+ * This step is used to validate the status code of the response
152
+ * @param code
153
+ * @returns {Promise<void>}
154
+ */
155
+ validateResponseCode: async function (code) {
156
+ if (this.response.status !== Number(code)) {
157
+ throw new Error(`Response code is different than expected, code: ${this.response.status}`);
158
+ }
159
+ },
160
+
161
+ /**
162
+ * Use this step whether the response is of type array or object
163
+ * @param type
164
+ * @returns {Promise<void>}
165
+ */
166
+ validateResponseType: async function (type) {
167
+ await assert.typeOf(this.response.data, type, `Response is not an ${type}`);
168
+ },
169
+
170
+ /**
171
+ * Asynchronously checks if a property of the response data is of a specified type.
172
+ *
173
+ * @async
174
+ * @function propertyIs
175
+ * @param {string} property - The property of the response data to check.
176
+ * @param {string} type - The type that the property should be.
177
+ * @throws {Error} - Will throw an error if the property is not of the specified type.
178
+ */
179
+ propertyIs: async function (property, type) {
180
+ const value = this.response.data[property];
181
+ await assert.typeOf(value, type, `The property is not an ${type}`);
182
+ },
183
+
184
+ /**
185
+ * Validate value of property from the http response.
186
+ * @param property
187
+ * @param expectedValue
188
+ * @returns {Promise<void>}
189
+ */
190
+ propertyHasValue: async function (property, expectedValue) {
191
+ const actualValue = await this.getPropertyValue(property);
192
+ assert.strictEqual(actualValue, expectedValue, `Property "${property}" does not have the expected value`);
193
+ },
194
+
195
+ /**
196
+ * @async
197
+ * @function iRememberVariable
198
+ * @param property - the name of the JSON property, written in root.parent.child syntax
199
+ * @param variable - the name of the variable to which it will be stored
200
+ * @throws {Error} - if no property is found in the response data
201
+ * @returns {Promise<void>}
202
+ */
203
+ iRememberVariable: async function (property, variable) {
204
+ const propValue = await this.getPropertyValue(property);
205
+ await storage.iStoreVariableWithValueToTheJsonFile(propValue, variable);
206
+ },
207
+
208
+ /**
209
+ * Go through the response object and return the value of specific property
210
+ * @param property - name of the property. For nested structure use -> parent.child1.child2 etc.
211
+ * @returns {Promise<*>}
212
+ */
213
+ getPropertyValue: async function (property) {
214
+ const response = this.response.data;
215
+ const keys = property.split('.');
216
+ let value = response;
217
+ for (let key of keys) {
218
+ value = value[key];
219
+ }
220
+ if (!value) {
221
+ throw new Error(`Value with property: ${property} is not found!`);
222
+ }
223
+ return value;
224
+ },
225
+
226
+ /**
227
+ * Load custom json file and make a request body from it
228
+ * @param path
229
+ * @returns {Promise<Object>}
230
+ */
231
+ createRequestBodyFromFile: async function (path) {
232
+ this.request = storage.getJsonFile(path);
233
+ return this.request;
234
+ },
235
+
236
+ /**
237
+ * Send request to an endpoint and validate whether the response is valid xml.
238
+ * @param url
239
+ * @returns {Promise<void>}
240
+ */
241
+ validateXMLEndpoint: async function (url) {
242
+ const xmlUrl = await this.prepareUrl(url);
243
+ let response;
244
+ try {
245
+ const auth = await this.setBasicAuth();
246
+ response = await axios.request({
247
+ url: xmlUrl,
248
+ method: 'get',
249
+ ...(Object.keys(auth).length && { auth }),
250
+ });
251
+ } catch (error) {
252
+ throw new Error(`Request failed with: ${error}`);
253
+ }
254
+
255
+ const isValid = await xml2js.parseStringPromise(response.data);
256
+ if (!isValid) {
257
+ throw new Error('XML is not valid!');
258
+ }
259
+ },
260
+
261
+ /**
262
+ *
263
+ * @param {string} method - method - GET,POST, PUT etc.
264
+ * @param {string} currentUrl - url of the page/endpoint
265
+ * @param {object} reqHeaders - request headers
266
+ * @param {string} resHeaders - response headers
267
+ * @param {boolean} flag - direction of presence (true/false)
268
+ * @returns {Promise<void>}
269
+ */
270
+ validateResponseHeader: async function (method, currentUrl, reqHeaders, resHeaders, flag) {
271
+ const prepareUrl = await this.prepareUrl(currentUrl);
272
+ const requestHeaders = await this.setHeaders(reqHeaders);
273
+ const auth = await this.setBasicAuth();
274
+ let response;
275
+ try {
276
+ response = await axios.request({
277
+ url: prepareUrl,
278
+ method: method,
279
+ ...(Object.keys(auth).length && { auth }),
280
+ headers: requestHeaders,
281
+ });
282
+ } catch (error) {
283
+ throw new Error(`Request failed with: ${error}`);
284
+ }
285
+ const hasProperty = Object.prototype.hasOwnProperty.call(response.headers, resHeaders.toLowerCase());
286
+ if (hasProperty !== flag) {
287
+ throw new Error('The response headers are different than expected!');
288
+ }
289
+ },
290
+ };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * @module appiumTesting
3
+ */
4
+ const config = require('config');
5
+
6
+ module.exports = {
7
+ /**
8
+ * Handles UiSelector case as it requires special prefixing
9
+ * @param {string} selector
10
+ * @returns {string} - Either the original selector or a UiSelector formatted string
11
+ */
12
+ prepareSelector: function (selector) {
13
+ if (selector.startsWith('.')) {
14
+ // If the selector starts with ., treat it as a UiSelector
15
+ // For example .text('Example Text') becomes android='new UiSelector().text("Example Text")'
16
+ return `android=new UiSelector()${selector}`;
17
+ } else {
18
+ return selector; // Otherwise, return the selector as is
19
+ }
20
+ },
21
+
22
+ /**
23
+ * @param {import('@wdio/globals').driver} driver - The WebDriverIO appium instance.
24
+ * @param {string} androidPackage - The name of the Android package.
25
+ * @param {string} activity - The name of the Android activity.
26
+ * @returns {Promise<void>} - Resolves when the session is reloaded successfully.
27
+ * @throws Will throw an error if the element is not found or not clickable.
28
+ */
29
+ reloadSession: async function (driver, androidPackage, activity) {
30
+ const appiumCapabilities = config.get('appiumCapabilities');
31
+ const capabilities = {
32
+ ...appiumCapabilities,
33
+ 'appium:appPackage': androidPackage,
34
+ 'appium:appActivity': activity,
35
+ };
36
+ // Reload the session with the updated capabilities
37
+ await driver.reloadSession(capabilities);
38
+ },
39
+
40
+ /**
41
+ * @param {import('@wdio/globals')} driver - The WebDriverIO appium instance.
42
+ * @param {string} selector - The selector of the element to click.
43
+ * Possible values for selector: https://webdriver.io/docs/selectors/
44
+ * - A UiSelector string, e.g. '.text("Example Text")'
45
+ * - An ID selector (e.g. 'com.example:id/button1')
46
+ * - A class name selector (e.g. 'android.widget.Button')
47
+ * - An accessibility ID selector (e.g. '~button1')
48
+ * @returns {Promise<void>} - Resolves when the element is clicked successfully.
49
+ * @throws Will throw an error if the element is not found or not clickable.
50
+ */
51
+ clickElement: async function (driver, selector) {
52
+ try {
53
+ // $ and $$ are not async functions, as Wdio uses lazy loading.
54
+ // The actual element is not fetched until an action is performed like .click(), .getText(), .isDisplayed(), etc.
55
+ const element = driver.$(selector);
56
+ await element.waitForDisplayed({ timeout: 5000 });
57
+ await element.click();
58
+ } catch (error) {
59
+ throw new Error(`Error clicking element with selector ${selector}: ${error}`);
60
+ }
61
+ },
62
+
63
+ /**
64
+ * @param {import('@wdio/globals').driver} driver - The WebDriverIO appium instance.
65
+ * @param {string} selector - The selector of the element.
66
+ * @returns {Promise<void>} - Resolves when the element is scrolled into view successfully.
67
+ * @throws Will throw an error if the element is not found.
68
+ */
69
+ scrollToElement: async function (driver, selector) {
70
+ try {
71
+ await driver.findElement(
72
+ '-android uiautomator',
73
+ `new UiScrollable(new UiSelector().scrollable(true)).scrollIntoView(new UiSelector().${selector})`
74
+ );
75
+ } catch (error) {
76
+ throw new Error(`Cannot find element with selector ${selector}: ${error}`);
77
+ }
78
+ },
79
+ };