regressify 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/.browserslistrc +7 -0
- package/.editorconfig +14 -0
- package/.engine_scripts/auto-scroll.js +63 -0
- package/.engine_scripts/imageStub.jpg +0 -0
- package/.engine_scripts/package.json +11 -0
- package/.engine_scripts/playwright/actions.js +174 -0
- package/.engine_scripts/playwright/clickAndHoverHelper.js +43 -0
- package/.engine_scripts/playwright/embedFiles.js +28 -0
- package/.engine_scripts/playwright/interceptImages.js +31 -0
- package/.engine_scripts/playwright/loadCookies.js +26 -0
- package/.engine_scripts/playwright/login-user.js +143 -0
- package/.engine_scripts/playwright/onBefore.js +4 -0
- package/.engine_scripts/playwright/onReady.js +38 -0
- package/.engine_scripts/playwright/overrideCSS.js +39 -0
- package/.engine_scripts/puppet/clickAndHoverHelper.js +39 -0
- package/.engine_scripts/puppet/ignoreCSP.js +65 -0
- package/.engine_scripts/puppet/interceptImages.js +37 -0
- package/.engine_scripts/puppet/loadCookies.js +41 -0
- package/.engine_scripts/puppet/login-user.js +142 -0
- package/.engine_scripts/puppet/onBefore.js +4 -0
- package/.engine_scripts/puppet/onReady.js +32 -0
- package/.engine_scripts/puppet/overrideCSS.js +28 -0
- package/.engine_scripts/replacement-profiles-schema.json +32 -0
- package/.engine_scripts/scroll-top.js +27 -0
- package/.engine_scripts/test-schema.json +629 -0
- package/.eslintrc.cjs +23 -0
- package/.github/workflows/deploy.yml +37 -0
- package/.nvmrc +1 -0
- package/.prettierignore +2 -0
- package/.prettierrc +7 -0
- package/.prettierrc.js +8 -0
- package/.vscode/settings.json +57 -0
- package/LICENSE +21 -0
- package/README.md +32 -0
- package/alias.ps1 +44 -0
- package/bun.lockb +0 -0
- package/cli.js +76 -0
- package/generate_tests.js +39 -0
- package/package.json +44 -0
- package/src/config.ts +172 -0
- package/src/helpers.ts +40 -0
- package/src/index.ts +55 -0
- package/src/replacements.ts +34 -0
- package/src/scenarios.ts +21 -0
- package/src/types.ts +44 -0
- package/tsconfig.json +26 -0
- package/visual_tests/_cookies.yaml +21 -0
- package/visual_tests/_on-ready.js +3 -0
- package/visual_tests/_override.css +1 -0
- package/visual_tests/_replacement-profiles.yaml +6 -0
- package/visual_tests/_signing-in.yaml +7 -0
- package/visual_tests/_viewports.yaml +11 -0
- package/visual_tests/alloy.tests.yaml +6 -0
- package/visual_tests/color-blender.tests.yaml +62 -0
- package/visual_tests/form-reuse-scenarios.tests.yaml +38 -0
- package/visual_tests/form-submission.tests.yaml +59 -0
- package/visual_tests/frequently-changed-data.tests.yaml +22 -0
- package/visual_tests/sign-in.tests.yaml +6 -0
package/.browserslistrc
ADDED
package/.editorconfig
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
root = true
|
|
2
|
+
|
|
3
|
+
[*]
|
|
4
|
+
indent_style = space
|
|
5
|
+
indent_size = 2
|
|
6
|
+
charset = utf-8
|
|
7
|
+
trim_trailing_whitespace = true
|
|
8
|
+
insert_final_newline = true
|
|
9
|
+
end_of_line = lf
|
|
10
|
+
# editorconfig-tools is unable to ignore longs strings or urls
|
|
11
|
+
max_line_length = 150
|
|
12
|
+
|
|
13
|
+
[CHANGELOG.md]
|
|
14
|
+
indent_size = false
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
module.exports = async () => {
|
|
2
|
+
const SCROLL_DISTANCE = 100;
|
|
3
|
+
const SCROLL_DOWN_MAX = 100;
|
|
4
|
+
const SCROLL_TOP_MAX = 100;
|
|
5
|
+
|
|
6
|
+
if (!!window.visualTestScrollingBottom) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const scrollToBottom = async () => {
|
|
11
|
+
window.visualTestScrollingBottom = true;
|
|
12
|
+
let counter = 0;
|
|
13
|
+
let lastScrollY = 0;
|
|
14
|
+
await new Promise((resolve) => {
|
|
15
|
+
const timer = setInterval(() => {
|
|
16
|
+
if (window.innerHeight + Math.ceil(window.scrollY) >= document.body.offsetHeight || counter > SCROLL_DOWN_MAX) {
|
|
17
|
+
// you're at the bottom of the page
|
|
18
|
+
clearInterval(timer);
|
|
19
|
+
window.visualTestScrollingBottom = false;
|
|
20
|
+
resolve();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (lastScrollY >= Math.ceil(window.scrollY)) {
|
|
25
|
+
counter++;
|
|
26
|
+
} else {
|
|
27
|
+
lastScrollY = Math.ceil(window.scrollY);
|
|
28
|
+
counter = 0;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
window.scrollBy(0, SCROLL_DISTANCE);
|
|
32
|
+
}, 100);
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const scrollToTop = async () => {
|
|
37
|
+
await new Promise((resolve) => {
|
|
38
|
+
const timer = setInterval(() => {
|
|
39
|
+
if (!window.visualTestScrollingBottom) {
|
|
40
|
+
clearInterval(timer);
|
|
41
|
+
resolve();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
}, 100);
|
|
45
|
+
});
|
|
46
|
+
await new Promise((resolve) => {
|
|
47
|
+
let counter = 0;
|
|
48
|
+
const timer = setInterval(() => {
|
|
49
|
+
if (window.scrollY === 0 || counter > SCROLL_TOP_MAX) {
|
|
50
|
+
clearInterval(timer);
|
|
51
|
+
resolve();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
counter++;
|
|
56
|
+
window.scrollTo(0, 0);
|
|
57
|
+
}, 100);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await scrollToBottom();
|
|
62
|
+
await scrollToTop();
|
|
63
|
+
};
|
|
Binary file
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalkImport = import('chalk').then((m) => m.default);
|
|
4
|
+
|
|
5
|
+
module.exports = async (currentPage, scenario) => {
|
|
6
|
+
if (!scenario.actions) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const chalk = await chalkImport;
|
|
11
|
+
const logPrefix = chalk.yellow(`[${scenario.index} of ${scenario.total}] `);
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < scenario.actions.length; i++) {
|
|
14
|
+
let page = currentPage;
|
|
15
|
+
let action = scenario.actions[i];
|
|
16
|
+
|
|
17
|
+
if (!action) {
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (!!action.frame) {
|
|
22
|
+
const frames = typeof action.frame === 'string' ? [action.frame] : action.frame;
|
|
23
|
+
for (let j = 0; j < frames.length; j++) {
|
|
24
|
+
await page.waitForSelector(frames[j]);
|
|
25
|
+
const handle = await page.locator(frames[j]).elementHandle();
|
|
26
|
+
page = await handle.contentFrame();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (!!action.check) {
|
|
31
|
+
console.log(logPrefix + 'check:', action.check);
|
|
32
|
+
await page.waitForSelector(action.check);
|
|
33
|
+
await page.check(action.check);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!!action.click) {
|
|
37
|
+
console.log(logPrefix + 'Click:', action.click);
|
|
38
|
+
await page.waitForSelector(action.click);
|
|
39
|
+
await page.click(action.click);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!!action.focus) {
|
|
43
|
+
console.log(logPrefix + 'Focus:', action.focus);
|
|
44
|
+
await page.waitForSelector(action.focus);
|
|
45
|
+
let el = await page.locator(action.focus);
|
|
46
|
+
el.focus();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!!action.hide) {
|
|
50
|
+
console.log(logPrefix + 'Hide:', action.hide);
|
|
51
|
+
await page.waitForSelector(action.hide);
|
|
52
|
+
let el = await page.locator(action.hide);
|
|
53
|
+
await el.evaluate((node) => node.style.setProperty('visibility', 'hidden', 'important'));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!!action.hover) {
|
|
57
|
+
console.log(logPrefix + 'Hover:', action.hover);
|
|
58
|
+
await page.waitForSelector(action.hover);
|
|
59
|
+
await page.locator(action.hover).hover();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (!!action.input) {
|
|
63
|
+
if (typeof action.value != 'undefined' && !!action.file) {
|
|
64
|
+
throw '`input` action must not contains both `value` and `file`';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof action.value != 'undefined') {
|
|
68
|
+
console.log(logPrefix + 'Input:', action.input, action.value);
|
|
69
|
+
await page.waitForSelector(action.input);
|
|
70
|
+
await page.click(action.input);
|
|
71
|
+
let el = await page.locator(action.input);
|
|
72
|
+
|
|
73
|
+
if (!action.append) {
|
|
74
|
+
await el.evaluate((node) => (node.value = ''));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await el.type(action.value);
|
|
78
|
+
} else if (!!action.file) {
|
|
79
|
+
console.log(logPrefix + 'Input:', action.input, action.file);
|
|
80
|
+
await page.waitForSelector(action.input);
|
|
81
|
+
let el = await page.locator(action.input);
|
|
82
|
+
|
|
83
|
+
const files = typeof action.file === 'string' ? [action.file] : action.file;
|
|
84
|
+
let normalizedPaths = [];
|
|
85
|
+
|
|
86
|
+
files.forEach((file) => {
|
|
87
|
+
if (path.isAbsolute(file)) {
|
|
88
|
+
throw '`file` must be relative path to "visual_tests" folder';
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const filePath = path.join('./visual_tests', file);
|
|
92
|
+
if (!fs.existsSync(filePath)) {
|
|
93
|
+
throw `file does not exist: ${file}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
normalizedPaths.push(filePath);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (action.useFileChooser) {
|
|
100
|
+
const fileChooserPromise = page.waitForEvent('filechooser');
|
|
101
|
+
el.click();
|
|
102
|
+
const fileChooser = await fileChooserPromise;
|
|
103
|
+
await fileChooser.setFiles(normalizedPaths);
|
|
104
|
+
} else {
|
|
105
|
+
el.setInputFiles(normalizedPaths);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!!action.remove) {
|
|
111
|
+
console.log(logPrefix + 'Remove:', action.remove);
|
|
112
|
+
await page.waitForSelector(action.remove);
|
|
113
|
+
let el = await page.locator(action.hide);
|
|
114
|
+
await el.evaluate((node) => node.style.setProperty('display', 'none', 'important'));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (!!action.press) {
|
|
118
|
+
console.log(logPrefix + 'Press:', action.press);
|
|
119
|
+
await page.waitForSelector(action.press);
|
|
120
|
+
await page.locator(action.press).press(action.key);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (!!action.scroll) {
|
|
124
|
+
console.log(logPrefix + 'Scroll:', action.scroll);
|
|
125
|
+
await page.waitForSelector(action.scroll);
|
|
126
|
+
await page.evaluate((scrollToSelector) => {
|
|
127
|
+
document.querySelector(scrollToSelector).scrollIntoView();
|
|
128
|
+
}, action.scroll);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (!!action.select) {
|
|
132
|
+
console.log(logPrefix + 'Select:', action.select);
|
|
133
|
+
if (!!action.value && !!action.label) {
|
|
134
|
+
throw 'Select action must have only either `value` or `label`, not both.';
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!action.value && !action.label) {
|
|
138
|
+
throw 'Select action must have only either `value` or `label`';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await page.waitForSelector(action.select);
|
|
142
|
+
|
|
143
|
+
let el = await page.locator(action.select);
|
|
144
|
+
if (!!action.value) {
|
|
145
|
+
await el.selectOption(action.value);
|
|
146
|
+
} else if (!!action.label) {
|
|
147
|
+
el.selectOption({ label: action.label });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!!action.uncheck) {
|
|
152
|
+
console.log(logPrefix + 'uncheck:', action.uncheck);
|
|
153
|
+
await page.waitForSelector(action.uncheck);
|
|
154
|
+
await page.uncheck(action.uncheck);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!!action.wait) {
|
|
158
|
+
console.log(logPrefix + 'Wait:', action.wait);
|
|
159
|
+
let url = action.url;
|
|
160
|
+
if (!!url) {
|
|
161
|
+
if (!!scenario.getTestUrl) {
|
|
162
|
+
url = scenario.getTestUrl(url);
|
|
163
|
+
}
|
|
164
|
+
await page.waitForURL(url);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (parseInt(action.wait) > 0) {
|
|
168
|
+
await page.waitForTimeout(action.wait);
|
|
169
|
+
} else {
|
|
170
|
+
await page.waitForSelector(action.wait);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module.exports = async (page, scenario) => {
|
|
2
|
+
const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector;
|
|
3
|
+
const clickSelector = scenario.clickSelectors || scenario.clickSelector;
|
|
4
|
+
const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector;
|
|
5
|
+
const scrollToSelector = scenario.scrollToSelector;
|
|
6
|
+
const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int]
|
|
7
|
+
|
|
8
|
+
if (keyPressSelector) {
|
|
9
|
+
for (const keyPressSelectorItem of [].concat(keyPressSelector)) {
|
|
10
|
+
await page.waitForSelector(keyPressSelectorItem.selector);
|
|
11
|
+
await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (hoverSelector) {
|
|
16
|
+
for (const hoverSelectorIndex of [].concat(hoverSelector)) {
|
|
17
|
+
await page.waitForSelector(hoverSelectorIndex);
|
|
18
|
+
await page.hover(hoverSelectorIndex);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (clickSelector) {
|
|
23
|
+
for (const clickSelectorIndex of [].concat(clickSelector)) {
|
|
24
|
+
await page.waitForSelector(clickSelectorIndex);
|
|
25
|
+
await page.click(clickSelectorIndex);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (postInteractionWait) {
|
|
30
|
+
if (parseInt(postInteractionWait) > 0) {
|
|
31
|
+
await page.waitForTimeout(postInteractionWait);
|
|
32
|
+
} else {
|
|
33
|
+
await page.waitForSelector(postInteractionWait);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (scrollToSelector) {
|
|
38
|
+
await page.waitForSelector(scrollToSelector);
|
|
39
|
+
await page.evaluate(scrollToSelector => {
|
|
40
|
+
document.querySelector(scrollToSelector).scrollIntoView();
|
|
41
|
+
}, scrollToSelector);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const chalkImport = import('chalk').then((m) => m.default);
|
|
3
|
+
|
|
4
|
+
const embedCss = async (scenario, page) => {
|
|
5
|
+
if (scenario.useCssOverride) {
|
|
6
|
+
await require('./overrideCSS')(page, scenario);
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const embedJs = async (scenario, page) => {
|
|
11
|
+
const jsOnReadyPath = scenario.jsOnReadyPath;
|
|
12
|
+
const chalk = await chalkImport;
|
|
13
|
+
const logPrefix = chalk.yellow(`[${scenario.index} of ${scenario.total}] `);
|
|
14
|
+
|
|
15
|
+
if (!jsOnReadyPath) {
|
|
16
|
+
return;
|
|
17
|
+
} else if (!fs.existsSync(jsOnReadyPath)) {
|
|
18
|
+
console.log(logPrefix + 'File not exist: ' + jsOnReadyPath);
|
|
19
|
+
} else {
|
|
20
|
+
const jsOnReadyScript = fs.readFileSync(jsOnReadyPath, 'utf-8');
|
|
21
|
+
await page.evaluate(jsOnReadyScript).then(() => console.log(logPrefix + '`onReady` script executed for: ' + scenario.label));
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
module.exports = async (scenario, page) => {
|
|
26
|
+
await embedJs(scenario, page);
|
|
27
|
+
await embedCss(scenario, page);
|
|
28
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* INTERCEPT IMAGES
|
|
3
|
+
* Listen to all requests. If a request matches IMAGE_URL_RE
|
|
4
|
+
* then stub the image with data from IMAGE_STUB_URL
|
|
5
|
+
*
|
|
6
|
+
* Use this in an onBefore script E.G.
|
|
7
|
+
```
|
|
8
|
+
module.exports = async function(page, scenario) {
|
|
9
|
+
require('./interceptImages')(page, scenario);
|
|
10
|
+
}
|
|
11
|
+
```
|
|
12
|
+
*
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const path = require('path');
|
|
17
|
+
|
|
18
|
+
const IMAGE_URL_RE = /\.gif|\.jpg|\.png/i;
|
|
19
|
+
const IMAGE_STUB_URL = path.resolve(__dirname, '../../imageStub.jpg');
|
|
20
|
+
const IMAGE_DATA_BUFFER = fs.readFileSync(IMAGE_STUB_URL);
|
|
21
|
+
const HEADERS_STUB = {};
|
|
22
|
+
|
|
23
|
+
module.exports = async function (page, scenario) {
|
|
24
|
+
page.route(IMAGE_URL_RE, route => {
|
|
25
|
+
route.fulfill({
|
|
26
|
+
body: IMAGE_DATA_BUFFER,
|
|
27
|
+
headers: HEADERS_STUB,
|
|
28
|
+
status: 200
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const YAML = require('js-yaml');
|
|
3
|
+
const chalkImport = import('chalk').then((m) => m.default);
|
|
4
|
+
|
|
5
|
+
module.exports = async (browserContext, scenario) => {
|
|
6
|
+
let cookies = [];
|
|
7
|
+
const cookiePath = scenario.cookiePath;
|
|
8
|
+
const chalk = await chalkImport;
|
|
9
|
+
const logPrefix = chalk.yellow(`[${scenario.index} of ${scenario.total}] `);
|
|
10
|
+
|
|
11
|
+
// Read Cookies from File, if exists
|
|
12
|
+
if (!!cookiePath && fs.existsSync(cookiePath)) {
|
|
13
|
+
let content = fs.readFileSync(cookiePath);
|
|
14
|
+
if (cookiePath.endsWith('.json')) {
|
|
15
|
+
cookies = JSON.parse(content);
|
|
16
|
+
} else if (cookiePath.endsWith('.yaml') || cookiePath.endsWith('.yml')) {
|
|
17
|
+
cookies = YAML.load(content);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Add cookies to browser
|
|
22
|
+
browserContext.addCookies(cookies);
|
|
23
|
+
|
|
24
|
+
// console.log('Cookie state restored with:', JSON.stringify(cookies, null, 2));
|
|
25
|
+
console.log(logPrefix + 'Cookie state restored for: ' + scenario.label);
|
|
26
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const YAML = require('js-yaml');
|
|
3
|
+
const { clearInterval } = require('timers');
|
|
4
|
+
const globalData = {
|
|
5
|
+
domain1: {
|
|
6
|
+
isSiginingIn: false,
|
|
7
|
+
cookies: undefined,
|
|
8
|
+
domain: '',
|
|
9
|
+
},
|
|
10
|
+
};
|
|
11
|
+
// loginAndSaveCookies.ts
|
|
12
|
+
const getSignInData = async () => {
|
|
13
|
+
const signInPath = 'visual_tests/_signing-in.yaml';
|
|
14
|
+
let signInConfig = [];
|
|
15
|
+
// Read Cookies from File, if exists
|
|
16
|
+
if (!!signInPath && fs.existsSync(signInPath)) {
|
|
17
|
+
let content = fs.readFileSync(signInPath);
|
|
18
|
+
if (signInPath.endsWith('.json')) {
|
|
19
|
+
signInConfig = JSON.parse(content);
|
|
20
|
+
} else if (signInPath.endsWith('.yaml') || signInPath.endsWith('.yml')) {
|
|
21
|
+
signInConfig = YAML.load(content);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return signInConfig;
|
|
26
|
+
};
|
|
27
|
+
const sleep = (time) => {
|
|
28
|
+
return new Promise((r) => {
|
|
29
|
+
setTimeout(() => {
|
|
30
|
+
r(true);
|
|
31
|
+
}, time);
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
const waitForCondition = (condition = () => true, intervalCheckTime = 1000, maxCheckTime = 30000) => {
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
if (condition()) {
|
|
37
|
+
resolve(true);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
let totalCheckedTime = 0;
|
|
41
|
+
let intervalCheckId = setInterval(() => {
|
|
42
|
+
totalCheckedTime += intervalCheckTime;
|
|
43
|
+
if (condition()) {
|
|
44
|
+
clearInterval(intervalCheckId);
|
|
45
|
+
resolve(true);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (totalCheckedTime >= maxCheckTime) {
|
|
49
|
+
clearInterval(intervalCheckId);
|
|
50
|
+
resolve(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
}, intervalCheckTime);
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const executeSignIn = async (browserContext, domain) => {
|
|
58
|
+
try {
|
|
59
|
+
const pageUrl = domain;
|
|
60
|
+
const signInConfig = await getSignInData();
|
|
61
|
+
const sigInForPage = signInConfig.find((a) => a.domain.includes(pageUrl.host));
|
|
62
|
+
if (!sigInForPage) {
|
|
63
|
+
console.warn('not found signin config for page url', pageUrl);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('found signin config for page url', pageUrl);
|
|
68
|
+
const page = await browserContext.newPage();
|
|
69
|
+
|
|
70
|
+
console.log('navigate to sigin url', sigInForPage.loginUrl);
|
|
71
|
+
// Navigate to the login page
|
|
72
|
+
await page.goto(sigInForPage.loginUrl);
|
|
73
|
+
|
|
74
|
+
const inputData = async (selector, value) => {
|
|
75
|
+
await page.waitForSelector(selector);
|
|
76
|
+
console.log('found selector', selector);
|
|
77
|
+
let el = await page.locator(selector);
|
|
78
|
+
await el.fill(value);
|
|
79
|
+
};
|
|
80
|
+
await inputData(sigInForPage.userNameSelector, sigInForPage.userName);
|
|
81
|
+
await inputData(sigInForPage.passwordSelector, sigInForPage.password);
|
|
82
|
+
|
|
83
|
+
await page.waitForSelector(sigInForPage.loginButtonSelector);
|
|
84
|
+
console.log('found selector login button');
|
|
85
|
+
await Promise.all([page.click(sigInForPage.loginButtonSelector), page.waitForNavigation()]);
|
|
86
|
+
|
|
87
|
+
page.close();
|
|
88
|
+
|
|
89
|
+
console.log('sign in successfully', page.url());
|
|
90
|
+
// Save cookies after login
|
|
91
|
+
cookies = await browserContext.cookies();
|
|
92
|
+
const loggingCookies = cookies.map((a) => a.name).sort();
|
|
93
|
+
console.log('cookies', loggingCookies);
|
|
94
|
+
return cookies;
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('sign in error', error, domain);
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const waitForOtherSiginingIn = async (signInData) => {
|
|
101
|
+
const isSignedIn = await waitForCondition(() => !signInData.isSiginingIn, 1000, 10000);
|
|
102
|
+
return isSignedIn;
|
|
103
|
+
};
|
|
104
|
+
const getOrSetSignInData = async (browserContext, loginScenario) => {
|
|
105
|
+
const pageUrl = new URL(loginScenario.url);
|
|
106
|
+
const signInDomain = pageUrl.host;
|
|
107
|
+
const signInData = (globalData[signInDomain] = globalData[signInDomain] || {
|
|
108
|
+
isSiginingIn: false,
|
|
109
|
+
cookies: undefined,
|
|
110
|
+
signInDomain: new URL(`${pageUrl.protocol}//${pageUrl.host}`),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!signInData.isSiginingIn) {
|
|
114
|
+
console.log('first signin was called');
|
|
115
|
+
// lock current signin for domain
|
|
116
|
+
signInData.isSiginingIn = true;
|
|
117
|
+
signInData.cookies = await executeSignIn(browserContext, signInData.signInDomain);
|
|
118
|
+
// release lock for domain
|
|
119
|
+
signInData.isSiginingIn = false;
|
|
120
|
+
} else {
|
|
121
|
+
const isSignedInSuccessfully = await waitForOtherSiginingIn(signInData);
|
|
122
|
+
if (isSignedInSuccessfully) {
|
|
123
|
+
console.log('cookies is populated by others, reuse it');
|
|
124
|
+
} else {
|
|
125
|
+
console.log('cookies is not populated by others, start logging in user');
|
|
126
|
+
signInData.cookies = await executeSignIn(browserContext, signInData.signInDomain);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return signInData;
|
|
131
|
+
};
|
|
132
|
+
module.exports = async function (browserContext, loginScenario) {
|
|
133
|
+
if (!loginScenario.requiredLogin) {
|
|
134
|
+
console.log('testing scenario run as anonymous user ', loginScenario.label);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
console.log('testing scenario required login cookies ', loginScenario.label);
|
|
138
|
+
let signInData = await getOrSetSignInData(browserContext, loginScenario);
|
|
139
|
+
if (signInData?.cookies) {
|
|
140
|
+
browserContext.addCookies(signInData.cookies);
|
|
141
|
+
console.log('------login cookies restored for ', loginScenario.label);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const autoScroll = require('../auto-scroll');
|
|
2
|
+
const scrollTop = require('../scroll-top');
|
|
3
|
+
const chalkImport = import('chalk').then((m) => m.default);
|
|
4
|
+
|
|
5
|
+
module.exports = async (page, scenario, viewport, isReference, browserContext) => {
|
|
6
|
+
await require('./embedFiles')(scenario, page);
|
|
7
|
+
await page.evaluate(autoScroll);
|
|
8
|
+
const chalk = await chalkImport;
|
|
9
|
+
const logPrefix = chalk.yellow(`[${scenario.index} of ${scenario.total}] `);
|
|
10
|
+
|
|
11
|
+
page.on('load', async (data) => {
|
|
12
|
+
try {
|
|
13
|
+
await require('./embedFiles')(scenario, data);
|
|
14
|
+
await data.evaluate(require('../auto-scroll'));
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.log(logPrefix + error);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
console.log(logPrefix + 'SCENARIO > ' + scenario.label);
|
|
21
|
+
|
|
22
|
+
if (!!scenario.actions) {
|
|
23
|
+
await require('./actions')(page, scenario);
|
|
24
|
+
} else {
|
|
25
|
+
await require('./clickAndHoverHelper')(page, scenario);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!scenario.noScrollTop) {
|
|
29
|
+
await page.evaluate(scrollTop);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// add more ready handlers here...
|
|
33
|
+
// await page.waitForLoadState('load', { timeout: 5000 });
|
|
34
|
+
|
|
35
|
+
if (scenario.postInteractionWait) {
|
|
36
|
+
await page.waitForTimeout(scenario.postInteractionWait);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const chalkImport = import('chalk').then((m) => m.default);
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OVERRIDE CSS
|
|
7
|
+
* Apply this CSS to the loaded page, as a way to override styles.
|
|
8
|
+
*
|
|
9
|
+
* Use this in an onReady script E.G.
|
|
10
|
+
```
|
|
11
|
+
module.exports = async function(page, scenario) {
|
|
12
|
+
await require('./overrideCSS')(page, scenario);
|
|
13
|
+
}
|
|
14
|
+
```
|
|
15
|
+
*
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
module.exports = async (page, scenario) => {
|
|
19
|
+
const cssOverridePath = scenario.cssOverridePath;
|
|
20
|
+
const chalk = await chalkImport;
|
|
21
|
+
const logPrefix = chalk.yellow(`[${scenario.index} of ${scenario.total}] `);
|
|
22
|
+
|
|
23
|
+
if (!cssOverridePath) {
|
|
24
|
+
return;
|
|
25
|
+
} else if (!fs.existsSync(cssOverridePath)) {
|
|
26
|
+
console.log(logPrefix + 'File not exist: ' + cssOverridePath);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// const override = fs.readFileSync(cssOverridePath, 'utf-8');
|
|
31
|
+
|
|
32
|
+
// inject arbitrary css to override styles
|
|
33
|
+
await page.addStyleTag({
|
|
34
|
+
path: cssOverridePath,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
console.log(logPrefix + 'BACKSTOP_TEST_CSS_OVERRIDE injected for: ' + scenario.label);
|
|
38
|
+
// console.log(override);
|
|
39
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module.exports = async (page, scenario) => {
|
|
2
|
+
const hoverSelector = scenario.hoverSelectors || scenario.hoverSelector;
|
|
3
|
+
const clickSelector = scenario.clickSelectors || scenario.clickSelector;
|
|
4
|
+
const keyPressSelector = scenario.keyPressSelectors || scenario.keyPressSelector;
|
|
5
|
+
const scrollToSelector = scenario.scrollToSelector;
|
|
6
|
+
const postInteractionWait = scenario.postInteractionWait; // selector [str] | ms [int]
|
|
7
|
+
|
|
8
|
+
if (keyPressSelector) {
|
|
9
|
+
for (const keyPressSelectorItem of [].concat(keyPressSelector)) {
|
|
10
|
+
await page.waitForSelector(keyPressSelectorItem.selector);
|
|
11
|
+
await page.type(keyPressSelectorItem.selector, keyPressSelectorItem.keyPress);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (hoverSelector) {
|
|
16
|
+
for (const hoverSelectorIndex of [].concat(hoverSelector)) {
|
|
17
|
+
await page.waitForSelector(hoverSelectorIndex);
|
|
18
|
+
await page.hover(hoverSelectorIndex);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (clickSelector) {
|
|
23
|
+
for (const clickSelectorIndex of [].concat(clickSelector)) {
|
|
24
|
+
await page.waitForSelector(clickSelectorIndex);
|
|
25
|
+
await page.click(clickSelectorIndex);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (postInteractionWait) {
|
|
30
|
+
await page.waitForTimeout(postInteractionWait);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (scrollToSelector) {
|
|
34
|
+
await page.waitForSelector(scrollToSelector);
|
|
35
|
+
await page.evaluate(scrollToSelector => {
|
|
36
|
+
document.querySelector(scrollToSelector).scrollIntoView();
|
|
37
|
+
}, scrollToSelector);
|
|
38
|
+
}
|
|
39
|
+
};
|