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,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files.exclude": {
|
|
3
|
+
".idea": true,
|
|
4
|
+
"**/tsconfig.tsbuildinfo": true,
|
|
5
|
+
"**/package-lock.json": true,
|
|
6
|
+
"**/*.css.map": true,
|
|
7
|
+
"**/node_modules": true,
|
|
8
|
+
"**/.turbo": true,
|
|
9
|
+
".vscode": true
|
|
10
|
+
},
|
|
11
|
+
"markdown.extension.tableFormatter.normalizeIndentation": true,
|
|
12
|
+
"markdown.extension.orderedList.marker": "one",
|
|
13
|
+
"markdownlint.ignore": [
|
|
14
|
+
"**/SUMMARY.md"
|
|
15
|
+
],
|
|
16
|
+
"compile-hero.typescriptx-output-toggle": false,
|
|
17
|
+
"compile-hero.sass-output-toggle": false,
|
|
18
|
+
"compile-hero.pug-output-toggle": false,
|
|
19
|
+
"compile-hero.less-output-toggle": false,
|
|
20
|
+
"compile-hero.javascript-output-toggle": false,
|
|
21
|
+
"compile-hero.jade-output-toggle": false,
|
|
22
|
+
"compile-hero.disable-compile-files-on-did-save-code": false,
|
|
23
|
+
"compile-hero.typescript-output-toggle": false,
|
|
24
|
+
"compile-hero.scss-output-directory": ".",
|
|
25
|
+
"path-intellisense.showHiddenFiles": true,
|
|
26
|
+
"editor.formatOnSave": true,
|
|
27
|
+
"editor.formatOnPaste": true,
|
|
28
|
+
"markdown.extension.list.indentationSize": "inherit",
|
|
29
|
+
"editor.tabSize": 2,
|
|
30
|
+
"editor.rulers": [
|
|
31
|
+
80,
|
|
32
|
+
120
|
|
33
|
+
],
|
|
34
|
+
"editor.minimap.enabled": false,
|
|
35
|
+
"markdown.extension.print.absoluteImgPath": false,
|
|
36
|
+
"markdown.extension.print.imgToBase64": true,
|
|
37
|
+
"markdown.extension.print.theme": "dark",
|
|
38
|
+
"liveSassCompile.settings.generateMap": false,
|
|
39
|
+
"search.exclude": {
|
|
40
|
+
"**/public/assets/css": true,
|
|
41
|
+
"**/public/assets/js": true,
|
|
42
|
+
"**/public/assets/vendor": true
|
|
43
|
+
},
|
|
44
|
+
"json.schemas": [
|
|
45
|
+
{
|
|
46
|
+
"fileMatch": [
|
|
47
|
+
"/*.tests.json"
|
|
48
|
+
],
|
|
49
|
+
"url": "./.engine_scripts/test-schema.json"
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"yaml.schemas": {
|
|
53
|
+
"./.engine_scripts/test-schema.json": "/*.tests.{yaml,yml}",
|
|
54
|
+
"./.engine_scripts/replacement-profiles-schema.json": "/_replacement-profiles.{yaml,yml}"
|
|
55
|
+
},
|
|
56
|
+
"git.branchValidationRegex": "^(main|master|develop|stage|fe-develop|fe-release|(features|bugfixes|infra|refactor)\\/.+)$"
|
|
57
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Tuyen Pham <tuyen-at-work>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Visual Regression Test
|
|
2
|
+
|
|
3
|
+
Please check [Documentation](https://tuyen.blog/optimizely-cms/testing/get-started/) for the instructions.
|
|
4
|
+
|
|
5
|
+
## Use
|
|
6
|
+
|
|
7
|
+
1. Install:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @eshn/visual-regression-tests
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
1. Manual Set up all test and config files in the **visual_tests** folder and place it at the root of the project, or automatically add it using the command:
|
|
14
|
+
```bash
|
|
15
|
+
npx eshn-visual generate
|
|
16
|
+
```
|
|
17
|
+
1. Add to scripts in package.json:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
"ref": "eshn-visual ref",
|
|
21
|
+
"approve": "eshn-visual approve",
|
|
22
|
+
"test": "eshn-visual test"
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
1. Use new command aliases:
|
|
26
|
+
|
|
27
|
+
| Command | Alias 1 | Alias 2 | Description |
|
|
28
|
+
| --------------------------------------------------- | ------- | ------------- | ------------------- |
|
|
29
|
+
| npm run ref -- --test-suite alloy | r alloy | ref alloy | |
|
|
30
|
+
| npm run approve -- --test-suite alloy | a alloy | approve alloy | |
|
|
31
|
+
| npm run ref -- --test-suite alloy | t alloy | test alloy | |
|
|
32
|
+
| npm run ref -- --test-suite sign-in --requiredLogin | t alloy | test alloy | Run with login mode |
|
package/alias.ps1
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
function Approve-TestSuite {
|
|
2
|
+
[alias("a")]
|
|
3
|
+
[alias("approve")]
|
|
4
|
+
param (
|
|
5
|
+
# Test suite name
|
|
6
|
+
[Parameter(
|
|
7
|
+
ValueFromPipeline,
|
|
8
|
+
Mandatory)]
|
|
9
|
+
[string]
|
|
10
|
+
$name
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
npm run approve -- --test-suite $name
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Reference-TestSuite {
|
|
17
|
+
[alias("r")]
|
|
18
|
+
[alias("ref")]
|
|
19
|
+
param (
|
|
20
|
+
# Test suite name
|
|
21
|
+
[Parameter(
|
|
22
|
+
ValueFromPipeline,
|
|
23
|
+
Mandatory)]
|
|
24
|
+
[string]
|
|
25
|
+
$name
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
npm run ref -- --test-suite $name
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Test-TestSuite {
|
|
32
|
+
[alias("t")]
|
|
33
|
+
[alias("test")]
|
|
34
|
+
param (
|
|
35
|
+
# Test suite name
|
|
36
|
+
[Parameter(
|
|
37
|
+
ValueFromPipeline,
|
|
38
|
+
Mandatory)]
|
|
39
|
+
[string]
|
|
40
|
+
$name
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
npm run test -- --test-suite $name
|
|
44
|
+
}
|
package/bun.lockb
ADDED
|
Binary file
|
package/cli.js
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path, { dirname } from 'path';
|
|
4
|
+
import { exec } from 'child_process';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
function getLibraryPath() {
|
|
9
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
10
|
+
const __dirname = dirname(__filename);
|
|
11
|
+
|
|
12
|
+
let currentDir = __dirname;
|
|
13
|
+
while (!fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
14
|
+
const parentDir = path.dirname(currentDir);
|
|
15
|
+
if (parentDir === currentDir) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
currentDir = parentDir;
|
|
19
|
+
}
|
|
20
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
21
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
22
|
+
return `node_modules/${packageJson.name}`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runCommand(command) {
|
|
26
|
+
const childProcess = exec(command, { env: { ...process.env, FORCE_COLOR: '1' } });
|
|
27
|
+
|
|
28
|
+
childProcess.stdout.on('data', (data) => {
|
|
29
|
+
process.stdout.write(data);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
childProcess.stderr.on('data', (data) => {
|
|
33
|
+
process.stderr.write(data);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
childProcess.on('close', (code) => {
|
|
37
|
+
if (code !== 0) {
|
|
38
|
+
console.log(chalk.red(`Command exited with code ${code}`));
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
childProcess.on('error', (err) => {
|
|
43
|
+
console.log(chalk.red(`Failed to start command: ${err.message}`));
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const args = process.argv.slice(2);
|
|
48
|
+
let commandBase = `tsx ${getLibraryPath()}/src/index.ts`;
|
|
49
|
+
|
|
50
|
+
if (args[0] === 'generate') {
|
|
51
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
52
|
+
const __dirname = dirname(__filename);
|
|
53
|
+
const postInstallPath = pathToFileURL(path.join(__dirname, 'generate_tests.js'));
|
|
54
|
+
if (fs.existsSync(postInstallPath)) {
|
|
55
|
+
console.log(chalk.yellow('generate folder visual_tests ...'));
|
|
56
|
+
await import(postInstallPath);
|
|
57
|
+
} else {
|
|
58
|
+
console.log(chalk.red('generate_tests.js not found!'));
|
|
59
|
+
}
|
|
60
|
+
} else if (args[0] === 'ref') {
|
|
61
|
+
const command = `${commandBase} --command test --ref ${args.slice(1).join(' ')}`;
|
|
62
|
+
console.log(chalk.yellow(`Running command: ${command}`));
|
|
63
|
+
runCommand(command);
|
|
64
|
+
} else if (args[0] === 'approve') {
|
|
65
|
+
const command = `${commandBase} --command approve ${args.slice(1).join(' ')}`;
|
|
66
|
+
console.log(chalk.yellow(`Running command: ${command}`));
|
|
67
|
+
runCommand(command);
|
|
68
|
+
} else if (args[0] === 'test') {
|
|
69
|
+
const command = `${commandBase} --command test ${args.slice(1).join(' ')}`;
|
|
70
|
+
console.log(chalk.yellow(`Running command: ${command}`));
|
|
71
|
+
runCommand(command);
|
|
72
|
+
} else {
|
|
73
|
+
console.log(
|
|
74
|
+
chalk.red("Invalid command. Use one of the following: 'eshn-visual generate' 'eshn-visual ref', 'eshn-visual approve', 'eshn-visual test'.")
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import pkg from 'ncp';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
const { ncp } = pkg;
|
|
9
|
+
|
|
10
|
+
async function askUser() {
|
|
11
|
+
const response = await inquirer.prompt([
|
|
12
|
+
{
|
|
13
|
+
type: 'confirm',
|
|
14
|
+
name: 'addFolder',
|
|
15
|
+
message: 'Do you want to add the "visual_tests" folder to the root of your project?',
|
|
16
|
+
default: true,
|
|
17
|
+
},
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
if (response.addFolder) {
|
|
21
|
+
const sourceFolder = path.join(path.dirname(fileURLToPath(import.meta.url)), 'visual_tests');
|
|
22
|
+
const destinationFolder = path.join(process.cwd(), 'visual_tests');
|
|
23
|
+
if (!fs.existsSync(destinationFolder)) {
|
|
24
|
+
ncp(sourceFolder, destinationFolder, function (err) {
|
|
25
|
+
if (err) {
|
|
26
|
+
console.log(chalk.red('Error copying folder:'), err);
|
|
27
|
+
} else {
|
|
28
|
+
console.log(chalk.green('Folder "visual_tests" has been copied to your project!'));
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
} else {
|
|
32
|
+
console.log(chalk.yellow('Folder "visual_tests" already exists.'));
|
|
33
|
+
}
|
|
34
|
+
} else {
|
|
35
|
+
console.log('No folder was added.');
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
askUser().catch((err) => console.error('Error:', err));
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "regressify",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Visual regression tests support",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"eshn-visual": "./cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"ref": "tsx src/index.ts --command test --ref",
|
|
12
|
+
"approve": "tsx src/index.ts --command approve",
|
|
13
|
+
"test": "tsx src/index.ts --command test"
|
|
14
|
+
},
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/precise-alloy/regression-test.git"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"cli",
|
|
21
|
+
"visual",
|
|
22
|
+
"test",
|
|
23
|
+
"regression"
|
|
24
|
+
],
|
|
25
|
+
"author": "donghoang",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"backstopjs": "^6.2.2",
|
|
29
|
+
"chalk": "^5.3.0",
|
|
30
|
+
"inquirer": "^12.2.0",
|
|
31
|
+
"js-yaml": "^4.1.0",
|
|
32
|
+
"ncp": "^2.0.0",
|
|
33
|
+
"tsx": "^4.19.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/backstopjs": "^6.1.2",
|
|
37
|
+
"@types/js-yaml": "^4.0.8",
|
|
38
|
+
"@types/node": "^20.8.10",
|
|
39
|
+
"typescript": "^5.2.2"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"tsx": "^4.19.2"
|
|
43
|
+
}
|
|
44
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { Config, Scenario, ViewportNext } from 'backstopjs';
|
|
3
|
+
import { createScenario } from './scenarios.js';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
import { getFlagArg, getStringArg, parseDataFromFile, getLibraryPath } from './helpers.js';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
import { TestSuiteModel, ScenarioModel } from './types.js';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { exit } from 'process';
|
|
10
|
+
import YAML from 'js-yaml';
|
|
11
|
+
import { getTestUrl } from './replacements.js';
|
|
12
|
+
|
|
13
|
+
const engine: 'puppeteer' | 'playwright' = 'playwright';
|
|
14
|
+
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const testSuite = getStringArg('--test-suite');
|
|
17
|
+
const isRef = getFlagArg('--ref');
|
|
18
|
+
const globalRequiredLogin = getFlagArg('--requiredLogin');
|
|
19
|
+
if (globalRequiredLogin) {
|
|
20
|
+
console.log('force run all scenarios in login mode');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const scenarios: Scenario[] = [];
|
|
24
|
+
|
|
25
|
+
const getScriptPath = (path: string, engine: 'puppeteer' | 'playwright') => {
|
|
26
|
+
return (engine == 'puppeteer' ? 'puppet' : 'playwright') + path;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
if (!testSuite) {
|
|
30
|
+
console.log(chalk.red('Argument `--test-suite` must be set.'));
|
|
31
|
+
console.log(chalk.red('Sample command: npm run <command> -- --test-suite <test-suite>'));
|
|
32
|
+
console.log(chalk.red('Command is either `ref`, `approve` or `test`.'));
|
|
33
|
+
exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const getData = (testSuite: String): TestSuiteModel | undefined => {
|
|
37
|
+
let extensions: { ext: string; parse: (content: string) => unknown }[] = [
|
|
38
|
+
{
|
|
39
|
+
ext: 'yaml',
|
|
40
|
+
parse: YAML.load,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
ext: 'yaml',
|
|
44
|
+
parse: YAML.load,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
ext: 'json',
|
|
48
|
+
parse: JSON.parse,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < extensions.length; i++) {
|
|
53
|
+
const dataPath = path.resolve(__dirname, `../visual_tests/${testSuite}.tests.${extensions[i].ext}`);
|
|
54
|
+
|
|
55
|
+
if (fs.existsSync(dataPath)) {
|
|
56
|
+
console.log('Data path: ', dataPath);
|
|
57
|
+
const content = fs.readFileSync(dataPath, 'utf-8');
|
|
58
|
+
return extensions[i].parse(content) as TestSuiteModel;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const expandScenarios = (model: ScenarioModel, scenarios: ScenarioModel[], level: number) => {
|
|
64
|
+
if (level > 100) {
|
|
65
|
+
throw 'Level is too large';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!model.needs) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const neededActions: string[] = [];
|
|
73
|
+
if (typeof model.needs === 'string') {
|
|
74
|
+
neededActions.push(model.needs);
|
|
75
|
+
} else {
|
|
76
|
+
model.needs.forEach((n) => neededActions.push(n));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
neededActions.reverse().forEach((n) => {
|
|
80
|
+
const targetScenarios = scenarios.filter((s) => !!s.id && s.id.toLowerCase() == n.toLowerCase());
|
|
81
|
+
if (targetScenarios.length !== 1) {
|
|
82
|
+
throw `The test suite must contains exactly ONE scenario with id: ${n}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
var targetScenario = targetScenarios[0];
|
|
86
|
+
expandScenarios(targetScenario, scenarios, level + 1);
|
|
87
|
+
if (!!targetScenario.actions) {
|
|
88
|
+
if (!model.actions) {
|
|
89
|
+
model.actions = [];
|
|
90
|
+
}
|
|
91
|
+
model.actions = [...targetScenario.actions, ...model.actions];
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
model.needs = undefined;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const data = getData(testSuite);
|
|
99
|
+
const viewports = parseDataFromFile(data?.viewportsPath ?? 'visual_tests/_viewports.yaml') as ViewportNext[];
|
|
100
|
+
if (data) {
|
|
101
|
+
[].forEach.call(data.scenarios, (s: ScenarioModel) => {
|
|
102
|
+
expandScenarios(s, data.scenarios, 0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const getTestUrlLocal = (url: string) => getTestUrl(url, isRef);
|
|
106
|
+
|
|
107
|
+
const pad = String(data?.scenarios.length).length;
|
|
108
|
+
data.scenarios.forEach((s, index) => {
|
|
109
|
+
const opts: ScenarioModel = {
|
|
110
|
+
...s,
|
|
111
|
+
requiredLogin: globalRequiredLogin || s.requiredLogin,
|
|
112
|
+
getTestUrl: getTestUrlLocal,
|
|
113
|
+
url: isRef ? s.url : getTestUrl(s.url, isRef),
|
|
114
|
+
index: String(index + 1).padStart(pad, ' '),
|
|
115
|
+
total: data.scenarios.length,
|
|
116
|
+
delay: s.delay ?? 1000,
|
|
117
|
+
hideSelectors: s.hideSelectors ?? data.hideSelectors,
|
|
118
|
+
removeSelectors: s.removeSelectors ?? data.removeSelectors,
|
|
119
|
+
useCssOverride: s.useCssOverride ?? data.useCssOverride,
|
|
120
|
+
jsOnReadyPath: s.jsOnReadyPath,
|
|
121
|
+
viewports: !!s.viewportNames
|
|
122
|
+
? typeof s.viewportNames === 'string'
|
|
123
|
+
? viewports.filter((v) => v.label.toLowerCase() == (s.viewportNames as string).trim().toLowerCase())
|
|
124
|
+
: viewports.filter((v) => s.viewportNames?.includes(v.label))
|
|
125
|
+
: !!data.viewportNames
|
|
126
|
+
? typeof data.viewportNames === 'string'
|
|
127
|
+
? viewports.filter((v) => v.label.toLowerCase() == (data.viewportNames as string).trim().toLowerCase())
|
|
128
|
+
: viewports.filter((v) => data.viewportNames?.includes(v.label))
|
|
129
|
+
: undefined,
|
|
130
|
+
referenceUrl: !isRef ? s.url : undefined,
|
|
131
|
+
misMatchThreshold: s.misMatchThreshold ?? data.misMatchThreshold ?? 0.1,
|
|
132
|
+
postInteractionWait: s.postInteractionWait ?? data.postInteractionWait ?? 1,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const scenario = createScenario(opts);
|
|
136
|
+
scenarios.push(scenario);
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const config: Config = {
|
|
141
|
+
id: testSuite,
|
|
142
|
+
viewports,
|
|
143
|
+
onBeforeScript: getScriptPath('/onBefore.js', engine),
|
|
144
|
+
onReadyScript: getScriptPath('/onReady.js', engine),
|
|
145
|
+
scenarios,
|
|
146
|
+
paths: {
|
|
147
|
+
bitmaps_reference: '.backstop/' + testSuite + '/bitmaps_reference',
|
|
148
|
+
bitmaps_test: '.backstop/' + testSuite + '/bitmaps_test',
|
|
149
|
+
engine_scripts: `${getLibraryPath()}/.engine_scripts`,
|
|
150
|
+
html_report: '.backstop/' + testSuite + '/html_report',
|
|
151
|
+
ci_report: '.backstop/' + testSuite + '/ci_report',
|
|
152
|
+
},
|
|
153
|
+
report: [isRef ? 'CI' : 'browser'],
|
|
154
|
+
engine,
|
|
155
|
+
engineOptions: {
|
|
156
|
+
args: [
|
|
157
|
+
'--disable-infobars',
|
|
158
|
+
'--disable-setuid-sandbox',
|
|
159
|
+
'--ignore-certifcate-errors',
|
|
160
|
+
'--ignore-certifcate-errors-spki-list',
|
|
161
|
+
'--no-sandbox',
|
|
162
|
+
'--window-position=0,0',
|
|
163
|
+
],
|
|
164
|
+
browser: data?.browser ?? 'chromium',
|
|
165
|
+
},
|
|
166
|
+
asyncCaptureLimit: data?.asyncCaptureLimit ?? 5,
|
|
167
|
+
asyncCompareLimit: data?.asyncCompareLimit ?? 50,
|
|
168
|
+
debug: false,
|
|
169
|
+
debugWindow: data?.debug,
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
export default config;
|
package/src/helpers.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import YAML from 'js-yaml';
|
|
3
|
+
import path, { dirname } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
export const getStringArg = (key: string): string | undefined => {
|
|
6
|
+
const index = process.argv.indexOf(key);
|
|
7
|
+
return index >= 0 && index < process.argv.length - 1 && !process.argv[index + 1].startsWith('-') ? process.argv[index + 1] : undefined;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const getFlagArg = (key: string): boolean => {
|
|
11
|
+
return process.argv.indexOf(key) >= 0;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const parseDataFromFile = (dataPath: string, type: 'yaml' | 'json' = 'yaml'): unknown | undefined => {
|
|
15
|
+
if (!!dataPath && fs.existsSync(dataPath)) {
|
|
16
|
+
let content = fs.readFileSync(dataPath, 'utf-8');
|
|
17
|
+
if (type === 'json') {
|
|
18
|
+
return JSON.parse(content);
|
|
19
|
+
} else if (type === 'yaml') {
|
|
20
|
+
return YAML.load(content);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function getLibraryPath() {
|
|
26
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
27
|
+
const __dirname = dirname(__filename);
|
|
28
|
+
|
|
29
|
+
let currentDir = __dirname;
|
|
30
|
+
while (!fs.existsSync(path.join(currentDir, 'package.json'))) {
|
|
31
|
+
const parentDir = path.dirname(currentDir);
|
|
32
|
+
if (parentDir === currentDir) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
currentDir = parentDir;
|
|
36
|
+
}
|
|
37
|
+
const packageJsonPath = path.join(currentDir, 'package.json');
|
|
38
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
39
|
+
return `node_modules/${packageJson.name}`;
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import backstop from 'backstopjs';
|
|
3
|
+
import config from './config.js';
|
|
4
|
+
import { getStringArg } from './helpers.js';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
const command = getStringArg('--command') as 'approve' | 'init' | 'reference' | 'test' | undefined;
|
|
12
|
+
if (!command) {
|
|
13
|
+
throw '`command` must be set';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const PATCH_START = '<!-- PATCH START -->';
|
|
17
|
+
const PATCH_END = '<!-- PATCH END -->';
|
|
18
|
+
|
|
19
|
+
const customStyle = `
|
|
20
|
+
${PATCH_START}
|
|
21
|
+
<style>
|
|
22
|
+
[id^="test"] > div[display="true"] > p[display] {
|
|
23
|
+
white-space: pre;
|
|
24
|
+
overflow-x: auto;
|
|
25
|
+
}
|
|
26
|
+
</style>
|
|
27
|
+
${PATCH_END}
|
|
28
|
+
`;
|
|
29
|
+
|
|
30
|
+
const packCompare = () => {
|
|
31
|
+
const projectRootDir = process.cwd();
|
|
32
|
+
const reportIndex = path.resolve(projectRootDir, 'node_modules/backstopjs/compare/output/index.html');
|
|
33
|
+
if (fs.existsSync(reportIndex)) {
|
|
34
|
+
let html = fs.readFileSync(reportIndex, 'utf-8');
|
|
35
|
+
const patchStartIndex = html.indexOf(PATCH_START);
|
|
36
|
+
const patchEndIndex = html.indexOf(PATCH_END);
|
|
37
|
+
if (patchStartIndex > 0 && patchEndIndex > patchStartIndex) {
|
|
38
|
+
html = html.replace(new RegExp(PATCH_START + '.*' + patchEndIndex, 'gi'), customStyle);
|
|
39
|
+
} else {
|
|
40
|
+
html = html.replace('</head>', customStyle + '</head>');
|
|
41
|
+
}
|
|
42
|
+
fs.writeFileSync(reportIndex, html);
|
|
43
|
+
} else {
|
|
44
|
+
console.log(chalk.red('File does not exist: ' + reportIndex));
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
packCompare();
|
|
49
|
+
backstop(command, { config })
|
|
50
|
+
.then(() => {
|
|
51
|
+
console.log(chalk.green(command.toUpperCase() + ' FINISHED SUCCESSFULLY'));
|
|
52
|
+
})
|
|
53
|
+
.catch(() => {
|
|
54
|
+
console.log(chalk.red(command.toUpperCase() + ' FAILED'));
|
|
55
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { ReplacementModel, ReplacementsModel } from './types';
|
|
4
|
+
import YAML from 'js-yaml';
|
|
5
|
+
import { getStringArg } from './helpers.js';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
|
|
10
|
+
const getReplacementProfile = (): ReplacementModel[] | undefined => {
|
|
11
|
+
const replacementProfileName = getStringArg('replacement-profile') ?? process.env.REPLACEMENT_PROFILE;
|
|
12
|
+
if (!!replacementProfileName) {
|
|
13
|
+
const replacementProfilePath = path.resolve(__dirname, '../visual_tests/_replacement-profiles.yaml');
|
|
14
|
+
if (!fs.existsSync(replacementProfilePath)) {
|
|
15
|
+
throw "Replacement profile doesn't exist: " + replacementProfilePath;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const profiles = YAML.load(fs.readFileSync(replacementProfilePath, 'utf-8')) as ReplacementsModel;
|
|
19
|
+
return profiles.profiles[replacementProfileName];
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const replacementProfile = getReplacementProfile();
|
|
24
|
+
|
|
25
|
+
export const getTestUrl = (url: string, isRef: boolean) => {
|
|
26
|
+
if (isRef || !replacementProfile) {
|
|
27
|
+
return url;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let testUrl = url;
|
|
31
|
+
replacementProfile.forEach((e) => (testUrl = testUrl.replace(e.ref, e.test)));
|
|
32
|
+
|
|
33
|
+
return testUrl;
|
|
34
|
+
};
|
package/src/scenarios.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ScenarioModel } from './types';
|
|
2
|
+
|
|
3
|
+
export const createScenario = (opts: ScenarioModel): ScenarioModel => {
|
|
4
|
+
const parsedUrl = new URL(opts.url);
|
|
5
|
+
|
|
6
|
+
return {
|
|
7
|
+
...opts,
|
|
8
|
+
label: opts.label ?? `${opts.index} of ${opts.total}: ${parsedUrl.pathname}`,
|
|
9
|
+
cookiePath: opts.cookiePath ?? 'visual_tests/_cookies.yaml',
|
|
10
|
+
cssOverridePath: opts.cssOverridePath ?? 'visual_tests/_override.css',
|
|
11
|
+
jsOnReadyPath: opts.jsOnReadyPath ?? 'visual_tests/_on-ready.js',
|
|
12
|
+
referenceUrl: opts.referenceUrl ?? '',
|
|
13
|
+
readyEvent: '',
|
|
14
|
+
hideSelectors: opts.hideSelectors ?? [],
|
|
15
|
+
removeSelectors: opts.removeSelectors ?? [],
|
|
16
|
+
selectors: [],
|
|
17
|
+
selectorExpansion: true,
|
|
18
|
+
expect: 0,
|
|
19
|
+
requireSameDimensions: true,
|
|
20
|
+
};
|
|
21
|
+
};
|