@vizzly-testing/cli 0.3.2 → 0.5.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/README.md +50 -28
- package/dist/cli.js +34 -30
- package/dist/client/index.js +1 -1
- package/dist/commands/run.js +38 -11
- package/dist/commands/tdd.js +30 -18
- package/dist/commands/upload.js +56 -5
- package/dist/server/handlers/api-handler.js +83 -0
- package/dist/server/handlers/tdd-handler.js +138 -0
- package/dist/server/http-server.js +132 -0
- package/dist/services/api-service.js +22 -2
- package/dist/services/server-manager.js +45 -29
- package/dist/services/test-runner.js +66 -69
- package/dist/services/uploader.js +11 -4
- package/dist/types/commands/run.d.ts +4 -1
- package/dist/types/commands/tdd.d.ts +5 -1
- package/dist/types/sdk/index.d.ts +6 -6
- package/dist/types/server/handlers/api-handler.d.ts +49 -0
- package/dist/types/server/handlers/tdd-handler.d.ts +85 -0
- package/dist/types/server/http-server.d.ts +5 -0
- package/dist/types/services/api-service.d.ts +1 -0
- package/dist/types/services/server-manager.d.ts +148 -3
- package/dist/types/services/test-runner.d.ts +1 -0
- package/dist/types/services/uploader.d.ts +2 -1
- package/dist/types/utils/ci-env.d.ts +55 -0
- package/dist/types/utils/config-helpers.d.ts +1 -1
- package/dist/types/utils/console-ui.d.ts +1 -1
- package/dist/types/utils/git.d.ts +12 -0
- package/dist/utils/ci-env.js +293 -0
- package/dist/utils/console-ui.js +4 -14
- package/dist/utils/git.js +38 -0
- package/docs/api-reference.md +17 -5
- package/docs/getting-started.md +1 -1
- package/docs/tdd-mode.md +9 -9
- package/docs/test-integration.md +9 -17
- package/docs/upload-command.md +7 -0
- package/package.json +4 -5
- package/dist/screenshot-wrapper.js +0 -68
- package/dist/server/index.js +0 -522
- package/dist/services/service-utils.js +0 -171
- package/dist/types/index.js +0 -153
- package/dist/types/screenshot-wrapper.d.ts +0 -27
- package/dist/types/server/index.d.ts +0 -38
- package/dist/types/services/service-utils.d.ts +0 -45
- package/dist/types/types/index.d.ts +0 -373
- package/dist/types/utils/diagnostics.d.ts +0 -69
- package/dist/types/utils/error-messages.d.ts +0 -42
- package/dist/types/utils/framework-detector.d.ts +0 -5
- package/dist/types/utils/help.d.ts +0 -11
- package/dist/types/utils/image-comparison.d.ts +0 -42
- package/dist/types/utils/package.d.ts +0 -1
- package/dist/types/utils/project-detection.d.ts +0 -19
- package/dist/types/utils/ui-helpers.d.ts +0 -23
- package/dist/utils/diagnostics.js +0 -184
- package/dist/utils/error-messages.js +0 -34
- package/dist/utils/framework-detector.js +0 -40
- package/dist/utils/help.js +0 -66
- package/dist/utils/image-comparison.js +0 -172
- package/dist/utils/package.js +0 -9
- package/dist/utils/project-detection.js +0 -145
- package/dist/utils/ui-helpers.js +0 -86
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagnostics utilities for the doctor command
|
|
3
|
-
*/
|
|
4
|
-
import { spawn } from 'child_process';
|
|
5
|
-
import { access, constants } from 'fs/promises';
|
|
6
|
-
import { getApiToken } from './environment-config.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Check if required dependencies are available
|
|
10
|
-
*/
|
|
11
|
-
export async function checkDependencies() {
|
|
12
|
-
const dependencies = {
|
|
13
|
-
node: await checkNodeVersion(),
|
|
14
|
-
npm: await checkCommandAvailable('npm'),
|
|
15
|
-
git: await checkCommandAvailable('git'),
|
|
16
|
-
odiff: await checkOdiffBinary()
|
|
17
|
-
};
|
|
18
|
-
const allOk = Object.values(dependencies).every(dep => dep.available);
|
|
19
|
-
return {
|
|
20
|
-
all_ok: allOk,
|
|
21
|
-
details: dependencies
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Check Node.js version
|
|
27
|
-
*/
|
|
28
|
-
async function checkNodeVersion() {
|
|
29
|
-
try {
|
|
30
|
-
const version = process.version;
|
|
31
|
-
const majorVersion = parseInt(version.slice(1).split('.')[0]);
|
|
32
|
-
const supported = majorVersion >= 20;
|
|
33
|
-
return {
|
|
34
|
-
available: true,
|
|
35
|
-
version,
|
|
36
|
-
supported,
|
|
37
|
-
message: supported ? 'OK' : 'Node.js 20+ required'
|
|
38
|
-
};
|
|
39
|
-
} catch (error) {
|
|
40
|
-
return {
|
|
41
|
-
available: false,
|
|
42
|
-
error: error.message
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Check if a command is available
|
|
49
|
-
*/
|
|
50
|
-
async function checkCommandAvailable(command) {
|
|
51
|
-
return new Promise(resolve => {
|
|
52
|
-
const child = spawn(command, ['--version'], {
|
|
53
|
-
stdio: 'pipe'
|
|
54
|
-
});
|
|
55
|
-
let output = '';
|
|
56
|
-
child.stdout.on('data', data => {
|
|
57
|
-
output += data.toString();
|
|
58
|
-
});
|
|
59
|
-
child.on('close', code => {
|
|
60
|
-
resolve({
|
|
61
|
-
available: code === 0,
|
|
62
|
-
version: output.trim().split('\n')[0] || 'Unknown',
|
|
63
|
-
message: code === 0 ? 'OK' : `Command '${command}' not found`
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
child.on('error', () => {
|
|
67
|
-
resolve({
|
|
68
|
-
available: false,
|
|
69
|
-
message: `Command '${command}' not found`
|
|
70
|
-
});
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Check if odiff binary is available
|
|
77
|
-
*/
|
|
78
|
-
async function checkOdiffBinary() {
|
|
79
|
-
try {
|
|
80
|
-
// Check if odiff-bin package is installed and binary is accessible
|
|
81
|
-
const {
|
|
82
|
-
findOdiffBin
|
|
83
|
-
} = await import('odiff-bin');
|
|
84
|
-
const odiffPath = findOdiffBin();
|
|
85
|
-
await access(odiffPath, constants.F_OK | constants.X_OK);
|
|
86
|
-
return {
|
|
87
|
-
available: true,
|
|
88
|
-
path: odiffPath,
|
|
89
|
-
message: 'OK'
|
|
90
|
-
};
|
|
91
|
-
} catch (error) {
|
|
92
|
-
return {
|
|
93
|
-
available: false,
|
|
94
|
-
error: error.message,
|
|
95
|
-
message: 'odiff binary not found or not executable'
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Check API connectivity
|
|
102
|
-
*/
|
|
103
|
-
|
|
104
|
-
export async function checkApiConnectivity(config) {
|
|
105
|
-
try {
|
|
106
|
-
const apiUrl = config?.apiUrl || 'https://vizzly.dev';
|
|
107
|
-
const apiKey = config?.apiKey || getApiToken();
|
|
108
|
-
|
|
109
|
-
// Basic URL validation
|
|
110
|
-
try {
|
|
111
|
-
new globalThis.URL(apiUrl);
|
|
112
|
-
} catch {
|
|
113
|
-
throw new Error(`Invalid API URL: ${apiUrl}`);
|
|
114
|
-
}
|
|
115
|
-
const result = {
|
|
116
|
-
url: apiUrl,
|
|
117
|
-
has_token: !!apiKey,
|
|
118
|
-
reachable: false,
|
|
119
|
-
authenticated: false
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
// Try to reach the API
|
|
123
|
-
try {
|
|
124
|
-
const response = await fetch(`${apiUrl}/health`, {
|
|
125
|
-
method: 'GET',
|
|
126
|
-
timeout: 5000
|
|
127
|
-
});
|
|
128
|
-
result.reachable = response.ok;
|
|
129
|
-
result.status_code = response.status;
|
|
130
|
-
|
|
131
|
-
// Test authentication if we have a token
|
|
132
|
-
if (apiKey && result.reachable) {
|
|
133
|
-
const authResponse = await fetch(`${apiUrl}/api/user`, {
|
|
134
|
-
method: 'GET',
|
|
135
|
-
headers: {
|
|
136
|
-
Authorization: `Bearer ${apiKey}`,
|
|
137
|
-
'Content-Type': 'application/json'
|
|
138
|
-
},
|
|
139
|
-
timeout: 5000
|
|
140
|
-
});
|
|
141
|
-
result.authenticated = authResponse.ok;
|
|
142
|
-
result.auth_status_code = authResponse.status;
|
|
143
|
-
}
|
|
144
|
-
} catch (fetchError) {
|
|
145
|
-
result.reachable = false;
|
|
146
|
-
result.error = fetchError.message;
|
|
147
|
-
}
|
|
148
|
-
return result;
|
|
149
|
-
} catch (error) {
|
|
150
|
-
return {
|
|
151
|
-
error: error.message,
|
|
152
|
-
reachable: false,
|
|
153
|
-
authenticated: false
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/**
|
|
159
|
-
* Check terminal capabilities
|
|
160
|
-
*/
|
|
161
|
-
export function checkTerminalCapabilities() {
|
|
162
|
-
return {
|
|
163
|
-
stdout_is_tty: Boolean(process.stdout.isTTY),
|
|
164
|
-
stdin_is_tty: Boolean(process.stdin.isTTY),
|
|
165
|
-
supports_color: Boolean(process.stdout.isTTY && process.env.TERM !== 'dumb'),
|
|
166
|
-
terminal_type: process.env.TERM || 'unknown',
|
|
167
|
-
columns: process.stdout.columns || 0,
|
|
168
|
-
rows: process.stdout.rows || 0
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
/**
|
|
173
|
-
* Get system information
|
|
174
|
-
*/
|
|
175
|
-
export function getSystemInfo() {
|
|
176
|
-
return {
|
|
177
|
-
platform: process.platform,
|
|
178
|
-
arch: process.arch,
|
|
179
|
-
node_version: process.version,
|
|
180
|
-
memory_usage: process.memoryUsage(),
|
|
181
|
-
uptime: process.uptime(),
|
|
182
|
-
working_directory: process.cwd()
|
|
183
|
-
};
|
|
184
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export const ERROR_MESSAGES = {
|
|
2
|
-
NO_API_TOKEN: {
|
|
3
|
-
message: 'No API token provided',
|
|
4
|
-
hint: 'Set the VIZZLY_TOKEN environment variable or pass --token flag',
|
|
5
|
-
docs: 'https://github.com/vizzly-testing/cli#set-up-your-api-token'
|
|
6
|
-
},
|
|
7
|
-
BUILD_CREATION_FAILED: {
|
|
8
|
-
message: 'Failed to create build',
|
|
9
|
-
hint: 'Check your API token permissions and network connection',
|
|
10
|
-
docs: 'https://github.com/vizzly-testing/cli/blob/main/docs/upload-command.md#troubleshooting'
|
|
11
|
-
},
|
|
12
|
-
NO_SCREENSHOTS_FOUND: {
|
|
13
|
-
message: 'No screenshots found',
|
|
14
|
-
hint: 'Make sure your test code calls vizzlyScreenshot() or check the screenshots directory',
|
|
15
|
-
docs: 'https://github.com/vizzly-testing/cli/blob/main/docs/getting-started.md'
|
|
16
|
-
},
|
|
17
|
-
PORT_IN_USE: {
|
|
18
|
-
message: 'Port is already in use',
|
|
19
|
-
hint: 'Try a different port with --port flag or stop the process using this port',
|
|
20
|
-
docs: 'https://github.com/vizzly-testing/cli/blob/main/docs/test-integration.md#troubleshooting'
|
|
21
|
-
}
|
|
22
|
-
};
|
|
23
|
-
export function formatError(errorCode, context = {}) {
|
|
24
|
-
const errorInfo = ERROR_MESSAGES[errorCode];
|
|
25
|
-
if (!errorInfo) return {
|
|
26
|
-
message: errorCode
|
|
27
|
-
};
|
|
28
|
-
return {
|
|
29
|
-
message: errorInfo.message,
|
|
30
|
-
hint: errorInfo.hint,
|
|
31
|
-
docs: errorInfo.docs,
|
|
32
|
-
context
|
|
33
|
-
};
|
|
34
|
-
}
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import fs from 'fs/promises';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Detect testing framework from project dependencies
|
|
6
|
-
* @returns {Promise<string|null>} Detected framework or null
|
|
7
|
-
*/
|
|
8
|
-
export async function detectFramework() {
|
|
9
|
-
try {
|
|
10
|
-
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
11
|
-
const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
|
|
12
|
-
const dependencies = {
|
|
13
|
-
...(packageJson.dependencies || {}),
|
|
14
|
-
...(packageJson.devDependencies || {})
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
// Check for common testing frameworks
|
|
18
|
-
if (dependencies['@playwright/test'] || dependencies.playwright) {
|
|
19
|
-
return 'playwright';
|
|
20
|
-
}
|
|
21
|
-
if (dependencies.cypress) {
|
|
22
|
-
return 'cypress';
|
|
23
|
-
}
|
|
24
|
-
if (dependencies.puppeteer) {
|
|
25
|
-
return 'puppeteer';
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
// Check for config files
|
|
29
|
-
const files = await fs.readdir(process.cwd());
|
|
30
|
-
if (files.some(f => f.includes('playwright.config'))) {
|
|
31
|
-
return 'playwright';
|
|
32
|
-
}
|
|
33
|
-
if (files.some(f => f.includes('cypress.config'))) {
|
|
34
|
-
return 'cypress';
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
}
|
package/dist/utils/help.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { createColors } from './colors.js';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Display help information for the CLI
|
|
5
|
-
* @param {Object} globalOptions - Global CLI options
|
|
6
|
-
*/
|
|
7
|
-
export function showHelp(globalOptions = {}) {
|
|
8
|
-
const colors = createColors({
|
|
9
|
-
useColor: !globalOptions.noColor
|
|
10
|
-
});
|
|
11
|
-
if (globalOptions.json) {
|
|
12
|
-
console.log(JSON.stringify({
|
|
13
|
-
status: 'info',
|
|
14
|
-
message: 'Vizzly CLI Help',
|
|
15
|
-
commands: [{
|
|
16
|
-
name: 'run',
|
|
17
|
-
description: 'Run tests with Vizzly visual testing'
|
|
18
|
-
}, {
|
|
19
|
-
name: 'upload',
|
|
20
|
-
description: 'Upload screenshots to Vizzly'
|
|
21
|
-
}, {
|
|
22
|
-
name: 'init',
|
|
23
|
-
description: 'Initialize Vizzly configuration'
|
|
24
|
-
}],
|
|
25
|
-
timestamp: new Date().toISOString()
|
|
26
|
-
}));
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
console.log('');
|
|
30
|
-
console.log(colors.cyan(colors.bold('🔍 Vizzly CLI')));
|
|
31
|
-
console.log(colors.dim('Visual testing tool for UI developers'));
|
|
32
|
-
console.log('');
|
|
33
|
-
console.log(colors.yellow(colors.bold('Available Commands:')));
|
|
34
|
-
console.log('');
|
|
35
|
-
console.log(` ${colors.green(colors.bold('run'))} Run tests with Vizzly visual testing`);
|
|
36
|
-
console.log(` ${colors.green(colors.bold('upload'))} Upload screenshots to Vizzly`);
|
|
37
|
-
console.log(` ${colors.green(colors.bold('init'))} Initialize Vizzly configuration`);
|
|
38
|
-
console.log('');
|
|
39
|
-
console.log(colors.dim('Use ') + colors.cyan('vizzly <command> --help') + colors.dim(' for command-specific options'));
|
|
40
|
-
console.log('');
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Show error for unknown command
|
|
45
|
-
* @param {string} command - Unknown command name
|
|
46
|
-
* @param {Object} globalOptions - Global CLI options
|
|
47
|
-
*/
|
|
48
|
-
export function showUnknownCommand(command, globalOptions = {}) {
|
|
49
|
-
const colors = createColors({
|
|
50
|
-
useColor: !globalOptions.noColor
|
|
51
|
-
});
|
|
52
|
-
if (globalOptions.json) {
|
|
53
|
-
console.error(JSON.stringify({
|
|
54
|
-
status: 'error',
|
|
55
|
-
message: `Unknown command: ${command}`,
|
|
56
|
-
availableCommands: ['run', 'upload', 'init'],
|
|
57
|
-
timestamp: new Date().toISOString()
|
|
58
|
-
}));
|
|
59
|
-
process.exit(1);
|
|
60
|
-
}
|
|
61
|
-
console.error(colors.red(`✖ Unknown command: ${command}`));
|
|
62
|
-
console.error('');
|
|
63
|
-
console.error(colors.dim('Available commands: run, upload, init'));
|
|
64
|
-
console.error('');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
@@ -1,172 +0,0 @@
|
|
|
1
|
-
import { compare } from 'odiff-bin';
|
|
2
|
-
import fs from 'fs/promises';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import os from 'os';
|
|
5
|
-
import crypto from 'crypto';
|
|
6
|
-
import { VizzlyError } from '../errors/vizzly-error.js';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Compare two images and return the difference
|
|
10
|
-
* @param {Buffer} imageBuffer1 - First image buffer
|
|
11
|
-
* @param {Buffer} imageBuffer2 - Second image buffer
|
|
12
|
-
* @param {Object} options - Comparison options
|
|
13
|
-
* @param {number} options.threshold - Matching threshold (0-1)
|
|
14
|
-
* @param {boolean} options.ignoreAntialiasing - Ignore antialiasing
|
|
15
|
-
* @param {boolean} options.ignoreColors - Ignore colors (not supported by odiff)
|
|
16
|
-
* @param {Array} options.ignoreRegions - Regions to ignore in comparison
|
|
17
|
-
* @returns {Promise<Object>} Comparison result
|
|
18
|
-
*/
|
|
19
|
-
export async function compareImages(imageBuffer1, imageBuffer2, options = {}) {
|
|
20
|
-
// Create temporary files for odiff
|
|
21
|
-
const tempDir = os.tmpdir();
|
|
22
|
-
const tempId = crypto.randomBytes(8).toString('hex');
|
|
23
|
-
const basePath = path.join(tempDir, `vizzly-base-${tempId}.png`);
|
|
24
|
-
const comparePath = path.join(tempDir, `vizzly-compare-${tempId}.png`);
|
|
25
|
-
const diffPath = path.join(tempDir, `vizzly-diff-${tempId}.png`);
|
|
26
|
-
try {
|
|
27
|
-
// Write buffers to temporary files
|
|
28
|
-
await fs.writeFile(basePath, imageBuffer1);
|
|
29
|
-
await fs.writeFile(comparePath, imageBuffer2);
|
|
30
|
-
|
|
31
|
-
// Configure odiff options
|
|
32
|
-
const odiffOptions = {
|
|
33
|
-
threshold: options.threshold || 0.1,
|
|
34
|
-
antialiasing: options.ignoreAntialiasing !== false,
|
|
35
|
-
outputDiffMask: true,
|
|
36
|
-
failOnLayoutDiff: false,
|
|
37
|
-
noFailOnFsErrors: false,
|
|
38
|
-
diffColor: '#ff0000',
|
|
39
|
-
// Red for differences
|
|
40
|
-
captureDiffLines: true,
|
|
41
|
-
reduceRamUsage: false,
|
|
42
|
-
ignoreRegions: options.ignoreRegions || []
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
// Run odiff comparison
|
|
46
|
-
const result = await compare(basePath, comparePath, diffPath, odiffOptions);
|
|
47
|
-
|
|
48
|
-
// Process results
|
|
49
|
-
if (result.match) {
|
|
50
|
-
return {
|
|
51
|
-
misMatchPercentage: 0,
|
|
52
|
-
diffPixels: 0,
|
|
53
|
-
totalPixels: 0,
|
|
54
|
-
dimensionDifference: {
|
|
55
|
-
width: 0,
|
|
56
|
-
height: 0
|
|
57
|
-
},
|
|
58
|
-
diffBuffer: null
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Handle different failure reasons
|
|
63
|
-
switch (result.reason) {
|
|
64
|
-
case 'layout-diff':
|
|
65
|
-
{
|
|
66
|
-
return {
|
|
67
|
-
misMatchPercentage: 100,
|
|
68
|
-
dimensionDifference: {
|
|
69
|
-
width: 'unknown',
|
|
70
|
-
height: 'unknown'
|
|
71
|
-
},
|
|
72
|
-
error: 'Image dimensions do not match',
|
|
73
|
-
diffBuffer: null
|
|
74
|
-
};
|
|
75
|
-
}
|
|
76
|
-
case 'pixel-diff':
|
|
77
|
-
{
|
|
78
|
-
// Read the diff image
|
|
79
|
-
const diffBuffer = await fs.readFile(diffPath);
|
|
80
|
-
return {
|
|
81
|
-
misMatchPercentage: result.diffPercentage,
|
|
82
|
-
diffPixels: result.diffCount,
|
|
83
|
-
totalPixels: Math.round(result.diffCount / (result.diffPercentage / 100)),
|
|
84
|
-
dimensionDifference: {
|
|
85
|
-
width: 0,
|
|
86
|
-
height: 0
|
|
87
|
-
},
|
|
88
|
-
diffBuffer,
|
|
89
|
-
diffLines: result.diffLines
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
case 'file-not-exists':
|
|
93
|
-
throw new VizzlyError(`Image file not found: ${result.file}`, 'IMAGE_NOT_FOUND', {
|
|
94
|
-
file: result.file
|
|
95
|
-
});
|
|
96
|
-
default:
|
|
97
|
-
throw new VizzlyError('Unknown comparison result', 'COMPARISON_UNKNOWN', {
|
|
98
|
-
result
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
|
-
} catch (error) {
|
|
102
|
-
// Re-throw VizzlyErrors
|
|
103
|
-
if (error instanceof VizzlyError) {
|
|
104
|
-
throw error;
|
|
105
|
-
}
|
|
106
|
-
throw new VizzlyError('Failed to compare images', 'IMAGE_COMPARISON_FAILED', {
|
|
107
|
-
error: error.message
|
|
108
|
-
});
|
|
109
|
-
} finally {
|
|
110
|
-
// Clean up temporary files
|
|
111
|
-
await Promise.all([fs.unlink(basePath).catch(() => {}), fs.unlink(comparePath).catch(() => {}), fs.unlink(diffPath).catch(() => {})]);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Check if buffer is a valid PNG image
|
|
117
|
-
* @param {Buffer} buffer - Image buffer
|
|
118
|
-
* @returns {boolean} True if valid PNG
|
|
119
|
-
*/
|
|
120
|
-
export function isValidPNG(buffer) {
|
|
121
|
-
// Check PNG signature
|
|
122
|
-
if (!buffer || buffer.length < 8) {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// PNG signature: 137 80 78 71 13 10 26 10
|
|
127
|
-
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
128
|
-
return buffer.subarray(0, 8).equals(pngSignature);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Get image dimensions from PNG buffer
|
|
133
|
-
* @param {Buffer} buffer - Image buffer
|
|
134
|
-
* @returns {Object|null} Dimensions or null if invalid
|
|
135
|
-
*/
|
|
136
|
-
export function getImageDimensions(buffer) {
|
|
137
|
-
if (!isValidPNG(buffer)) {
|
|
138
|
-
return null;
|
|
139
|
-
}
|
|
140
|
-
try {
|
|
141
|
-
// PNG dimensions are stored in the IHDR chunk
|
|
142
|
-
// Skip PNG signature (8 bytes) + chunk length (4 bytes) + chunk type (4 bytes)
|
|
143
|
-
const width = buffer.readUInt32BE(16);
|
|
144
|
-
const height = buffer.readUInt32BE(20);
|
|
145
|
-
return {
|
|
146
|
-
width,
|
|
147
|
-
height
|
|
148
|
-
};
|
|
149
|
-
} catch {
|
|
150
|
-
return null;
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* Compare images with ignore regions
|
|
156
|
-
* @param {Buffer} imageBuffer1 - First image buffer
|
|
157
|
-
* @param {Buffer} imageBuffer2 - Second image buffer
|
|
158
|
-
* @param {Array<{x: number, y: number, width: number, height: number}>} ignoreRegions - Regions to ignore
|
|
159
|
-
* @returns {Promise<Object>} Comparison result
|
|
160
|
-
*/
|
|
161
|
-
export async function compareImagesWithIgnoreRegions(imageBuffer1, imageBuffer2, ignoreRegions = []) {
|
|
162
|
-
// Convert ignore regions to odiff format
|
|
163
|
-
const odiffIgnoreRegions = ignoreRegions.map(region => ({
|
|
164
|
-
x1: region.x,
|
|
165
|
-
y1: region.y,
|
|
166
|
-
x2: region.x + region.width,
|
|
167
|
-
y2: region.y + region.height
|
|
168
|
-
}));
|
|
169
|
-
return compareImages(imageBuffer1, imageBuffer2, {
|
|
170
|
-
ignoreRegions: odiffIgnoreRegions
|
|
171
|
-
});
|
|
172
|
-
}
|
package/dist/utils/package.js
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'fs/promises';
|
|
2
|
-
import { resolve, dirname } from 'path';
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
4
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
export async function getPackageInfo() {
|
|
6
|
-
const packagePath = resolve(__dirname, '../../package.json');
|
|
7
|
-
const content = await readFile(packagePath, 'utf-8');
|
|
8
|
-
return JSON.parse(content);
|
|
9
|
-
}
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Project Detection Utilities
|
|
3
|
-
* Automatically detect project type and framework
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { readFile, access } from 'fs/promises';
|
|
7
|
-
import { join } from 'path';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Detect project type and framework
|
|
11
|
-
* @param {string} directory - Directory to analyze
|
|
12
|
-
* @returns {Promise<Object>} Project information
|
|
13
|
-
*/
|
|
14
|
-
export async function detectProjectType(directory = process.cwd()) {
|
|
15
|
-
const packageJsonPath = join(directory, 'package.json');
|
|
16
|
-
let packageJsonData = null;
|
|
17
|
-
try {
|
|
18
|
-
const content = await readFile(packageJsonPath, 'utf8');
|
|
19
|
-
packageJsonData = JSON.parse(content);
|
|
20
|
-
} catch {
|
|
21
|
-
// No package.json found
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// Detect framework based on dependencies and files
|
|
25
|
-
const framework = await detectFramework(directory, packageJsonData);
|
|
26
|
-
const type = detectProjectTypeFromFramework(framework);
|
|
27
|
-
return {
|
|
28
|
-
type,
|
|
29
|
-
framework,
|
|
30
|
-
hasPackageJson: !!packageJsonData,
|
|
31
|
-
projectName: packageJsonData?.name || 'unknown',
|
|
32
|
-
dependencies: packageJsonData?.dependencies || {},
|
|
33
|
-
devDependencies: packageJsonData?.devDependencies || {}
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Detect testing framework
|
|
39
|
-
* @param {string} directory - Directory to analyze
|
|
40
|
-
* @param {Object} packageJson - Package.json content
|
|
41
|
-
* @returns {Promise<string>} Framework name
|
|
42
|
-
*/
|
|
43
|
-
async function detectFramework(directory, packageJson) {
|
|
44
|
-
const dependencies = {
|
|
45
|
-
...packageJson?.dependencies,
|
|
46
|
-
...packageJson?.devDependencies
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
// Check for specific framework dependencies
|
|
50
|
-
if (dependencies.cypress) {
|
|
51
|
-
return 'cypress';
|
|
52
|
-
}
|
|
53
|
-
if (dependencies.playwright || dependencies['@playwright/test']) {
|
|
54
|
-
return 'playwright';
|
|
55
|
-
}
|
|
56
|
-
if (dependencies.webdriverio || dependencies['@wdio/cli']) {
|
|
57
|
-
return 'webdriver';
|
|
58
|
-
}
|
|
59
|
-
if (dependencies.jest || dependencies['@jest/core']) {
|
|
60
|
-
return 'jest';
|
|
61
|
-
}
|
|
62
|
-
if (dependencies.vitest) {
|
|
63
|
-
return 'vitest';
|
|
64
|
-
}
|
|
65
|
-
if (dependencies.mocha) {
|
|
66
|
-
return 'mocha';
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Check for config files
|
|
70
|
-
const configFiles = ['cypress.config.js', 'playwright.config.js', 'wdio.conf.js', 'jest.config.js', 'vitest.config.js'];
|
|
71
|
-
for (const configFile of configFiles) {
|
|
72
|
-
try {
|
|
73
|
-
await access(join(directory, configFile));
|
|
74
|
-
return configFile.split('.')[0];
|
|
75
|
-
} catch {
|
|
76
|
-
// File doesn't exist
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
return 'generic';
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Determine project type from framework
|
|
84
|
-
* @param {string} framework - Framework name
|
|
85
|
-
* @returns {string} Project type
|
|
86
|
-
*/
|
|
87
|
-
function detectProjectTypeFromFramework(framework) {
|
|
88
|
-
const e2eFrameworks = ['cypress', 'playwright', 'webdriver'];
|
|
89
|
-
if (e2eFrameworks.includes(framework)) {
|
|
90
|
-
return 'e2e';
|
|
91
|
-
}
|
|
92
|
-
return 'web';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get suggested test command for framework
|
|
97
|
-
* @param {string} framework - Framework name
|
|
98
|
-
* @param {Object} packageJson - Package.json content
|
|
99
|
-
* @returns {string} Suggested test command
|
|
100
|
-
*/
|
|
101
|
-
export function getSuggestedTestCommand(framework, packageJson) {
|
|
102
|
-
const scripts = packageJson?.scripts || {};
|
|
103
|
-
|
|
104
|
-
// Check for common script names
|
|
105
|
-
const testScripts = ['test:e2e', 'test:integration', 'e2e', 'cypress:run', 'playwright:test', 'test'];
|
|
106
|
-
for (const script of testScripts) {
|
|
107
|
-
if (scripts[script]) {
|
|
108
|
-
return `npm run ${script}`;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Framework-specific defaults
|
|
113
|
-
switch (framework) {
|
|
114
|
-
case 'cypress':
|
|
115
|
-
return 'npx cypress run';
|
|
116
|
-
case 'playwright':
|
|
117
|
-
return 'npx playwright test';
|
|
118
|
-
case 'webdriver':
|
|
119
|
-
return 'npx wdio run';
|
|
120
|
-
case 'jest':
|
|
121
|
-
return 'npx jest';
|
|
122
|
-
case 'vitest':
|
|
123
|
-
return 'npx vitest run';
|
|
124
|
-
default:
|
|
125
|
-
return 'npm test';
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Get suggested screenshots directory for framework
|
|
131
|
-
* @param {string} framework - Framework name
|
|
132
|
-
* @returns {string} Screenshots directory
|
|
133
|
-
*/
|
|
134
|
-
export function getSuggestedScreenshotsDir(framework) {
|
|
135
|
-
switch (framework) {
|
|
136
|
-
case 'cypress':
|
|
137
|
-
return './cypress/screenshots';
|
|
138
|
-
case 'playwright':
|
|
139
|
-
return './test-results';
|
|
140
|
-
case 'webdriver':
|
|
141
|
-
return './screenshots';
|
|
142
|
-
default:
|
|
143
|
-
return './screenshots';
|
|
144
|
-
}
|
|
145
|
-
}
|