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
2 changes: 1 addition & 1 deletion docs/docs/dtc/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}',
}
```

Expand Down
4 changes: 2 additions & 2 deletions dtc-playwright-plugin/src/browser.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion dtc-playwright-plugin/test/fixtures/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
}
15 changes: 1 addition & 14 deletions dtc-playwright-plugin/test/playwright-runner.test.ts
Original file line number Diff line number Diff line change
@@ -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`,
Expand Down
2 changes: 1 addition & 1 deletion dtc/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 5 additions & 5 deletions dtc/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,29 +5,29 @@ export type Config = {
plugins: Plugin[]
loader: Loader
runner: Runner
testRegex: RegExp
testPattern: string
}

export const resolveConfig = async (configPath?: string): Promise<Config> => {
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}
}

46 changes: 25 additions & 21 deletions dtc/src/loader.ts
Original file line number Diff line number Diff line change
@@ -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<string[]> => {
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<string[]> => {
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<string[]> => {
const files = await resolveGlob(pattern, currentPath)
return files.map((f) => join(currentPath, f))
}

const loadTestCase =
Expand Down Expand Up @@ -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<TestCaseExecution[]> => {
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()
}
4 changes: 2 additions & 2 deletions dtc/test/fixtures/dtc.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export default {
testRegex: /.*\.invalid.file?$/,
export default {
testPattern: '**/*.invalid.file',
}
60 changes: 60 additions & 0 deletions dtc/test/glob-integration.test.ts
Original file line number Diff line number Diff line change
@@ -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')
})
})
107 changes: 98 additions & 9 deletions dtc/test/loader.test.ts
Original file line number Diff line number Diff line change
@@ -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)

Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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)
})
})
Loading