From 71037d0ffc3870d5768444b96f4209e81ff3ade4 Mon Sep 17 00:00:00 2001 From: Oleksandr Pelykh Date: Tue, 28 Apr 2026 17:15:13 +0300 Subject: [PATCH 1/4] add --files option --- bin/check.js | 12 ++++-- src/updateIds/updateIds-markdown.js | 10 ++++- tests/push_command_test.js | 57 ++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 8 deletions(-) diff --git a/bin/check.js b/bin/check.js index 7cf553d1..615e55cb 100755 --- a/bin/check.js +++ b/bin/check.js @@ -238,10 +238,11 @@ program } }); -// Push command (alias for check-tests manual **/**.md) +// Push command (alias for check-tests manual **/*.test.md) program .command('push') .option('-d, --dir ', 'test directory') + .option('-f, --files ', 'file paths or glob patterns to push (defaults to **/*.test.md)') .option('--no-skipped', 'throw error if skipped tests found') .option('--typescript', 'enable typescript support') .option('--sync', 'import tests to testomatio and wait for completion') @@ -260,12 +261,15 @@ program .option('--test-alias ', 'Specify custom alias for test/it etc (separated by commas if multiple)') .option('--exclude ', 'Glob pattern to exclude files from analysis') .option('--force', 'skip git checks and force push files') - .description('Push manual tests from markdown files (alias for check-tests manual **/**.md)') + .description( + 'Push manual tests from markdown files. Use --files to pass paths or glob patterns (defaults to **/*.test.md).', + ) .action(async opts => { - // Alias: call main action with 'manual' framework and '**/**.md' files const globalOpts = program.opts(); const mergedOpts = { ...globalOpts, ...opts, updateIds: true }; - await mainAction('manual', '**/**.md', mergedOpts); + const files = + opts.files && opts.files.length ? (opts.files.length === 1 ? opts.files[0] : opts.files) : '**/*.test.md'; + await mainAction('manual', files, mergedOpts); }); if (process.argv.length <= 2) { diff --git a/src/updateIds/updateIds-markdown.js b/src/updateIds/updateIds-markdown.js index e07b4836..4182dfc3 100644 --- a/src/updateIds/updateIds-markdown.js +++ b/src/updateIds/updateIds-markdown.js @@ -4,6 +4,12 @@ const glob = require('glob'); const path = require('path'); const { TAG_REGEX } = require('./constants'); +function resolvePatterns(workDir, pattern) { + const base = path.resolve(workDir); + const patterns = Array.isArray(pattern) ? pattern : [pattern]; + return patterns.map(p => path.join(base, p)); +} + /** * Insert test ids (@T12345678) and suite ids (@S12345678) into markdown test files * @param {*} testomatioMap mapping of test ids received from testomatio server @@ -12,7 +18,7 @@ const { TAG_REGEX } = require('./constants'); * @returns */ function updateIdsMarkdown(testomatioMap, workDir, opts = {}) { - const patternWithFullPath = path.join(path.resolve(workDir), opts.pattern); + const patternWithFullPath = resolvePatterns(workDir, opts.pattern); const files = glob.sync(patternWithFullPath); debug('Files:', files); @@ -131,7 +137,7 @@ function updateId(lines, lineNumber, mappedId) { * Remove test ids from markdown test files */ function cleanIdsMarkdown(testomatioMap, workDir, opts = { dangerous: false }) { - const patternWithFullPath = path.join(path.resolve(workDir), opts.pattern); + const patternWithFullPath = resolvePatterns(workDir, opts.pattern); const files = glob.sync(patternWithFullPath); debug('Files:', files); diff --git a/tests/push_command_test.js b/tests/push_command_test.js index f71c6834..98af763f 100644 --- a/tests/push_command_test.js +++ b/tests/push_command_test.js @@ -5,7 +5,7 @@ const path = require('path'); describe('push command', () => { let testDir; - const testMarkdownFile = 'test-manual.md'; + const testMarkdownFile = 'manual.test.md'; const testMarkdownContent = ` # Manual Test Suite @@ -67,7 +67,7 @@ priority: medium }); expect(output).to.include('SHOWING MANUAL TESTS'); - expect(output).to.include('**/**.md'); + expect(output).to.include('**/*.test.md'); expect(output).to.include('Login functionality'); expect(output).to.include('Invalid login'); } catch (error) { @@ -157,6 +157,59 @@ priority: medium } }); + it('should accept a single file path via --files', () => { + const extraContent = ` +# Extra Suite + + +## Extra Test +- step +`; + fs.writeFileSync(path.join(testDir, 'extra.md'), extraContent); + + const output = execSync(`node bin/check.js push -d ${testDir} --files extra.md`, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 10000, + }); + + expect(output).to.include('SHOWING MANUAL TESTS FROM extra.md'); + expect(output).to.include('Extra Test'); + expect(output).to.not.include('Login functionality'); + }); + + it('should accept multiple file paths via --files', () => { + const extraContent = ` +# Extra Suite + + +## Extra Test +- step +`; + fs.writeFileSync(path.join(testDir, 'extra.md'), extraContent); + + const output = execSync(`node bin/check.js push -d ${testDir} -f ${testMarkdownFile} extra.md`, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 10000, + }); + + expect(output).to.include('SHOWING MANUAL TESTS'); + expect(output).to.include('Login functionality'); + expect(output).to.include('Extra Test'); + }); + + it('should accept a glob pattern via --files', () => { + const output = execSync(`node bin/check.js push -d ${testDir} --files "*.md"`, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 10000, + }); + + expect(output).to.include('SHOWING MANUAL TESTS FROM *.md'); + expect(output).to.include('Login functionality'); + }); + it('should work with empty directory (finds tests in current project)', () => { const emptyDir = path.join(testDir, 'empty'); fs.mkdirSync(emptyDir, { recursive: true }); From 102fc046ce760a249a88806247104591afe11d95 Mon Sep 17 00:00:00 2001 From: Oleksandr Pelykh Date: Wed, 29 Apr 2026 17:34:22 +0300 Subject: [PATCH 2/4] add docs --- cli.md | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/cli.md b/cli.md index e37a1c95..6dbcd2f6 100644 --- a/cli.md +++ b/cli.md @@ -13,6 +13,57 @@ npx check-tests [options] - `` - Test framework to analyze (codeceptjs, jasmine, jest, mocha, newman, playwright, qunit, testcafe, nightwatch) - `` - Glob pattern to match test files (e.g., `"tests/**/*_test.js"`) +## Push Command + +The `push` command is a shortcut for importing markdown-based manual tests into Testomat.io. It is equivalent to `check-tests manual ` with `--update-ids` enabled by default. + +```bash +npx check-tests push [options] +``` + +### Push Options + +| Option | Description | Default | +| ------------------------ | ----------------------------------------------- | -------------- | +| `-d, --dir ` | Test directory to scan | Current dir | +| `-f, --files ` | One or more file paths or glob patterns to push | `**/*.test.md` | +| `--force` | Skip git checks and force push files | false | + +The `push` command also accepts the same Testomat.io and analysis options as the main command (`--sync`, `--create`, `--no-empty`, `--keep-structure`, `--clean-ids`, `--purge`, `--no-detached`, `--no-skipped`, `--exclude`, etc.). + +### `--files` Option + +Use `--files` (or `-f`) to override the default glob (`**/*.test.md`). It accepts: + +- a **single file path** — push exactly that file +- **multiple file paths** — push every listed file +- a **glob pattern** (in quotes) — push every file matched by the pattern +- **multiple glob patterns** — push the union of files matched by each pattern + +Paths and patterns are resolved relative to `--dir` (or the current directory if `--dir` is not set). + +### Push Examples + +```bash +# Push every **/*.test.md file under the current directory (default behaviour) +TESTOMATIO=your-api-key npx check-tests push + +# Push a single markdown file +TESTOMATIO=your-api-key npx check-tests push --files docs/login.test.md + +# Push several specific files +TESTOMATIO=your-api-key npx check-tests push -f docs/login.test.md docs/checkout.test.md + +# Push everything matching a custom glob (quote the pattern!) +TESTOMATIO=your-api-key npx check-tests push --files "manual-tests/**/*.md" + +# Combine multiple globs (e.g. smoke + regression suites) +TESTOMATIO=your-api-key npx check-tests push -f "smoke/**/*.test.md" "regression/**/*.test.md" + +# Use a non-default directory together with --files +TESTOMATIO=your-api-key npx check-tests push -d ./tests --files "**/*.md" +``` + ## CLI Options ### Basic Options From e8c944128cb7c6206c23e85f8858a3c412e890fc Mon Sep 17 00:00:00 2001 From: Oleksandr Pelykh Date: Wed, 29 Apr 2026 17:42:38 +0300 Subject: [PATCH 3/4] add unit test --- tests/push_command_test.js | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/push_command_test.js b/tests/push_command_test.js index 98af763f..3b4c27ba 100644 --- a/tests/push_command_test.js +++ b/tests/push_command_test.js @@ -210,6 +210,40 @@ priority: medium expect(output).to.include('Login functionality'); }); + it('should accept a file path inside a subdirectory via --files', () => { + const subDir = path.join(testDir, 'nested'); + fs.mkdirSync(subDir, { recursive: true }); + const nestedContent = ` +# Nested Suite + + +## Nested Test +- step +`; + fs.writeFileSync(path.join(subDir, 'nested.md'), nestedContent); + + const output = execSync(`node bin/check.js push -d ${testDir} --files nested/nested.md`, { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + timeout: 10000, + }); + + expect(output).to.include('SHOWING MANUAL TESTS FROM nested/nested.md'); + expect(output).to.include('Nested Test'); + expect(output).to.not.include('Login functionality'); + }); + + it('should expose --files option in --help output', () => { + const output = execSync('node bin/check.js push --help', { + cwd: path.join(__dirname, '..'), + encoding: 'utf8', + }); + + expect(output).to.include('--files'); + expect(output).to.include('-f'); + expect(output).to.include('**/*.test.md'); + }); + it('should work with empty directory (finds tests in current project)', () => { const emptyDir = path.join(testDir, 'empty'); fs.mkdirSync(emptyDir, { recursive: true }); From d7f8d128d44e2f3b2e66a360c081a3ea6e4dc4ce Mon Sep 17 00:00:00 2001 From: Oleksandr Pelykh Date: Wed, 29 Apr 2026 17:44:03 +0300 Subject: [PATCH 4/4] fix tags and labels parsing for markdown --- src/lib/frameworks/markdown.js | 21 ++++++- tests/manual_test.js | 82 +++++++++++++++++++++++++ tests/updateIds_markdown_test.js | 102 ++++++++++++++++++++++++++++++- 3 files changed, 200 insertions(+), 5 deletions(-) diff --git a/src/lib/frameworks/markdown.js b/src/lib/frameworks/markdown.js index ff7786b8..3576abab 100644 --- a/src/lib/frameworks/markdown.js +++ b/src/lib/frameworks/markdown.js @@ -3,7 +3,8 @@ * Expected format: * - HTML comment with YAML-like metadata for suite: * - Level 1 heading (#) for suite title - * - HTML comment with YAML-like metadata for test: + * - HTML comment with YAML-like metadata for test: + * - `tags` and `labels`: comma-separated lists normalized to trimmed string arrays; all other metadata values stay plain strings * - Level 2 heading (##) for test title */ module.exports = (ast, file = '', source = '') => { @@ -82,6 +83,22 @@ module.exports = (ast, file = '', source = '') => { return tests; }; +/** Metadata keys parsed as comma-separated lists (same rules for each). */ +const COMMA_SEPARATED_LIST_KEYS = new Set(['tags', 'labels']); + +/** + * Comma-separated list; trim each segment; drop empties. + * @param {string} value + * @returns {string[]} + */ +function parseCommaSeparatedList(value) { + if (!value) return []; + return value + .split(',') + .map(t => t.trim()) + .filter(Boolean); +} + /** * Parse metadata block from HTML comment * Returns the parsed data and the end index @@ -108,7 +125,7 @@ function parseMetadataBlock(lines, startIndex) { if (match) { const key = match[1].trim(); const value = match[2].trim(); - metadata[key] = value; + metadata[key] = COMMA_SEPARATED_LIST_KEYS.has(key) ? parseCommaSeparatedList(value) : value; } i++; diff --git a/tests/manual_test.js b/tests/manual_test.js index dfdd1b13..2d51db60 100644 --- a/tests/manual_test.js +++ b/tests/manual_test.js @@ -132,4 +132,86 @@ describe('manual (markdown) parser', () => { expect(testB1.suites).to.include('Suite B @S00000001'); }); }); + + context('tags / labels comma-separated metadata (arrays)', () => { + it('should split tags by comma, trim segments', () => { + const md = ` +# Suite + + + +## Case +`; + const tests = markdownParser(null, 'tags.test.md', md); + expect(tests).to.have.length(1); + expect(tests[0].tags).to.deep.equal(['smoke', 'critical']); + }); + + it('should yield empty array when tags key has no values', () => { + const md = ` +# S + + +## T +`; + const tests = markdownParser(null, 'empty-tags.test.md', md); + expect(tests[0].tags).to.deep.equal([]); + }); + + it('should parse single tag without comma', () => { + const md = ` +# S + + +## T +`; + const tests = markdownParser(null, 'single-tag.test.md', md); + expect(tests[0].tags).to.deep.equal(['smoke']); + }); + + it('should parse labels like tags (comma split, trim)', () => { + const md = ` +# S + + + +## L +`; + const tests = markdownParser(null, 'labels.test.md', md); + expect(tests[0].labels).to.deep.equal(['beta', 'qa-team']); + }); + + it('should yield empty labels array when empty value', () => { + const md = ` +# S + + + +## L +`; + const tests = markdownParser(null, 'empty-labels.test.md', md); + expect(tests[0].labels).to.deep.equal([]); + }); + }); }); diff --git a/tests/updateIds_markdown_test.js b/tests/updateIds_markdown_test.js index 1b1db26d..8b7fe9ae 100644 --- a/tests/updateIds_markdown_test.js +++ b/tests/updateIds_markdown_test.js @@ -5,11 +5,15 @@ const { updateIdsMarkdown, cleanIdsMarkdown } = require('../src/updateIds/update describe('updateIds markdown', () => { const testFile = path.join(__dirname, 'temp-test.md'); + const testFileA = path.join(__dirname, 'temp-test-a.md'); + const testFileB = path.join(__dirname, 'temp-test-b.md'); afterEach(() => { - // Clean up test file - if (fs.existsSync(testFile)) { - fs.unlinkSync(testFile); + // Clean up test files + for (const f of [testFile, testFileA, testFileB]) { + if (fs.existsSync(f)) { + fs.unlinkSync(f); + } } }); @@ -285,6 +289,69 @@ User should be able to login.`; const updatedContent = fs.readFileSync(testFile, 'utf8'); expect(updatedContent).to.not.include('@T12345678'); }); + + it('should accept an array of patterns and update every matched file', () => { + const contentA = ` +## Test A + +Step.`; + const contentB = ` +## Test B + +Step.`; + + fs.writeFileSync(testFileA, contentA); + fs.writeFileSync(testFileB, contentB); + + const testomatioMap = { + tests: { + 'Test A': '@T11111111', + 'Test B': '@T22222222', + }, + suites: {}, + }; + + const result = updateIdsMarkdown(testomatioMap, __dirname, { + pattern: ['temp-test-a.md', 'temp-test-b.md'], + }); + + const resolved = result.map(f => path.resolve(f)); + expect(resolved).to.include(path.resolve(testFileA)); + expect(resolved).to.include(path.resolve(testFileB)); + + expect(fs.readFileSync(testFileA, 'utf8')).to.include('id: @T11111111'); + expect(fs.readFileSync(testFileB, 'utf8')).to.include('id: @T22222222'); + }); + + it('should accept an array of glob patterns', () => { + const contentA = ` +## Test A + +Step.`; + const contentB = ` +## Test B + +Step.`; + + fs.writeFileSync(testFileA, contentA); + fs.writeFileSync(testFileB, contentB); + + const testomatioMap = { + tests: { + 'Test A': '@T11111111', + 'Test B': '@T22222222', + }, + suites: {}, + }; + + const result = updateIdsMarkdown(testomatioMap, __dirname, { + pattern: ['temp-test-a.md', 'temp-test-*.md'], + }); + + const resolved = result.map(f => path.resolve(f)); + expect(resolved).to.include(path.resolve(testFileA)); + expect(resolved).to.include(path.resolve(testFileB)); + }); }); describe('cleanIdsMarkdown', () => { @@ -333,5 +400,34 @@ User should be able to login.`; expect(updatedContent).to.not.include('id: @S87654321'); expect(updatedContent).to.include('priority: high'); }); + + it('should accept an array of patterns', () => { + const contentA = ` +## Test A + +Step.`; + const contentB = ` +## Test B + +Step.`; + + fs.writeFileSync(testFileA, contentA); + fs.writeFileSync(testFileB, contentB); + + const result = cleanIdsMarkdown({}, __dirname, { + pattern: ['temp-test-a.md', 'temp-test-b.md'], + }); + + const resolved = result.map(f => path.resolve(f)); + expect(resolved).to.include(path.resolve(testFileA)); + expect(resolved).to.include(path.resolve(testFileB)); + + expect(fs.readFileSync(testFileA, 'utf8')).to.not.include('@T11111111'); + expect(fs.readFileSync(testFileB, 'utf8')).to.not.include('@T22222222'); + }); }); });