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.
Files changed (58) hide show
  1. package/.browserslistrc +7 -0
  2. package/.editorconfig +14 -0
  3. package/.engine_scripts/auto-scroll.js +63 -0
  4. package/.engine_scripts/imageStub.jpg +0 -0
  5. package/.engine_scripts/package.json +11 -0
  6. package/.engine_scripts/playwright/actions.js +174 -0
  7. package/.engine_scripts/playwright/clickAndHoverHelper.js +43 -0
  8. package/.engine_scripts/playwright/embedFiles.js +28 -0
  9. package/.engine_scripts/playwright/interceptImages.js +31 -0
  10. package/.engine_scripts/playwright/loadCookies.js +26 -0
  11. package/.engine_scripts/playwright/login-user.js +143 -0
  12. package/.engine_scripts/playwright/onBefore.js +4 -0
  13. package/.engine_scripts/playwright/onReady.js +38 -0
  14. package/.engine_scripts/playwright/overrideCSS.js +39 -0
  15. package/.engine_scripts/puppet/clickAndHoverHelper.js +39 -0
  16. package/.engine_scripts/puppet/ignoreCSP.js +65 -0
  17. package/.engine_scripts/puppet/interceptImages.js +37 -0
  18. package/.engine_scripts/puppet/loadCookies.js +41 -0
  19. package/.engine_scripts/puppet/login-user.js +142 -0
  20. package/.engine_scripts/puppet/onBefore.js +4 -0
  21. package/.engine_scripts/puppet/onReady.js +32 -0
  22. package/.engine_scripts/puppet/overrideCSS.js +28 -0
  23. package/.engine_scripts/replacement-profiles-schema.json +32 -0
  24. package/.engine_scripts/scroll-top.js +27 -0
  25. package/.engine_scripts/test-schema.json +629 -0
  26. package/.eslintrc.cjs +23 -0
  27. package/.github/workflows/deploy.yml +37 -0
  28. package/.nvmrc +1 -0
  29. package/.prettierignore +2 -0
  30. package/.prettierrc +7 -0
  31. package/.prettierrc.js +8 -0
  32. package/.vscode/settings.json +57 -0
  33. package/LICENSE +21 -0
  34. package/README.md +32 -0
  35. package/alias.ps1 +44 -0
  36. package/bun.lockb +0 -0
  37. package/cli.js +76 -0
  38. package/generate_tests.js +39 -0
  39. package/package.json +44 -0
  40. package/src/config.ts +172 -0
  41. package/src/helpers.ts +40 -0
  42. package/src/index.ts +55 -0
  43. package/src/replacements.ts +34 -0
  44. package/src/scenarios.ts +21 -0
  45. package/src/types.ts +44 -0
  46. package/tsconfig.json +26 -0
  47. package/visual_tests/_cookies.yaml +21 -0
  48. package/visual_tests/_on-ready.js +3 -0
  49. package/visual_tests/_override.css +1 -0
  50. package/visual_tests/_replacement-profiles.yaml +6 -0
  51. package/visual_tests/_signing-in.yaml +7 -0
  52. package/visual_tests/_viewports.yaml +11 -0
  53. package/visual_tests/alloy.tests.yaml +6 -0
  54. package/visual_tests/color-blender.tests.yaml +62 -0
  55. package/visual_tests/form-reuse-scenarios.tests.yaml +38 -0
  56. package/visual_tests/form-submission.tests.yaml +59 -0
  57. package/visual_tests/frequently-changed-data.tests.yaml +22 -0
  58. 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
+ };
@@ -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
+ };