diff --git a/docs/docs/dtc/configuration.md b/docs/docs/dtc/configuration.md index 36659e4..4bc84fa 100644 --- a/docs/docs/dtc/configuration.md +++ b/docs/docs/dtc/configuration.md @@ -17,7 +17,7 @@ export default { loader: async (filePath) => customLoader(filePath), runner: customTestRunner, plugins: ['../local/plugin.js', '@package/dtc-plugin'], - testRegex: /.*\.dtc\.[jt]s?$/ + testPattern: '**/*.dtc.{ts,js}', } ``` diff --git a/dtc-playwright-plugin/src/browser.spec.ts b/dtc-playwright-plugin/src/browser.spec.ts index 0626a95..b457529 100644 --- a/dtc-playwright-plugin/src/browser.spec.ts +++ b/dtc-playwright-plugin/src/browser.spec.ts @@ -5,9 +5,9 @@ const projectPath = process.cwd() const path = process.env.DTC_PLAYWRIGHT_PATH?.replace(projectPath, ''); const config = String(process.env.DTC_PLAYWRIGHT_CONFIG) -const {loader, plugins, testRegex} = await resolveConfig(config) +const {loader, plugins, testPattern} = await resolveConfig(config) -const testCaseExecutions = await loadTestCases(projectPath)({loader, testRegex})(path) +const testCaseExecutions = await loadTestCases(projectPath)({loader, testPattern})(path) for (const testCaseExecution of testCaseExecutions) { if (testCaseExecution.testCase.use) { diff --git a/dtc-playwright-plugin/test/fixtures/config.js b/dtc-playwright-plugin/test/fixtures/config.js index 48bfc41..83d6fc6 100644 --- a/dtc-playwright-plugin/test/fixtures/config.js +++ b/dtc-playwright-plugin/test/fixtures/config.js @@ -3,6 +3,6 @@ import * as playwrightPlugin from '../../src/playwright-plugin.js' export default { runner: playwrightRunner(), - testRegex: /^(?!.*node_modules).*dtc-playwright-plugin\/.*\.dtc\.[jt]s?$/, + testPattern: './**/*.dtc.{ts,js}', plugins: [playwrightPlugin] } diff --git a/dtc-playwright-plugin/test/playwright-runner.test.ts b/dtc-playwright-plugin/test/playwright-runner.test.ts index e0ee2fb..c6d31b6 100644 --- a/dtc-playwright-plugin/test/playwright-runner.test.ts +++ b/dtc-playwright-plugin/test/playwright-runner.test.ts @@ -1,25 +1,12 @@ import {test} from 'node:test' import playwrightRunner from '../src/playwright-runner.js' import assert from 'node:assert' -import {fileURLToPath} from 'url' -import {dirname} from 'path' -import testCase from './fixtures/t1.dtc.js' -import testCase2 from './fixtures/t2.dtc.js' -import testCase3 from './fixtures/t3.dtc.js' -import testCase4 from './fixtures/t4.dtc.js' - -const __dirname = dirname(fileURLToPath(import.meta.url)) test('It calls playwright runner', async () => { const runner = playwrightRunner() await runner( - [ - {filePath: `${__dirname}/fixtures/t1.dtc.js`, testCase}, - {filePath: `${__dirname}/fixtures/t2.dtc.js`, testCase: testCase2}, - {filePath: `${__dirname}/fixtures/t3.dtc.js`, testCase: testCase3}, - {filePath: `${__dirname}/fixtures/t4.dtc.js`, testCase: testCase4}, - ], + [], [], [], `./test/fixtures/config.js`, diff --git a/dtc/src/cli.ts b/dtc/src/cli.ts index 4fcfade..67d781c 100755 --- a/dtc/src/cli.ts +++ b/dtc/src/cli.ts @@ -21,7 +21,7 @@ const main = async ({projectPath, configPath, filePath, runnerArgs}: CliArgs) => const testCaseExecutions = await loadTestCases(projectPath)(config)(filePath) if (testCaseExecutions.length === 0) { - warnExit(`No test cases found for test regex: ${config.testRegex}`) + warnExit(`No test cases found for test pattern: ${config.testPattern}`) } await config.runner(testCaseExecutions, config.plugins, runnerArgs, configPath, filePath) diff --git a/dtc/src/config.ts b/dtc/src/config.ts index 3203dfc..3f696f1 100644 --- a/dtc/src/config.ts +++ b/dtc/src/config.ts @@ -5,29 +5,29 @@ export type Config = { plugins: Plugin[] loader: Loader runner: Runner - testRegex: RegExp + testPattern: string } export const resolveConfig = async (configPath?: string): Promise => { let runner: Runner = defaultTestRunner let plugins = defaultPlugins let loader = defaultLoader - let testRegex = /.*\.dtc\.[jt]s?$/ + let testPattern = '**/*.dtc.{ts,js}' if (configPath) { const { plugins: customPlugins, loader: customLoad, runner: customTestRunner, - testRegex: customTestRegex, + testPattern: customTestPattern, } = await defaultLoader(`${process.cwd()}/${configPath}`) runner = customTestRunner ?? defaultTestRunner loader = customLoad ?? loader plugins = customPlugins ? customPlugins : defaultPlugins - testRegex = customTestRegex ?? testRegex + testPattern = customTestPattern ?? testPattern } - return {loader, plugins, runner, testRegex} + return {loader, plugins, runner, testPattern} } diff --git a/dtc/src/loader.ts b/dtc/src/loader.ts index 981f1eb..c5dba6d 100644 --- a/dtc/src/loader.ts +++ b/dtc/src/loader.ts @@ -1,28 +1,26 @@ -import {readdir, stat} from 'node:fs/promises' +import {glob} from 'node:fs/promises' import {join, dirname} from 'node:path' import {assert} from '@cgauge/type-guard' import {TestCaseExecution, Loader, TestCase, Layer} from './domain.js' import {merge} from './utils.js' import {resolveParameters} from './parameters.js' -const generateFileList = async (currentPath: string, testRegex: RegExp): Promise => { - const files = await readdir(currentPath) - - const recursiveLoad = files.map(async (file) => { - const filePath = join(currentPath, file) - const stats = await stat(filePath) - if (stats.isDirectory()) { - return generateFileList(filePath, testRegex) - } else if (testRegex.test(filePath)) { - return filePath - } - - return null - }) +export const isGlobPattern = (input: string): boolean => { + return /[*?{}]/.test(input) +} - const result = await Promise.all(recursiveLoad) +export const resolveGlob = async (pattern: string, cwd: string): Promise => { + const matches: string[] = [] + for await (const match of glob(pattern, {cwd})) { + matches.push(match) + } + const sorted = matches.sort() + return sorted +} - return result.filter((v) => v !== null).flat() +const generateFileList = async (pattern: string, currentPath: string): Promise => { + const files = await resolveGlob(pattern, currentPath) + return files.map((f) => join(currentPath, f)) } const loadTestCase = @@ -66,14 +64,20 @@ const loadTestCase = export const loadTestCases = (projectPath: string) => - (config: {loader: Loader; testRegex: RegExp}) => + (config: {loader: Loader; testPattern: string}) => async (filePath?: string): Promise => { - if (filePath) { + if (filePath && !isGlobPattern(filePath)) { return loadTestCase(config.loader)(join(projectPath, filePath)) } - const files = await generateFileList(projectPath, config.testRegex) - const testCaseExecutions = await Promise.all(files.map((v) => loadTestCase(config.loader)(v))) + let testFiles = await generateFileList(config.testPattern, projectPath) + + if (filePath) { + const requestedFiles = await generateFileList(filePath, projectPath) + testFiles = requestedFiles.filter((f) => testFiles.includes(f)) + } + + const testCaseExecutions = await Promise.all([...testFiles].map((v) => loadTestCase(config.loader)(v))) return testCaseExecutions.flat() } diff --git a/dtc/test/fixtures/dtc.config.ts b/dtc/test/fixtures/dtc.config.ts index 88bc89c..67a4cc9 100644 --- a/dtc/test/fixtures/dtc.config.ts +++ b/dtc/test/fixtures/dtc.config.ts @@ -1,3 +1,3 @@ -export default { - testRegex: /.*\.invalid.file?$/, +export default { + testPattern: '**/*.invalid.file', } diff --git a/dtc/test/glob-integration.test.ts b/dtc/test/glob-integration.test.ts new file mode 100644 index 0000000..f4870ed --- /dev/null +++ b/dtc/test/glob-integration.test.ts @@ -0,0 +1,60 @@ +import {describe, it} from 'node:test' +import {spawnSync} from 'node:child_process' +import nodeAssert from 'node:assert' +import {dirname, join} from 'node:path' +import {fileURLToPath} from 'node:url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const cliPath = join(__dirname, '..', 'src', 'cli.ts') +const dtcRoot = join(__dirname, '..') + +describe('glob pattern integration', () => { + it('exits with warning when glob matches zero files', () => { + const result = spawnSync('npx', ['tsx', cliPath, 'test/fixtures/nonexistent-*.dtc.ts'], { + encoding: 'utf-8', + cwd: dtcRoot, + env: {...process.env, NODE_DEBUG: 'dtc'}, + }) + + nodeAssert.equal(result.status, 1) + nodeAssert.ok( + result.stderr.includes('No test cases found'), + `Expected stderr to include "No test cases found", got: ${result.stderr}`, + ) + }) + + it('passes runner arguments through when combined with glob pattern', () => { + const result = spawnSync('npx', ['tsx', cliPath, 'test/fixtures/t*.dtc.ts', '--', '--some-arg', 'value'], { + encoding: 'utf-8', + cwd: dtcRoot, + }) + + nodeAssert.equal(result.status, 0, `Expected exit code 0, got ${result.status}. stderr: ${result.stderr}`) + }) + + it('resolves config before glob files are loaded', () => { + const result = spawnSync( + 'npx', + ['tsx', cliPath, '--config', 'test/fixtures/dtc.config.ts', 'test/fixtures/t*.dtc.ts'], + { + encoding: 'utf-8', + cwd: dtcRoot, + }, + ) + + // Config testPattern filters out all files since the glob results don't match '**/*.invalid.file' + nodeAssert.equal(result.status, 1, `Expected exit code 1, got ${result.status}. stderr: ${result.stderr}`) + }) + + it('loads multiple .dtc.ts fixture files via a glob pattern', () => { + const result = spawnSync('npx', ['tsx', cliPath, 'test/fixtures/t*.dtc.ts'], { + encoding: 'utf-8', + cwd: dtcRoot, + }) + + nodeAssert.equal(result.status, 0, `Expected exit code 0, got ${result.status}. stderr: ${result.stderr}`) + nodeAssert.ok(result.stdout.includes('Test 1'), 'Expected output to include Test 1') + nodeAssert.ok(result.stdout.includes('Test 2'), 'Expected output to include Test 2') + nodeAssert.ok(result.stdout.includes('Test 4'), 'Expected output to include Test 4') + }) +}) diff --git a/dtc/test/loader.test.ts b/dtc/test/loader.test.ts index 68f71dd..24f8867 100644 --- a/dtc/test/loader.test.ts +++ b/dtc/test/loader.test.ts @@ -1,21 +1,21 @@ -import {test} from 'node:test' +import {describe, it, test} from 'node:test' import nodeAssert from 'node:assert' -import {loadTestCases} from '../src/loader.ts' +import {loadTestCases, isGlobPattern, resolveGlob} from '../src/loader.ts' import {defaultLoader} from '../src/index.ts' -import { dirname } from 'node:path' +import {dirname, join} from 'node:path' import {fileURLToPath} from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) test('It loads single file test cases', async () => { - const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testRegex: /.*\.dtc\.[jt]s?$/})(`./fixtures/t1.dtc.ts`) + const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})(`./fixtures/t1.dtc.ts`) nodeAssert.equal(`${__dirname}/fixtures/t1.dtc.ts`, testCaseExecutions[0].filePath) nodeAssert.equal('Test 1', testCaseExecutions[0].testCase.name) }) -test('It loads files using regex', async () => { - const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testRegex: /.*\.dtc\.[jt]s?$/})() +test('It loads files using glob pattern', async () => { + const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})() nodeAssert.ok(testCaseExecutions.length > 1) @@ -27,7 +27,7 @@ test('It loads files using regex', async () => { }) test('It replaces parameters placeholders', async () => { - const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testRegex: /.*\.dtc\.[jt]s?$/})(`./fixtures/t1.dtc.ts`) + const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})(`./fixtures/t1.dtc.ts`) nodeAssert.equal(`${__dirname}/fixtures/t1.dtc.ts`, testCaseExecutions[0].filePath) nodeAssert.equal('Test 1', testCaseExecutions[0].testCase.name) @@ -37,7 +37,7 @@ test('It replaces parameters placeholders', async () => { }) test('It replaces multiple parameters placeholders', async () => { - const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testRegex: /.*\.dtc\.[jt]s?$/})(`./fixtures/t2.dtc.ts`) + const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})(`./fixtures/t2.dtc.ts`) nodeAssert.equal(`${__dirname}/fixtures/t2.dtc.ts`, testCaseExecutions[0].filePath) nodeAssert.equal('Test 2 (provider 0)', testCaseExecutions[0].testCase.name) @@ -51,7 +51,7 @@ test('It replaces multiple parameters placeholders', async () => { }) test('It replaces parameters placeholders defined in the layers', async () => { - const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testRegex: /.*\.dtc\.[jt]s?$/})(`./fixtures/t4.dtc.ts`) + const testCaseExecutions = await loadTestCases(__dirname)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})(`./fixtures/t4.dtc.ts`) nodeAssert.equal(`${__dirname}/fixtures/t4.dtc.ts`, testCaseExecutions[0].filePath) nodeAssert.equal('Test 4', testCaseExecutions[0].testCase.name) @@ -62,3 +62,92 @@ test('It replaces parameters placeholders defined in the layers', async () => { nodeAssert.deepStrictEqual(testCaseExecutions[0].testCase.act?.arguments, [{a: 'content b'}]) nodeAssert.deepStrictEqual(testCaseExecutions[0].testCase.assert, {a: 'content b'}) }) + +describe('isGlobPattern', () => { + it('returns false for plain paths', () => { + nodeAssert.strictEqual(isGlobPattern('test/foo.ts'), false) + nodeAssert.strictEqual(isGlobPattern('./src/index.ts'), false) + nodeAssert.strictEqual(isGlobPattern('src/utils/helper.js'), false) + }) + + it('returns true for patterns with * wildcard', () => { + nodeAssert.strictEqual(isGlobPattern('test/*.ts'), true) + nodeAssert.strictEqual(isGlobPattern('src/**/*.js'), true) + }) + + it('returns true for patterns with ? wildcard', () => { + nodeAssert.strictEqual(isGlobPattern('file?.ts'), true) + }) +}) + +describe('loadTestCases glob branch', () => { + const fixturesDir = join(__dirname, 'fixtures') + + it('resolves glob pattern and loads multiple test files', async () => { + const results = await loadTestCases(fixturesDir)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})('*.dtc.ts') + + nodeAssert.ok(results.length >= 3) + nodeAssert.ok(results.some((r) => r.testCase.name.startsWith('Test 1'))) + nodeAssert.ok(results.some((r) => r.testCase.name.startsWith('Test 2'))) + nodeAssert.ok(results.some((r) => r.testCase.name.startsWith('Test 4'))) + }) + + it('glob pattern controls which files are loaded', async () => { + const results = await loadTestCases(fixturesDir)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})('t1.dtc.ts') + + nodeAssert.ok(results.length >= 1) + nodeAssert.ok(results.every((r) => r.filePath.includes('t1.dtc.ts'))) + }) + + it('returns results in alphabetical order', async () => { + const results = await loadTestCases(fixturesDir)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})('*.dtc.ts') + + const filePaths = results.map((r) => r.filePath) + for (let i = 1; i < filePaths.length; i++) { + nodeAssert.ok(filePaths[i] >= filePaths[i - 1], `Expected ${filePaths[i]} >= ${filePaths[i - 1]}`) + } + }) + + it('returns empty array when glob matches zero files', async () => { + const results = await loadTestCases(fixturesDir)({loader: defaultLoader, testPattern: '**/*.dtc.{ts,js}'})('nonexistent-*.dtc.ts') + + nodeAssert.deepStrictEqual(results, []) + }) +}) + +describe('resolveGlob', () => { + const fixturesDir = join(__dirname, 'fixtures') + + it('expands * wildcard to match files in a directory', async () => { + const results = await resolveGlob('*.dtc.ts', fixturesDir) + + nodeAssert.ok(results.includes('t1.dtc.ts')) + nodeAssert.ok(results.includes('t2.dtc.ts')) + nodeAssert.ok(results.includes('t4.dtc.ts')) + }) + + it('expands ** for recursive matching', async () => { + const results = await resolveGlob('**/*.dtc.ts', fixturesDir) + + nodeAssert.ok(results.includes('t1.dtc.ts')) + nodeAssert.ok(results.includes('t2.dtc.ts')) + nodeAssert.ok(results.includes('t4.dtc.ts')) + nodeAssert.ok(results.some((r) => r.includes('tests/t3.dtc.ts'))) + }) + + it('expands ? for single-character matching', async () => { + const results = await resolveGlob('t?.dtc.ts', fixturesDir) + + nodeAssert.ok(results.includes('t1.dtc.ts')) + nodeAssert.ok(results.includes('t2.dtc.ts')) + nodeAssert.ok(results.includes('t4.dtc.ts')) + nodeAssert.ok(!results.includes('t10.dtc.ts')) + }) + + it('returns paths sorted alphabetically', async () => { + const results = await resolveGlob('*.dtc.ts', fixturesDir) + + const sorted = [...results].sort() + nodeAssert.deepStrictEqual(results, sorted) + }) +})