feat(web-test): test CLI принимает несколько путей + флаг --url, fail-fast валидация

Привод контракта `run.mjs test` к общей практике (pytest/jest/playwright):
- позиционные аргументы = пути к тестам, можно несколько
  (`test a.test.mjs b.test.mjs dir/`); discoverTests объединяет, дедуплицирует,
  сортирует (порядок по числовым префиксам сохраняется);
- URL переехал из первого позиционного во флаг --url= (по умолчанию из
  webtest.config.mjs) — раньше url-первым путал: второй путь уходил в page.goto()
  и падал с криптовым 'Cannot navigate to invalid URL';
- fail-fast: несуществующий путь → понятная ошибка вместо падения goto;
  аргумент, похожий на URL, подсказывает про --url=.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Nick Shirokov
2026-06-02 20:07:03 +03:00
co-authored by Claude Opus 4.8
parent 31fa66d8fe
commit 7f2bf9d2d3
4 changed files with 46 additions and 25 deletions
@@ -1,4 +1,4 @@
// web-test cli/commands/test v1.2 — regression test runner // web-test cli/commands/test v1.3 — regression test runner
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, writeFileSync, mkdirSync } from 'fs'; import { existsSync, writeFileSync, mkdirSync } from 'fs';
import { resolve, dirname, basename, relative } from 'path'; import { resolve, dirname, basename, relative } from 'path';
@@ -20,11 +20,12 @@ export async function cmdTest(rawArgs) {
// Parse flags // Parse flags
const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null, record: false }; const opts = { bail: false, retry: 0, timeout: 30000, report: null, format: 'json', screenshot: null, reportDir: null, record: false };
let tags = null, grep = null; let tags = null, grep = null, urlFlag = null;
const positional = []; const positional = [];
for (const a of ownArgs) { for (const a of ownArgs) {
if (a.startsWith('--tags=')) tags = a.slice(7).split(','); if (a.startsWith('--tags=')) tags = a.slice(7).split(',');
else if (a.startsWith('--grep=')) grep = new RegExp(a.slice(7), 'i'); else if (a.startsWith('--grep=')) grep = new RegExp(a.slice(7), 'i');
else if (a.startsWith('--url=')) urlFlag = a.slice(6);
else if (a === '--bail') opts.bail = true; else if (a === '--bail') opts.bail = true;
else if (a.startsWith('--retry=')) opts.retry = parseInt(a.slice(8)) || 0; else if (a.startsWith('--retry=')) opts.retry = parseInt(a.slice(8)) || 0;
else if (a.startsWith('--timeout=')) opts.timeout = parseInt(a.slice(10)) || 30000; else if (a.startsWith('--timeout=')) opts.timeout = parseInt(a.slice(10)) || 30000;
@@ -36,20 +37,28 @@ export async function cmdTest(rawArgs) {
else if (!a.startsWith('--')) positional.push(a); else if (!a.startsWith('--')) positional.push(a);
} }
// Determine URL and test path // Positional args are ALWAYS test paths (one or many). URL comes from --url= or config
let url, testPath; // (see webtest.config.mjs). This matches pytest/jest/playwright; a positional that looks
if (positional.length === 2) { // like a URL is a mistake → fail fast with a hint instead of feeding it to page.goto().
url = positional[0]; const isUrl = (s) => /^https?:\/\//i.test(s);
testPath = resolve(positional[1]); let url = urlFlag || null;
} else if (positional.length === 1) { const testPaths = [...positional];
testPath = resolve(positional[0]); if (testPaths.length === 0) {
} else { die('Usage: node run.mjs test <dir|file>... [--url=URL] [--tags=...] [--grep=...] [--bail] [--retry=N] [--timeout=ms] [--report=path]');
die('Usage: node run.mjs test [url] <dir|file> [--tags=...] [--bail] [--retry=N] [--timeout=ms] [--report=path]'); }
for (const p of testPaths) {
if (existsSync(resolve(p))) continue;
if (isUrl(p)) {
die(`"${p}" looks like a URL — use --url=<url>; positional args are test paths.`);
}
die(`Test path not found: "${p}". To run a subset use --grep= / --tags=, or pass an existing dir/file.`);
} }
// Load config if exists // Load config if exists. config (webtest.config.mjs) and hooks (_hooks.mjs) resolve from
const isFile = testPath.endsWith('.test.mjs'); // the FIRST path's directory — list paths from the same suite folder.
const testDir = isFile ? dirname(testPath) : testPath; const firstPath = resolve(testPaths[0]);
const isFile = firstPath.endsWith('.test.mjs');
const testDir = isFile ? dirname(firstPath) : firstPath;
const configPath = resolve(testDir, 'webtest.config.mjs'); const configPath = resolve(testDir, 'webtest.config.mjs');
let config = {}; let config = {};
if (existsSync(configPath)) { if (existsSync(configPath)) {
@@ -110,8 +119,8 @@ export async function cmdTest(rawArgs) {
} }
// Discover test files // Discover test files
const testFiles = discoverTests(testPath); const testFiles = discoverTests(testPaths);
if (!testFiles.length) die(`No *.test.mjs files found in ${testPath}`); if (!testFiles.length) die(`No *.test.mjs files found in ${testPaths.join(', ')}`);
// Import and filter tests // Import and filter tests
const tests = []; const tests = [];
@@ -1,10 +1,14 @@
// web-test cli/test-runner/discover v1.0 — test file discovery + state reset between tests // web-test cli/test-runner/discover v1.1 — test file discovery + state reset between tests
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
import { existsSync, readdirSync } from 'fs'; import { existsSync, readdirSync } from 'fs';
import { resolve } from 'path'; import { resolve } from 'path';
export function discoverTests(testPath) { // Accepts a single path or an array of paths (files and/or dirs). Each .test.mjs file is
if (testPath.endsWith('.test.mjs')) return existsSync(testPath) ? [testPath] : []; // taken directly; each directory is walked recursively (skipping _ / . prefixes). Results
// are deduped and sorted — sorting preserves the numeric-prefix order the suite relies on
// (00-, 01-, …) even when paths are listed out of order.
export function discoverTests(testPaths) {
const paths = Array.isArray(testPaths) ? testPaths : [testPaths];
const files = []; const files = [];
function walk(dir) { function walk(dir) {
for (const entry of readdirSync(dir, { withFileTypes: true })) { for (const entry of readdirSync(dir, { withFileTypes: true })) {
@@ -14,8 +18,15 @@ export function discoverTests(testPath) {
else if (entry.name.endsWith('.test.mjs')) files.push(full); else if (entry.name.endsWith('.test.mjs')) files.push(full);
} }
} }
walk(testPath); for (const p of paths) {
return files.sort(); const full = resolve(p);
if (full.endsWith('.test.mjs')) {
if (existsSync(full)) files.push(full);
} else if (existsSync(full)) {
walk(full);
}
}
return [...new Set(files)].sort();
} }
export async function resetState(ctx) { export async function resetState(ctx) {
+3 -2
View File
@@ -1,4 +1,4 @@
// web-test cli/util v1.1 — generic helpers for CLI commands // web-test cli/util v1.2 — generic helpers for CLI commands
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
export function out(obj) { export function out(obj) {
@@ -85,7 +85,7 @@ Commands:
shot [file] Take screenshot (default: shot.png) shot [file] Take screenshot (default: shot.png)
stop Logout and close browser stop Logout and close browser
status Check session status status Check session status
test [url] <dir|file> Run regression tests (*.test.mjs) test <dir|file>... Run regression tests (*.test.mjs); accepts multiple paths
Options for exec: Options for exec:
--no-record Skip video recording (record() becomes no-op) --no-record Skip video recording (record() becomes no-op)
@@ -95,6 +95,7 @@ Global options (any command):
Default: on (env: WEB_TEST_PRESERVE_CLIPBOARD=0 to disable globally). Default: on (env: WEB_TEST_PRESERVE_CLIPBOARD=0 to disable globally).
Options for test: Options for test:
--url=URL Override the base URL (default: from webtest.config.mjs)
--tags=smoke,crud Filter tests by tags --tags=smoke,crud Filter tests by tags
--grep=pattern Filter tests by name (regex) --grep=pattern Filter tests by name (regex)
--bail Stop on first failure --bail Stop on first failure
+2 -2
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env node #!/usr/bin/env node
// web-test run v1.17 — CLI entry-point (распилено по cli/) // web-test run v1.18 — CLI entry-point (распилено по cli/)
// Source: https://github.com/Nikolay-Shirokov/cc-1c-skills // Source: https://github.com/Nikolay-Shirokov/cc-1c-skills
/** /**
* CLI runner for 1C web client automation. * CLI runner for 1C web client automation.
@@ -14,7 +14,7 @@
* node src/run.mjs shot [file] — take screenshot * node src/run.mjs shot [file] — take screenshot
* node src/run.mjs stop — logout + close browser * node src/run.mjs stop — logout + close browser
* node src/run.mjs status — check session * node src/run.mjs status — check session
* node src/run.mjs test [url] <dir|file> — run regression tests * node src/run.mjs test <dir|file>... [--url] — run regression tests
* *
* Внутренности живут в cli/: util, session, exec-context, server, * Внутренности живут в cli/: util, session, exec-context, server,
* commands/{start,run,exec,shot,stop,status,test}, test-runner/*. * commands/{start,run,exec,shot,stop,status,test}, test-runner/*.