Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions analyzer.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,22 +114,23 @@ const analyzeFeatureFiles = (filePattern, dir = '.', excludePattern) => {
workDir = dir;

console.log('\n 🗄️ Parsing files\n');
const pattern = path.join(dir, filePattern);

const promise = new Promise((resolve) => {
const promiseArray = [];
glob(pattern, (er, files) => {

// Use cwd option instead of changing process directory
glob(filePattern, { cwd: dir }, (er, files) => {
let filteredFiles = files;

if (excludePattern) {
const excludeFullPattern = path.join(path.resolve(dir), excludePattern);
const excludedFiles = glob.sync(excludeFullPattern);
const excludedFiles = glob.sync(excludePattern, { cwd: dir });
filteredFiles = files.filter(file => !excludedFiles.includes(file));
console.log('Excluded files:', excludedFiles);
}

for (const file of filteredFiles) {
const data = parseFile(file);
const fullPath = path.join(dir, file);
const data = parseFile(fullPath);
promiseArray.push(data);
}

Expand Down
171 changes: 166 additions & 5 deletions bin/check.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const path = require('path');
const analyze = require('../analyzer');
const Reporter = require('../reporter');
const { updateFiles, cleanFiles, checkFiles } = require('../util');
const Pull = require('../pull');

function checkPattern(pattern) {
pattern = pattern.trim(); // eslint-disable-line
Expand Down Expand Up @@ -37,8 +38,9 @@ program
.option('--keep-structure', 'Prefer structure of source code over structure in Testomat.io')
.option('--no-detached', 'Don\t mark all unmatched tests as detached')
.action(async (filesArg, opts) => {
const workDir = opts.dir || process.cwd();
const isPattern = checkPattern(filesArg);
const features = await analyze(filesArg || '**/*.feature', opts.dir || process.cwd(), opts.exclude);
const features = await analyze(filesArg || '**/*.feature', workDir, opts.exclude);
if (opts.cleanIds || opts.unsafeCleanIds) {
let idMap = {};
if (apiKey) {
Expand All @@ -48,7 +50,7 @@ program
console.log(' ✖️ API key not provided');
return;
}
const files = cleanFiles(features, idMap, opts.dir || process.cwd(), opts.unsafeCleanIds);
const files = cleanFiles(features, idMap, workDir, opts.unsafeCleanIds);
console.log(` ${files.length} files updated.`);
return;
}
Expand All @@ -75,7 +77,7 @@ program
}
// Store file content using the final fileName as key for consistency, but avoid duplicate reads
if (!files[fileName]) {
files[fileName] = fs.readFileSync(path.join(opts.dir || process.cwd(), file)).toString();
files[fileName] = fs.readFileSync(path.join(workDir, file)).toString();
}
tests.push({
name, suites: [suite.feature], tags, description, code, file: fileName, steps,
Expand Down Expand Up @@ -105,7 +107,7 @@ program
console.log("Checking test IDs...");
const { checkedFiles, suitesWithoutIds, testsWithoutIds } = checkFiles(
features,
opts.dir || process.cwd()
workDir
);
console.log(` ${checkedFiles.length} Files checked`);
if (suitesWithoutIds.length || testsWithoutIds.length) {
Expand Down Expand Up @@ -135,7 +137,7 @@ program
console.log('Updating test IDs...');
if (apiKey) {
reporter.getIds({ branch }).then(idMap => {
const updatedFiles = updateFiles(features, idMap, opts.dir || process.cwd());
const updatedFiles = updateFiles(features, idMap, workDir);
console.log(`${updatedFiles.length} Files updated`);
});
} else {
Expand All @@ -148,6 +150,165 @@ program
}
});

// Pull command
program
.command('pull')
.description('Pull manual tests from Testomat.io as .feature files')
.option('-d, --dir <dir>', 'Target directory', '.')
.option('--dry-run', 'Show what files would be created without creating them')
.action(async (opts) => {
if (!apiKey) {
console.log(chalk.red(' ✖️ API key not provided. Set TESTOMATIO environment variable.'));
process.exit(1);
}

try {
const reporter = new Reporter(apiKey.trim());
const pull = new Pull(reporter, opts.dir);
await pull.execute({ dryRun: opts.dryRun });
} catch (error) {
console.error(chalk.red('Pull failed:'), error.message);
process.exit(1);
}
});

// Push command - alias for main command with default pattern
program
.command('push')
.description('Push feature files to Testomat.io (alias for main command)')
.option('-d, --dir <dir>', 'Test directory')
.option('-e, --exclude <pattern>', 'Exclude files by glob pattern')
.option('-c, --codeceptjs', 'Is codeceptJS project')
.option('--sync', 'Import tests and wait for completion')
.option('-U, --update-ids', 'Update test IDs in files after push')
.option('--clean-ids', 'Remove test IDs from feature files')
.option('--purge, --unsafe-clean-ids', 'Remove test IDs without server verification')
.option('--check-ids', 'Ensure that all suites and tests have test IDs before import')
.option('--create', 'Create tests and suites for missing IDs')
.option('--no-empty', 'Remove empty suites after import')
.option('--keep-structure', 'Prefer structure of source code over structure in Testomat.io')
.option('--no-detached', 'Don\'t mark all unmatched tests as detached')
.action(async (cmd) => {

// Use default pattern if none provided
const filesArg = '**/*.feature';

// Call the main command logic directly with cmd as options (like check-tests does)
const workDir = cmd.dir || process.cwd();
const isPattern = checkPattern(filesArg);
const features = await analyze(filesArg, workDir, cmd.exclude);

if (cmd.cleanIds || cmd.unsafeCleanIds) {
let idMap = {};
if (apiKey) {
const reporter = new Reporter(apiKey.trim(), cmd.codeceptjs);
idMap = await reporter.getIds();
} else if (cmd.cleanIds) {
console.log(' ✖️ API key not provided');
return;
}
const files = cleanFiles(features, idMap, workDir, cmd.unsafeCleanIds);
console.log(` ${files.length} files updated.`);
return;
}

let scenarioSkipped = 0;
const tests = [];
const errors = [];
const files = {};

for (const suite of features) {
if (suite.scenario) {
for (const scenario of suite.scenario) {
const {
name, description, code, file, steps, tags,
} = scenario;
if (name) {
let fileName = file;
// make file path relative to TESTOMATIO_WORKDIR if provided
if (process.env.TESTOMATIO_WORKDIR && fileName) {
const workdir = path.resolve(process.env.TESTOMATIO_WORKDIR);
const absoluteTestPath = path.resolve(fileName);
fileName = path.relative(workdir, absoluteTestPath);
}
if (process.env.TESTOMATIO_PREPEND_DIR) {
fileName = path.join(process.env.TESTOMATIO_PREPEND_DIR, file);
}
// Store file content using the final fileName as key for consistency, but avoid duplicate reads
if (!files[fileName]) {
files[fileName] = fs.readFileSync(path.join(workDir, file)).toString();
}
tests.push({
name, suites: [suite.feature], tags, description, code, file: fileName, steps,
});
} else {
scenarioSkipped += 1;
}
}
} else if (suite.error) {
errors.push(suite.error);
}
}

if (tests.length) {
const reporter = new Reporter(apiKey.trim(), cmd.codeceptjs);
reporter.addTests(tests);
reporter.addFiles(files);
console.log(chalk.greenBright.bold(`Total Scenarios found ${tests.length} \n`));
if (scenarioSkipped) console.log(chalk.red.bold(`Total Scenarios skipped ${scenarioSkipped}\n`));
if (errors.length) {
console.log(chalk.red.bold('Errors :'));
for (const error of errors) {
console.log(chalk.red(error));
}
}

if (cmd.checkIds) {
console.log("Checking test IDs...");
const { checkedFiles, suitesWithoutIds, testsWithoutIds } = checkFiles(
features,
workDir
);
console.log(` ${checkedFiles.length} Files checked`);
if (suitesWithoutIds.length || testsWithoutIds.length) {
console.log(
`\n 🔴 ${suitesWithoutIds.length} suites and ${testsWithoutIds.length} tests are missing test IDs!`
);
console.log(" Use the `--update-ids` flag to update the files.\n");
process.exit(1);
}
}

const resp = reporter.send({
branch,
sync: cmd.sync || cmd.updateIds,
noempty: !cmd.empty,
suite: process.env.TESTOMATIO_SUITE,
'no-detach': process.env.TESTOMATIO_NO_DETACHED || !cmd.detached,
structure: cmd.keepStructure,
create: cmd.create || false,
});

if (cmd.sync || cmd.updateIds) {
console.log(' Wait for Testomatio to synchronize tests...');
await resp;
}
if (cmd.updateIds) {
console.log('Updating test IDs...');
if (apiKey) {
reporter.getIds({ branch }).then(idMap => {
const updatedFiles = updateFiles(features, idMap, workDir);
console.log(`${updatedFiles.length} Files updated`);
});
} else {
console.log(' ✖️ API key not provided');
}
}
} else {
console.log('Can\'t find any tests in this folder');
console.log('Change file pattern or directory to scan to find test files:\n\nUsage: npx check-cucumber push -d [directory]');
}
});

if (process.argv.length <= 2) {
program.outputHelp();
Expand Down
118 changes: 118 additions & 0 deletions pull.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
const fs = require('fs');
const path = require('path');
const chalk = require('chalk');

/**
* Pull tests from Testomat.io and save as .feature files
*/
class Pull {
constructor(reporter, targetDir = '.') {
this.reporter = reporter;
this.targetDir = targetDir;
}

/**
* Execute pull operation
* @param {Object} options - Pull options
* @param {boolean} options.dryRun - Preview files without creating them
*/
async execute(options = {}) {
try {
console.log(chalk.cyan('🔄 Fetching manual tests from Testomat.io...'));

const data = await this.reporter.getFilesFromServer();

if (!data || (!data.files && (!data.tests || data.tests.length === 0))) {
console.log(chalk.yellow('⚠️ No files found on server'));
return;
}

if (!data.files) {
data.files = {};
}

const filesCreated = [];
const fileTree = {};

// Process files from server - server now sends .feature files directly
for (const [fileName, content] of Object.entries(data.files)) {
const targetPath = path.join(this.targetDir, fileName);
const targetDir = path.dirname(targetPath);

// Create directory structure
if (!fs.existsSync(targetDir)) {
if (!options.dryRun) {
fs.mkdirSync(targetDir, { recursive: true });
}
}

// Build file tree for display
const relativePath = path.relative(this.targetDir, targetPath);
this.addToFileTree(fileTree, relativePath);

if (options.dryRun) {
console.log(chalk.gray(` Would create: ${relativePath}`));
} else {
fs.writeFileSync(targetPath, content);
filesCreated.push(relativePath);
console.log(chalk.green(` Created: ${relativePath}`));
}
}

// Display results
if (options.dryRun) {
console.log(chalk.cyan('\n📋 Dry run completed. Files that would be created:'));
} else {
console.log(chalk.green(`\n✅ Successfully pulled ${filesCreated.length} files`));
}

// Display file tree
this.displayFileTree(fileTree);

} catch (error) {
console.error(chalk.red('❌ Failed to pull files:'), error.message);
throw error;
}
}


/**
* Add file to tree structure for display
* @param {Object} tree - File tree object
* @param {string} filePath - File path
*/
addToFileTree(tree, filePath) {
const parts = filePath.split(path.sep);
let current = tree;

parts.forEach((part, index) => {
if (!current[part]) {
current[part] = index === parts.length - 1 ? null : {};
}
if (current[part]) {
current = current[part];
}
});
}

/**
* Display file tree
* @param {Object} tree - File tree object
* @param {string} prefix - Prefix for indentation
*/
displayFileTree(tree, prefix = '') {
Object.keys(tree).forEach((key, index, array) => {
const isLast = index === array.length - 1;
const connector = isLast ? '└── ' : '├── ';

console.log(chalk.gray(prefix + connector + key));

if (tree[key] && typeof tree[key] === 'object') {
const newPrefix = prefix + (isLast ? ' ' : '│ ');
this.displayFileTree(tree[key], newPrefix);
}
});
}
}

module.exports = Pull;
Loading