cypress-validate 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/LICENSE +21 -0
- package/README.md +302 -0
- package/bin/cypress-validate.js +153 -0
- package/lib/commands/generate.js +200 -0
- package/lib/commands/info.js +68 -0
- package/lib/commands/install.js +78 -0
- package/lib/commands/open.js +43 -0
- package/lib/commands/record.js +62 -0
- package/lib/commands/run.js +138 -0
- package/lib/commands/screenshot.js +106 -0
- package/lib/commands/show-report.js +96 -0
- package/lib/commands/verify.js +45 -0
- package/lib/index.js +21 -0
- package/lib/utils/config-finder.js +46 -0
- package/lib/utils/logger.js +31 -0
- package/package.json +65 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execa } = require('execa');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const { logger } = require('../utils/logger');
|
|
6
|
+
const { findProjectRoot } = require('../utils/config-finder');
|
|
7
|
+
|
|
8
|
+
module.exports = async function infoCommand() {
|
|
9
|
+
logger.title('Cypress Validate — Info');
|
|
10
|
+
logger.divider();
|
|
11
|
+
|
|
12
|
+
const projectRoot = findProjectRoot();
|
|
13
|
+
|
|
14
|
+
// Node & OS info
|
|
15
|
+
logger.step(`Node.js: ${chalk.cyan(process.version)}`);
|
|
16
|
+
logger.step(`Platform: ${chalk.cyan(process.platform)} (${process.arch})`);
|
|
17
|
+
logger.step(`CWD: ${chalk.cyan(projectRoot)}`);
|
|
18
|
+
logger.blank();
|
|
19
|
+
|
|
20
|
+
// Cypress version
|
|
21
|
+
logger.step('Fetching Cypress info...');
|
|
22
|
+
logger.blank();
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const result = await execa('npx', ['cypress', 'info'], {
|
|
26
|
+
cwd: projectRoot,
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
reject: false,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (result.exitCode === 0) {
|
|
32
|
+
const lines = result.stdout.split('\n');
|
|
33
|
+
lines.forEach((line) => {
|
|
34
|
+
if (line.trim()) {
|
|
35
|
+
if (line.includes('Cypress:')) {
|
|
36
|
+
console.log(' ' + chalk.bold.cyan(line));
|
|
37
|
+
} else if (line.includes('✔') || line.includes('✓')) {
|
|
38
|
+
console.log(' ' + chalk.green(line));
|
|
39
|
+
} else if (line.includes('✖') || line.includes('✗')) {
|
|
40
|
+
console.log(' ' + chalk.red(line));
|
|
41
|
+
} else {
|
|
42
|
+
console.log(' ' + chalk.gray(line));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
} else {
|
|
47
|
+
logger.warn('Could not retrieve full Cypress info. Is Cypress installed?');
|
|
48
|
+
logger.info('Install with: ' + chalk.cyan('npx cypress-validate install'));
|
|
49
|
+
}
|
|
50
|
+
} catch (err) {
|
|
51
|
+
logger.error('Failed to run cypress info: ' + err.message);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
logger.blank();
|
|
55
|
+
logger.divider();
|
|
56
|
+
|
|
57
|
+
// package.json cypress version
|
|
58
|
+
try {
|
|
59
|
+
const pkg = require(`${projectRoot}/package.json`);
|
|
60
|
+
const cyVersion =
|
|
61
|
+
(pkg.devDependencies && pkg.devDependencies.cypress) ||
|
|
62
|
+
(pkg.dependencies && pkg.dependencies.cypress) ||
|
|
63
|
+
'not found in package.json';
|
|
64
|
+
logger.info(`Cypress in package.json: ${chalk.cyan(cyVersion)}`);
|
|
65
|
+
} catch (_) { }
|
|
66
|
+
|
|
67
|
+
logger.blank();
|
|
68
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execa } = require('execa');
|
|
4
|
+
const { logger, createSpinner } = require('../utils/logger');
|
|
5
|
+
const { findProjectRoot } = require('../utils/config-finder');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
module.exports = async function installCommand(opts) {
|
|
9
|
+
logger.title('Cypress Validate — Install');
|
|
10
|
+
logger.divider();
|
|
11
|
+
|
|
12
|
+
const projectRoot = findProjectRoot();
|
|
13
|
+
|
|
14
|
+
// Build the npm install command
|
|
15
|
+
const versionSuffix = opts.version ? `@${opts.version}` : '';
|
|
16
|
+
const pkg = `cypress${versionSuffix}`;
|
|
17
|
+
const npmArgs = opts.global
|
|
18
|
+
? ['install', '-g', pkg]
|
|
19
|
+
: ['install', '--save-dev', pkg];
|
|
20
|
+
|
|
21
|
+
if (!opts.force) {
|
|
22
|
+
// Check if cypress is already installed
|
|
23
|
+
try {
|
|
24
|
+
const { stdout } = await execa('node', ['-e', "require('cypress')"], {
|
|
25
|
+
cwd: projectRoot,
|
|
26
|
+
reject: false,
|
|
27
|
+
});
|
|
28
|
+
const spinner2 = createSpinner('Checking existing Cypress installation...');
|
|
29
|
+
spinner2.start();
|
|
30
|
+
const result = await execa('npx', ['cypress', 'version'], {
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
reject: false,
|
|
33
|
+
});
|
|
34
|
+
spinner2.stop();
|
|
35
|
+
if (result.exitCode === 0) {
|
|
36
|
+
logger.success('Cypress is already installed: ' + chalk.cyan(result.stdout.trim()));
|
|
37
|
+
logger.info('Use ' + chalk.cyan('--force') + ' to reinstall, or ' + chalk.cyan('cypress-validate verify') + ' to verify the installation.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
} catch (_) {
|
|
41
|
+
// not installed, continue
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const spinner = createSpinner(`Installing ${chalk.cyan(pkg)}...`);
|
|
46
|
+
spinner.start();
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
await execa('npm', npmArgs, {
|
|
50
|
+
cwd: projectRoot,
|
|
51
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
52
|
+
});
|
|
53
|
+
spinner.succeed(chalk.green(`Successfully installed ${pkg}`));
|
|
54
|
+
} catch (err) {
|
|
55
|
+
spinner.fail(chalk.red('Installation failed'));
|
|
56
|
+
logger.error(err.message);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Run verify after install
|
|
61
|
+
logger.blank();
|
|
62
|
+
logger.step('Running ' + chalk.cyan('cypress verify') + ' ...');
|
|
63
|
+
const verifySpinner = createSpinner('Verifying Cypress...');
|
|
64
|
+
verifySpinner.start();
|
|
65
|
+
try {
|
|
66
|
+
const verifyResult = await execa('npx', ['cypress', 'verify'], {
|
|
67
|
+
cwd: projectRoot,
|
|
68
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
69
|
+
});
|
|
70
|
+
verifySpinner.succeed(chalk.green('Cypress verified successfully!'));
|
|
71
|
+
logger.blank();
|
|
72
|
+
logger.info(verifyResult.stdout);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
verifySpinner.fail('Cypress verification failed');
|
|
75
|
+
logger.error(err.stderr || err.message);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execa } = require('execa');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { logger } = require('../utils/logger');
|
|
6
|
+
const { findProjectRoot } = require('../utils/config-finder');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
|
|
9
|
+
module.exports = async function openCommand(opts) {
|
|
10
|
+
const projectRoot = findProjectRoot();
|
|
11
|
+
|
|
12
|
+
logger.title('Cypress Validate — Open (Interactive Test Runner)');
|
|
13
|
+
logger.divider();
|
|
14
|
+
logger.step(`Mode: ${chalk.cyan('Interactive / GUI')}`);
|
|
15
|
+
if (opts.browser) logger.step(`Browser: ${chalk.cyan(opts.browser)}`);
|
|
16
|
+
if (opts.spec) logger.step(`Spec: ${chalk.cyan(opts.spec)}`);
|
|
17
|
+
logger.divider();
|
|
18
|
+
logger.blank();
|
|
19
|
+
|
|
20
|
+
const args = ['open'];
|
|
21
|
+
if (opts.browser) args.push('--browser', opts.browser);
|
|
22
|
+
if (opts.spec) args.push('--spec', opts.spec);
|
|
23
|
+
if (opts.config) args.push('--config', opts.config);
|
|
24
|
+
if (opts.env) args.push('--env', opts.env);
|
|
25
|
+
if (opts.port) args.push('--port', opts.port);
|
|
26
|
+
|
|
27
|
+
const cypressBin = path.join(projectRoot, 'node_modules', '.bin', 'cypress');
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await execa(cypressBin, args, {
|
|
31
|
+
cwd: projectRoot,
|
|
32
|
+
stdio: 'inherit',
|
|
33
|
+
});
|
|
34
|
+
} catch (err) {
|
|
35
|
+
if (err.exitCode !== undefined) {
|
|
36
|
+
logger.error(`Cypress exited with code ${err.exitCode}`);
|
|
37
|
+
} else {
|
|
38
|
+
logger.error('Failed to open Cypress: ' + err.message);
|
|
39
|
+
logger.warn('Make sure Cypress is installed: ' + chalk.cyan('npx cypress-validate install'));
|
|
40
|
+
}
|
|
41
|
+
process.exit(err.exitCode || 1);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execa } = require('execa');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const { logger } = require('../utils/logger');
|
|
7
|
+
const { findProjectRoot } = require('../utils/config-finder');
|
|
8
|
+
|
|
9
|
+
module.exports = async function recordCommand(opts) {
|
|
10
|
+
logger.title('Cypress Validate — Record');
|
|
11
|
+
logger.divider();
|
|
12
|
+
|
|
13
|
+
const projectRoot = findProjectRoot();
|
|
14
|
+
|
|
15
|
+
const recordKey = opts.key || process.env.CYPRESS_RECORD_KEY;
|
|
16
|
+
if (!recordKey) {
|
|
17
|
+
logger.error('No Cypress Cloud record key found!');
|
|
18
|
+
logger.info('Provide it via:');
|
|
19
|
+
logger.step(chalk.cyan('--key <CYPRESS_RECORD_KEY>'));
|
|
20
|
+
logger.step('or set env var: ' + chalk.cyan('CYPRESS_RECORD_KEY=<key>'));
|
|
21
|
+
logger.blank();
|
|
22
|
+
logger.info('Sign up at: ' + chalk.cyan('https://cloud.cypress.io'));
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const args = ['run', '--record', '--key', recordKey];
|
|
27
|
+
|
|
28
|
+
if (opts.spec) args.push('--spec', opts.spec);
|
|
29
|
+
if (opts.browser) args.push('--browser', opts.browser);
|
|
30
|
+
if (opts.tag) args.push('--tag', opts.tag);
|
|
31
|
+
if (opts.group) args.push('--group', opts.group);
|
|
32
|
+
if (opts.parallel) args.push('--parallel');
|
|
33
|
+
if (opts.ciBuildId) args.push('--ci-build-id', opts.ciBuildId);
|
|
34
|
+
|
|
35
|
+
logger.step(`Browser: ${chalk.cyan(opts.browser || 'electron')}`);
|
|
36
|
+
if (opts.spec) logger.step(`Spec: ${chalk.cyan(opts.spec)}`);
|
|
37
|
+
if (opts.group) logger.step(`Group: ${chalk.cyan(opts.group)}`);
|
|
38
|
+
if (opts.parallel) logger.step(`Parallel: ${chalk.cyan('enabled')}`);
|
|
39
|
+
logger.step(`Key: ${chalk.cyan('*'.repeat(Math.min(8, recordKey.length)) + '...')}`);
|
|
40
|
+
logger.divider();
|
|
41
|
+
logger.blank();
|
|
42
|
+
|
|
43
|
+
const cypressBin = path.join(projectRoot, 'node_modules', '.bin', 'cypress');
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
await execa(cypressBin, args, {
|
|
47
|
+
cwd: projectRoot,
|
|
48
|
+
stdio: 'inherit',
|
|
49
|
+
env: {
|
|
50
|
+
...process.env,
|
|
51
|
+
CYPRESS_RECORD_KEY: recordKey,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
logger.blank();
|
|
55
|
+
logger.success('Run recorded successfully to Cypress Cloud!');
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logger.blank();
|
|
58
|
+
logger.error(`Recording exited with code ${err.exitCode || 1}`);
|
|
59
|
+
logger.info('View your run at: ' + chalk.cyan('https://cloud.cypress.io'));
|
|
60
|
+
process.exit(err.exitCode || 1);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execa } = require('execa');
|
|
4
|
+
const chalk = require('chalk');
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const { logger } = require('../utils/logger');
|
|
8
|
+
const { findProjectRoot, lastRunResultsPath } = require('../utils/config-finder');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the cypress run argument list from CLI options
|
|
12
|
+
*/
|
|
13
|
+
function buildArgs(opts) {
|
|
14
|
+
const args = ['run'];
|
|
15
|
+
|
|
16
|
+
if (opts.spec) args.push('--spec', opts.spec);
|
|
17
|
+
if (opts.browser) args.push('--browser', opts.browser);
|
|
18
|
+
if (opts.headed) args.push('--headed');
|
|
19
|
+
if (opts.grep) args.push('--env', `grep=${opts.grep}`);
|
|
20
|
+
if (opts.config) args.push('--config', opts.config);
|
|
21
|
+
if (opts.env) args.push('--env', opts.env);
|
|
22
|
+
if (opts.port) args.push('--port', opts.port);
|
|
23
|
+
if (opts.record) args.push('--record');
|
|
24
|
+
if (opts.key) args.push('--key', opts.key);
|
|
25
|
+
if (opts.tag) args.push('--tag', opts.tag);
|
|
26
|
+
if (opts.forbidOnly) args.push('--config', 'forbidOnly=true');
|
|
27
|
+
|
|
28
|
+
// Reporter
|
|
29
|
+
if (opts.reporter && opts.reporter !== 'spec') {
|
|
30
|
+
args.push('--reporter', opts.reporter);
|
|
31
|
+
if (opts.reporter === 'mochawesome') {
|
|
32
|
+
args.push('--reporter-options',
|
|
33
|
+
'reportDir=cypress/reports,overwrite=false,html=true,json=true');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return args;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Load specs that failed in the last run
|
|
42
|
+
*/
|
|
43
|
+
async function getLastFailedSpecs(projectRoot) {
|
|
44
|
+
const resultsFile = lastRunResultsPath(projectRoot);
|
|
45
|
+
if (!await fs.pathExists(resultsFile)) {
|
|
46
|
+
logger.warn('No previous run results found. Running all tests.');
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const data = await fs.readJson(resultsFile);
|
|
50
|
+
if (!data.failedSpecs || data.failedSpecs.length === 0) {
|
|
51
|
+
logger.success('No failed specs from last run. Nothing to re-run!');
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
logger.info(`Re-running ${data.failedSpecs.length} failed spec(s):`);
|
|
55
|
+
data.failedSpecs.forEach((s) => logger.step(s));
|
|
56
|
+
return data.failedSpecs.join(',');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Save results summary for --last-failed support
|
|
61
|
+
*/
|
|
62
|
+
async function saveRunResults(projectRoot, exitCode, specPattern) {
|
|
63
|
+
const resultsDir = path.join(projectRoot, '.cypress-validate');
|
|
64
|
+
await fs.ensureDir(resultsDir);
|
|
65
|
+
const resultsFile = lastRunResultsPath(projectRoot);
|
|
66
|
+
|
|
67
|
+
// Try to parse mochawesome/junit JSON to extract failed specs
|
|
68
|
+
const failedSpecs = exitCode !== 0 && specPattern ? [specPattern] : [];
|
|
69
|
+
|
|
70
|
+
await fs.writeJson(resultsFile, {
|
|
71
|
+
timestamp: new Date().toISOString(),
|
|
72
|
+
exitCode,
|
|
73
|
+
failedSpecs,
|
|
74
|
+
}, { spaces: 2 });
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = async function runCommand(opts) {
|
|
78
|
+
const projectRoot = findProjectRoot();
|
|
79
|
+
|
|
80
|
+
logger.title('Cypress Validate — Run');
|
|
81
|
+
logger.divider();
|
|
82
|
+
|
|
83
|
+
// Resolve --last-failed
|
|
84
|
+
if (opts.lastFailed) {
|
|
85
|
+
const failed = await getLastFailedSpecs(projectRoot);
|
|
86
|
+
if (failed) opts.spec = failed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Build args
|
|
90
|
+
const args = buildArgs(opts);
|
|
91
|
+
|
|
92
|
+
// Handle retry
|
|
93
|
+
const retryCount = parseInt(opts.retry || '0', 10);
|
|
94
|
+
if (retryCount > 0) {
|
|
95
|
+
args.push('--config', `retries=${retryCount}`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Show summary before running
|
|
99
|
+
logger.step(`Browser: ${chalk.cyan(opts.browser || 'electron')}`);
|
|
100
|
+
if (opts.spec) logger.step(`Spec: ${chalk.cyan(opts.spec)}`);
|
|
101
|
+
if (opts.grep) logger.step(`Grep: ${chalk.cyan(opts.grep)}`);
|
|
102
|
+
if (opts.headed) logger.step(`Mode: ${chalk.cyan('headed')}`);
|
|
103
|
+
else logger.step(`Mode: ${chalk.cyan('headless (default)')}`);
|
|
104
|
+
if (retryCount) logger.step(`Retries: ${chalk.cyan(retryCount)}`);
|
|
105
|
+
logger.divider();
|
|
106
|
+
logger.blank();
|
|
107
|
+
|
|
108
|
+
// Resolve cypress binary
|
|
109
|
+
const cypressBin = path.join(projectRoot, 'node_modules', '.bin', 'cypress');
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const proc = execa(cypressBin, args, {
|
|
113
|
+
cwd: projectRoot,
|
|
114
|
+
stdio: 'inherit',
|
|
115
|
+
env: {
|
|
116
|
+
...process.env,
|
|
117
|
+
CYPRESS_RECORD_KEY: opts.key || process.env.CYPRESS_RECORD_KEY,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const result = await proc;
|
|
122
|
+
await saveRunResults(projectRoot, result.exitCode, opts.spec);
|
|
123
|
+
logger.blank();
|
|
124
|
+
logger.success(chalk.green('All tests passed!'));
|
|
125
|
+
process.exit(0);
|
|
126
|
+
} catch (err) {
|
|
127
|
+
await saveRunResults(projectRoot, err.exitCode || 1, opts.spec);
|
|
128
|
+
logger.blank();
|
|
129
|
+
if (err.exitCode !== undefined) {
|
|
130
|
+
logger.error(`Tests completed with exit code ${err.exitCode}`);
|
|
131
|
+
logger.info('Run ' + chalk.cyan('cypress-validate show-report') + ' to view the HTML report.');
|
|
132
|
+
} else {
|
|
133
|
+
logger.error('Failed to run Cypress: ' + err.message);
|
|
134
|
+
logger.warn('Make sure Cypress is installed: ' + chalk.cyan('npx cypress-validate install'));
|
|
135
|
+
}
|
|
136
|
+
process.exit(err.exitCode || 1);
|
|
137
|
+
}
|
|
138
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execa } = require('execa');
|
|
4
|
+
const fs = require('fs-extra');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const chalk = require('chalk');
|
|
8
|
+
const { logger, createSpinner } = require('../utils/logger');
|
|
9
|
+
const { findProjectRoot } = require('../utils/config-finder');
|
|
10
|
+
|
|
11
|
+
module.exports = async function screenshotCommand(opts) {
|
|
12
|
+
logger.title('Cypress Validate — Screenshot');
|
|
13
|
+
logger.divider();
|
|
14
|
+
logger.step(`URL: ${chalk.cyan(opts.url)}`);
|
|
15
|
+
logger.step(`Output: ${chalk.cyan(opts.output)}`);
|
|
16
|
+
logger.step(`Browser: ${chalk.cyan(opts.browser)}`);
|
|
17
|
+
if (opts.fullPage) logger.step('Full-page: enabled');
|
|
18
|
+
logger.divider();
|
|
19
|
+
logger.blank();
|
|
20
|
+
|
|
21
|
+
const projectRoot = findProjectRoot();
|
|
22
|
+
|
|
23
|
+
// Parse viewport
|
|
24
|
+
let viewportWidth = 1280;
|
|
25
|
+
let viewportHeight = 720;
|
|
26
|
+
if (opts.viewport) {
|
|
27
|
+
const parts = opts.viewport.split('x');
|
|
28
|
+
viewportWidth = parseInt(parts[0], 10) || 1280;
|
|
29
|
+
viewportHeight = parseInt(parts[1], 10) || 720;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Build a temporary spec file
|
|
33
|
+
const tmpDir = path.join(os.tmpdir(), 'cypress-validate-screenshot');
|
|
34
|
+
await fs.ensureDir(tmpDir);
|
|
35
|
+
const specsDir = path.join(tmpDir, 'cypress', 'e2e');
|
|
36
|
+
await fs.ensureDir(specsDir);
|
|
37
|
+
|
|
38
|
+
const outputAbs = path.resolve(process.cwd(), opts.output);
|
|
39
|
+
await fs.ensureDir(path.dirname(outputAbs));
|
|
40
|
+
|
|
41
|
+
const tempSpec = path.join(specsDir, '__screenshot__.cy.js');
|
|
42
|
+
const specContent = `
|
|
43
|
+
/// <reference types="cypress" />
|
|
44
|
+
describe('cypress-validate screenshot', () => {
|
|
45
|
+
it('captures a screenshot of ${opts.url}', () => {
|
|
46
|
+
cy.viewport(${viewportWidth}, ${viewportHeight});
|
|
47
|
+
cy.visit(${JSON.stringify(opts.url)});
|
|
48
|
+
cy.screenshot('screenshot', {
|
|
49
|
+
capture: ${opts.fullPage ? "'fullPage'" : "'viewport'"},
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
`;
|
|
54
|
+
await fs.writeFile(tempSpec, specContent, 'utf-8');
|
|
55
|
+
|
|
56
|
+
// Write a temporary cypress config
|
|
57
|
+
const tmpConfigPath = path.join(tmpDir, 'cypress.config.js');
|
|
58
|
+
const screenshotDir = path.dirname(outputAbs);
|
|
59
|
+
await fs.writeFile(tmpConfigPath, `
|
|
60
|
+
const { defineConfig } = require('cypress');
|
|
61
|
+
module.exports = defineConfig({
|
|
62
|
+
e2e: {
|
|
63
|
+
supportFile: false,
|
|
64
|
+
screenshotsFolder: ${JSON.stringify(screenshotDir)},
|
|
65
|
+
video: false,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
`, 'utf-8');
|
|
69
|
+
|
|
70
|
+
const spinner = createSpinner('Taking screenshot...');
|
|
71
|
+
spinner.start();
|
|
72
|
+
|
|
73
|
+
const cypressBin = path.join(projectRoot, 'node_modules', '.bin', 'cypress');
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
await execa(cypressBin, [
|
|
77
|
+
'run',
|
|
78
|
+
'--spec', tempSpec,
|
|
79
|
+
'--config-file', tmpConfigPath,
|
|
80
|
+
'--browser', opts.browser,
|
|
81
|
+
'--headless',
|
|
82
|
+
], {
|
|
83
|
+
cwd: tmpDir,
|
|
84
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Move screenshot to intended output location
|
|
88
|
+
const generatedScreenshot = path.join(screenshotDir, 'cypress-validate screenshot', 'screenshot.png');
|
|
89
|
+
if (await fs.pathExists(generatedScreenshot)) {
|
|
90
|
+
await fs.move(generatedScreenshot, outputAbs, { overwrite: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
spinner.succeed(chalk.green('Screenshot saved!'));
|
|
94
|
+
logger.blank();
|
|
95
|
+
logger.info(`File: ${chalk.cyan(outputAbs)}`);
|
|
96
|
+
} catch (err) {
|
|
97
|
+
spinner.fail(chalk.red('Screenshot failed'));
|
|
98
|
+
logger.error(err.message);
|
|
99
|
+
logger.warn('Ensure Cypress is installed: ' + chalk.cyan('npx cypress-validate install'));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
} finally {
|
|
102
|
+
await fs.remove(tmpDir).catch(() => { });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
logger.blank();
|
|
106
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs-extra');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const open = require('open');
|
|
7
|
+
const { logger, createSpinner } = require('../utils/logger');
|
|
8
|
+
const { findProjectRoot } = require('../utils/config-finder');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Recursively find HTML report files inside a directory
|
|
12
|
+
*/
|
|
13
|
+
async function findHtmlReports(dir) {
|
|
14
|
+
const reports = [];
|
|
15
|
+
if (!await fs.pathExists(dir)) return reports;
|
|
16
|
+
const stat = await fs.stat(dir);
|
|
17
|
+
if (stat.isFile() && dir.endsWith('.html')) return [dir];
|
|
18
|
+
|
|
19
|
+
const walk = async (d) => {
|
|
20
|
+
const entries = await fs.readdir(d, { withFileTypes: true });
|
|
21
|
+
for (const entry of entries) {
|
|
22
|
+
const full = path.join(d, entry.name);
|
|
23
|
+
if (entry.isDirectory()) await walk(full);
|
|
24
|
+
else if (entry.isFile() && entry.name.endsWith('.html')) reports.push(full);
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
await walk(dir);
|
|
28
|
+
return reports;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = async function showReportCommand(opts) {
|
|
32
|
+
logger.title('Cypress Validate — Show Report');
|
|
33
|
+
logger.divider();
|
|
34
|
+
|
|
35
|
+
const projectRoot = findProjectRoot();
|
|
36
|
+
const reportRoot = path.resolve(process.cwd(), opts.path || 'cypress/reports');
|
|
37
|
+
|
|
38
|
+
// If --serve, start a local HTTP server
|
|
39
|
+
if (opts.serve) {
|
|
40
|
+
const port = opts.port || '9323';
|
|
41
|
+
logger.step(`Serving report from: ${chalk.cyan(reportRoot)}`);
|
|
42
|
+
logger.step(`Port: ${chalk.cyan(port)}`);
|
|
43
|
+
logger.blank();
|
|
44
|
+
try {
|
|
45
|
+
const { execa } = require('execa');
|
|
46
|
+
logger.info(`Open: ${chalk.cyan(`http://localhost:${port}`)}`);
|
|
47
|
+
logger.blank();
|
|
48
|
+
await execa('npx', ['serve', '-p', port, reportRoot], {
|
|
49
|
+
cwd: projectRoot,
|
|
50
|
+
stdio: 'inherit',
|
|
51
|
+
});
|
|
52
|
+
} catch (err) {
|
|
53
|
+
if (err.exitCode !== undefined && err.exitCode !== 1) {
|
|
54
|
+
logger.error(`Server exited: ${err.message}`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Otherwise, locate the HTML report and open it
|
|
62
|
+
const spinner = createSpinner('Looking for HTML report...');
|
|
63
|
+
spinner.start();
|
|
64
|
+
|
|
65
|
+
const reports = await findHtmlReports(reportRoot);
|
|
66
|
+
|
|
67
|
+
if (reports.length === 0) {
|
|
68
|
+
spinner.fail(chalk.red('No HTML report found'));
|
|
69
|
+
logger.blank();
|
|
70
|
+
logger.warn(`Searched in: ${chalk.cyan(reportRoot)}`);
|
|
71
|
+
logger.info('Generate a report by running:');
|
|
72
|
+
logger.step(chalk.cyan('npx cypress-validate run --reporter mochawesome'));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Sort by modification time — open the most recent
|
|
77
|
+
const sorted = await Promise.all(
|
|
78
|
+
reports.map(async (f) => ({ file: f, mtime: (await fs.stat(f)).mtimeMs }))
|
|
79
|
+
);
|
|
80
|
+
sorted.sort((a, b) => b.mtime - a.mtime);
|
|
81
|
+
const reportFile = sorted[0].file;
|
|
82
|
+
|
|
83
|
+
spinner.succeed(chalk.green(`Found: ${path.relative(process.cwd(), reportFile)}`));
|
|
84
|
+
logger.blank();
|
|
85
|
+
logger.step(`Opening: ${chalk.cyan(reportFile)}`);
|
|
86
|
+
logger.blank();
|
|
87
|
+
|
|
88
|
+
if (reports.length > 1) {
|
|
89
|
+
logger.info(`${chalk.gray(`(${reports.length} reports found — opening the most recent)`)}`);
|
|
90
|
+
logger.blank();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
await open(reportFile);
|
|
94
|
+
logger.success('Report opened in your default browser!');
|
|
95
|
+
logger.blank();
|
|
96
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { execa } = require('execa');
|
|
4
|
+
const { logger, createSpinner } = require('../utils/logger');
|
|
5
|
+
const { findProjectRoot } = require('../utils/config-finder');
|
|
6
|
+
const chalk = require('chalk');
|
|
7
|
+
|
|
8
|
+
module.exports = async function verifyCommand(opts) {
|
|
9
|
+
logger.title('Cypress Validate — Verify');
|
|
10
|
+
logger.divider();
|
|
11
|
+
|
|
12
|
+
const projectRoot = findProjectRoot();
|
|
13
|
+
|
|
14
|
+
const args = ['cypress', 'verify'];
|
|
15
|
+
if (opts.force) args.push('--force');
|
|
16
|
+
|
|
17
|
+
const spinner = createSpinner('Verifying Cypress installation...');
|
|
18
|
+
spinner.start();
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await execa('npx', args, {
|
|
22
|
+
cwd: projectRoot,
|
|
23
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
24
|
+
reject: false,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (result.exitCode === 0) {
|
|
28
|
+
spinner.succeed(chalk.green('Cypress is installed and verified!'));
|
|
29
|
+
logger.blank();
|
|
30
|
+
const lines = result.stdout.split('\n').filter(Boolean);
|
|
31
|
+
lines.forEach((line) => logger.info(line));
|
|
32
|
+
} else {
|
|
33
|
+
spinner.fail(chalk.red('Cypress verification failed'));
|
|
34
|
+
logger.blank();
|
|
35
|
+
logger.error(result.stderr || result.stdout || 'Unknown error');
|
|
36
|
+
logger.warn('Try running: ' + chalk.cyan('npx cypress-validate install'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
} catch (err) {
|
|
40
|
+
spinner.fail('Could not run cypress verify');
|
|
41
|
+
logger.error(err.message);
|
|
42
|
+
logger.warn('Is Cypress installed? Run: ' + chalk.cyan('npx cypress-validate install'));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
};
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cypress-validate — Programmatic API
|
|
5
|
+
* Allows using cypress-validate commands as a Node.js library.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* const { run, open, info } = require('cypress-validate');
|
|
9
|
+
* await run({ browser: 'chrome', headed: true });
|
|
10
|
+
*/
|
|
11
|
+
module.exports = {
|
|
12
|
+
run: require('./commands/run'),
|
|
13
|
+
open: require('./commands/open'),
|
|
14
|
+
install: require('./commands/install'),
|
|
15
|
+
verify: require('./commands/verify'),
|
|
16
|
+
info: require('./commands/info'),
|
|
17
|
+
generate: require('./commands/generate'),
|
|
18
|
+
screenshot: require('./commands/screenshot'),
|
|
19
|
+
showReport: require('./commands/show-report'),
|
|
20
|
+
record: require('./commands/record'),
|
|
21
|
+
};
|