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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IGNORE CSP HEADERS
|
|
3
|
+
* Listen to all requests. If a request matches scenario.url
|
|
4
|
+
* then fetch the request again manually, strip out CSP headers
|
|
5
|
+
* and respond to the original request without CSP headers.
|
|
6
|
+
* Allows `ignoreHTTPSErrors: true` BUT... requires `debugWindow: true`
|
|
7
|
+
*
|
|
8
|
+
* see https://github.com/GoogleChrome/puppeteer/issues/1229#issuecomment-380133332
|
|
9
|
+
* this is the workaround until Page.setBypassCSP lands... https://github.com/GoogleChrome/puppeteer/pull/2324
|
|
10
|
+
*
|
|
11
|
+
* @param {REQUEST} request
|
|
12
|
+
* @return {VOID}
|
|
13
|
+
*
|
|
14
|
+
* Use this in an onBefore script E.G.
|
|
15
|
+
```
|
|
16
|
+
module.exports = async function(page, scenario) {
|
|
17
|
+
require('./removeCSP')(page, scenario);
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
*
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const fetch = require('node-fetch');
|
|
24
|
+
const https = require('https');
|
|
25
|
+
const agent = new https.Agent({
|
|
26
|
+
rejectUnauthorized: false
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
module.exports = async function (page, scenario) {
|
|
30
|
+
const intercept = async (request, targetUrl) => {
|
|
31
|
+
const requestUrl = request.url();
|
|
32
|
+
|
|
33
|
+
// FIND TARGET URL REQUEST
|
|
34
|
+
if (requestUrl === targetUrl) {
|
|
35
|
+
const cookiesList = await page.cookies(requestUrl);
|
|
36
|
+
const cookies = cookiesList.map(cookie => `${cookie.name}=${cookie.value}`).join('; ');
|
|
37
|
+
const headers = Object.assign(request.headers(), { cookie: cookies });
|
|
38
|
+
const options = {
|
|
39
|
+
headers,
|
|
40
|
+
body: request.postData(),
|
|
41
|
+
method: request.method(),
|
|
42
|
+
follow: 20,
|
|
43
|
+
agent
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const result = await fetch(requestUrl, options);
|
|
47
|
+
|
|
48
|
+
const buffer = await result.buffer();
|
|
49
|
+
const cleanedHeaders = result.headers._headers || {};
|
|
50
|
+
cleanedHeaders['content-security-policy'] = '';
|
|
51
|
+
await request.respond({
|
|
52
|
+
body: buffer,
|
|
53
|
+
headers: cleanedHeaders,
|
|
54
|
+
status: result.status
|
|
55
|
+
});
|
|
56
|
+
} else {
|
|
57
|
+
request.continue();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
await page.setRequestInterception(true);
|
|
62
|
+
page.on('request', req => {
|
|
63
|
+
intercept(req, scenario.url);
|
|
64
|
+
});
|
|
65
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
const intercept = async (request, targetUrl) => {
|
|
25
|
+
if (IMAGE_URL_RE.test(request.url())) {
|
|
26
|
+
await request.respond({
|
|
27
|
+
body: IMAGE_DATA_BUFFER,
|
|
28
|
+
headers: HEADERS_STUB,
|
|
29
|
+
status: 200
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
request.continue();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
await page.setRequestInterception(true);
|
|
36
|
+
page.on('request', intercept);
|
|
37
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const YAML = require('js-yaml');
|
|
3
|
+
|
|
4
|
+
module.exports = async (page, scenario) => {
|
|
5
|
+
let cookies = [];
|
|
6
|
+
const cookiePath = scenario.cookiePath;
|
|
7
|
+
|
|
8
|
+
// READ COOKIES FROM FILE IF EXISTS
|
|
9
|
+
if (!!cookiePath && fs.existsSync(cookiePath)) {
|
|
10
|
+
let content = fs.readFileSync(cookiePath);
|
|
11
|
+
if (cookiePath.endsWith('.json')) {
|
|
12
|
+
cookies = JSON.parse(content);
|
|
13
|
+
} else if (cookiePath.endsWith('.yaml') || cookiePath.endsWith('.yml')) {
|
|
14
|
+
cookies = YAML.load(content);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// MUNGE COOKIE DOMAIN
|
|
19
|
+
cookies = cookies.map((cookie) => {
|
|
20
|
+
if (cookie.domain.startsWith('http://') || cookie.domain.startsWith('https://')) {
|
|
21
|
+
cookie.url = cookie.domain;
|
|
22
|
+
} else {
|
|
23
|
+
cookie.url = 'https://' + cookie.domain;
|
|
24
|
+
}
|
|
25
|
+
delete cookie.domain;
|
|
26
|
+
return cookie;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// SET COOKIES
|
|
30
|
+
const setCookies = async () => {
|
|
31
|
+
return Promise.all(
|
|
32
|
+
cookies.map(async (cookie) => {
|
|
33
|
+
await page.setCookie(cookie);
|
|
34
|
+
})
|
|
35
|
+
);
|
|
36
|
+
};
|
|
37
|
+
await setCookies();
|
|
38
|
+
|
|
39
|
+
// console.log('Cookie state restored with:', JSON.stringify(cookies, null, 2));
|
|
40
|
+
console.log('Cookie state restored for: ' + scenario.label);
|
|
41
|
+
};
|
|
@@ -0,0 +1,142 @@
|
|
|
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 page.click(sigInForPage.loginButtonSelector);
|
|
86
|
+
await page.waitForNavigation();
|
|
87
|
+
|
|
88
|
+
console.log('sign in successfully', page.url());
|
|
89
|
+
// Save cookies after login
|
|
90
|
+
cookies = await browserContext.cookies();
|
|
91
|
+
const loggingCookies = cookies.map((a) => a.name).sort();
|
|
92
|
+
console.log('cookies', loggingCookies);
|
|
93
|
+
return cookies;
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('sign in error', error, domain);
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const waitForOtherSiginingIn = async (signInData) => {
|
|
100
|
+
const isSignedIn = await waitForCondition(() => !signInData.isSiginingIn, 1000, 10000);
|
|
101
|
+
return isSignedIn;
|
|
102
|
+
};
|
|
103
|
+
const getOrSetSignInData = async (browserContext, loginScenario) => {
|
|
104
|
+
const pageUrl = new URL(loginScenario.url);
|
|
105
|
+
const signInDomain = pageUrl.host;
|
|
106
|
+
const signInData = (globalData[signInDomain] = globalData[signInDomain] || {
|
|
107
|
+
isSiginingIn: false,
|
|
108
|
+
cookies: undefined,
|
|
109
|
+
signInDomain: new URL(`${pageUrl.protocol}//${pageUrl.host}`),
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
if (!signInData.isSiginingIn) {
|
|
113
|
+
console.log('first signin was called');
|
|
114
|
+
// lock current signin for domain
|
|
115
|
+
signInData.isSiginingIn = true;
|
|
116
|
+
signInData.cookies = await executeSignIn(browserContext, signInData.signInDomain);
|
|
117
|
+
// release lock for domain
|
|
118
|
+
signInData.isSiginingIn = false;
|
|
119
|
+
} else {
|
|
120
|
+
const isSignedInSuccessfully = await waitForOtherSiginingIn(signInData);
|
|
121
|
+
if (isSignedInSuccessfully) {
|
|
122
|
+
console.log('cookies is populated by others, reuse it');
|
|
123
|
+
} else {
|
|
124
|
+
console.log('cookies is not populated by others, start logging in user');
|
|
125
|
+
signInData.cookies = await executeSignIn(browserContext, signInData.signInDomain);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return signInData;
|
|
130
|
+
};
|
|
131
|
+
module.exports = async function (browserContext, loginScenario) {
|
|
132
|
+
if (!loginScenario.requiredLogin) {
|
|
133
|
+
console.log('testing scenario run as anonymous user ', loginScenario.label);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
console.log('testing scenario required login cookies ', loginScenario.label);
|
|
137
|
+
let signInData = await getOrSetSignInData(browserContext, loginScenario);
|
|
138
|
+
if (signInData?.cookies) {
|
|
139
|
+
browserContext.addCookies(signInData.cookies);
|
|
140
|
+
console.log('------login cookies restored for ', loginScenario.label);
|
|
141
|
+
}
|
|
142
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const autoScroll = require('../auto-scroll');
|
|
3
|
+
|
|
4
|
+
module.exports = async (page, scenario, vp) => {
|
|
5
|
+
console.log('SCENARIO > ' + scenario.label);
|
|
6
|
+
if (scenario.useCssOverride) {
|
|
7
|
+
await require('./overrideCSS')(page, scenario);
|
|
8
|
+
}
|
|
9
|
+
await require('./clickAndHoverHelper')(page, scenario);
|
|
10
|
+
|
|
11
|
+
const jsOnReadyPath = scenario.jsOnReadyPath;
|
|
12
|
+
|
|
13
|
+
if (!jsOnReadyPath) {
|
|
14
|
+
return;
|
|
15
|
+
} else if (!fs.existsSync(jsOnReadyPath)) {
|
|
16
|
+
console.log('File not exist: ' + jsOnReadyPath);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const jsOnReadyScript = fs.readFileSync(jsOnReadyPath, 'utf-8');
|
|
21
|
+
await page
|
|
22
|
+
.evaluate(jsOnReadyScript)
|
|
23
|
+
.then(() => "ONREADY script executed for: " + scenario.label);
|
|
24
|
+
|
|
25
|
+
// add more ready handlers here...
|
|
26
|
+
await page.evaluate(autoScroll);
|
|
27
|
+
|
|
28
|
+
await page.waitForNetworkIdle({
|
|
29
|
+
idleTime: 500,
|
|
30
|
+
timeout: 5000
|
|
31
|
+
});
|
|
32
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
module.exports = async (page, scenario) => {
|
|
5
|
+
const cssOverridePath = scenario.cssOverridePath;
|
|
6
|
+
|
|
7
|
+
if (!cssOverridePath) {
|
|
8
|
+
return;
|
|
9
|
+
} else if (!fs.existsSync(cssOverridePath)) {
|
|
10
|
+
console.log('File not exist: ' + cssOverridePath);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const override = fs.readFileSync(cssOverridePath, 'utf-8');
|
|
15
|
+
|
|
16
|
+
// inject arbitrary css to override styles
|
|
17
|
+
await page.evaluate(`window._styleData = '${override}'`);
|
|
18
|
+
await page.evaluate(() => {
|
|
19
|
+
const style = document.createElement('style');
|
|
20
|
+
style.type = 'text/css';
|
|
21
|
+
const styleNode = document.createTextNode(window._styleData);
|
|
22
|
+
style.appendChild(styleNode);
|
|
23
|
+
document.head.appendChild(style);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
console.log('BACKSTOP_TEST_CSS_OVERRIDE injected for: ' + scenario.label);
|
|
27
|
+
// console.log(override);
|
|
28
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "http://json-schema.org/draft-06/schema#",
|
|
3
|
+
"$ref": "#/definitions/Root",
|
|
4
|
+
"definitions": {
|
|
5
|
+
"Root": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"properties": {
|
|
8
|
+
"profiles": {
|
|
9
|
+
"additionalProperties": {
|
|
10
|
+
"type": "array",
|
|
11
|
+
"items": {
|
|
12
|
+
"$ref": "#/definitions/Replacement"
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"required": ["profiles"]
|
|
18
|
+
},
|
|
19
|
+
"Replacement": {
|
|
20
|
+
"type": "object",
|
|
21
|
+
"properties": {
|
|
22
|
+
"ref": {
|
|
23
|
+
"type": "string"
|
|
24
|
+
},
|
|
25
|
+
"test": {
|
|
26
|
+
"type": "string"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"required": ["ref", "test"]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module.exports = async () => {
|
|
2
|
+
const SCROLL_TOP_MAX = 100;
|
|
3
|
+
|
|
4
|
+
await new Promise((resolve) => {
|
|
5
|
+
const timer = setInterval(() => {
|
|
6
|
+
if (!window.visualTestScrollingBottom) {
|
|
7
|
+
clearInterval(timer);
|
|
8
|
+
resolve();
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
}, 100);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
await new Promise((resolve) => {
|
|
15
|
+
let counter = 0;
|
|
16
|
+
const timer = setInterval(() => {
|
|
17
|
+
if (window.scrollY === 0 || counter > SCROLL_TOP_MAX) {
|
|
18
|
+
clearInterval(timer);
|
|
19
|
+
resolve();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
counter++;
|
|
24
|
+
window.scrollTo(0, 0);
|
|
25
|
+
}, 100);
|
|
26
|
+
});
|
|
27
|
+
};
|