Skip to content

Commit

Permalink
fix: support test.each filtering in browser mode (#500)
Browse files Browse the repository at this point in the history
* fix: support test.each filtering in the browser mode

* refactor: cleanup

* chore: cleanup

* chore: use unique names ids for dynamic tests
  • Loading branch information
sheremet-va authored Oct 9, 2024
1 parent b18a514 commit b66b919
Show file tree
Hide file tree
Showing 4 changed files with 77 additions and 38 deletions.
7 changes: 5 additions & 2 deletions src/testTree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -316,10 +316,13 @@ export class TestTree extends vscode.Disposable {
log.error(`Cannot find location for "${testItem.label}". Using "id" to sort instead.`)
testItem.sortText = task.id
}
// dynamic exists only during browser collection
// see src/worker/collect.ts:172
const isDynamic = (task as any).dynamic
if (task.type === 'suite')
TestSuite.register(testItem, parent, fileData)
TestSuite.register(testItem, parent, fileData, isDynamic)
else if (task.type === 'test' || task.type === 'custom')
TestCase.register(testItem, parent, fileData)
TestCase.register(testItem, parent, fileData, isDynamic)

this.flatTestItems.set(task.id, testItem)
parent.children.add(testItem)
Expand Down
55 changes: 43 additions & 12 deletions src/testTreeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,16 +74,21 @@ export class TestFile extends BaseTestData {
class TaskName {
constructor(
private readonly data: TestData,
public readonly dynamic: boolean,
) {}

public get label() {
return this.data.label
}

getTestNamePattern() {
const patterns = [escapeRegex(this.data.label)]
const patterns = [escapeTestName(this.data.label, this.dynamic)]
let iter = this.data.parent
while (iter) {
// if we reached test file, then stop
if (iter instanceof TestFile || iter instanceof TestFolder)
break
patterns.push(escapeRegex(iter.label))
patterns.push(escapeTestName(iter.label, iter.name.dynamic))
iter = iter.parent
}
// vitest's test task name starts with ' ' of root suite
Expand All @@ -93,49 +98,75 @@ class TaskName {
}

export class TestCase extends BaseTestData {
private nameResolver: TaskName
public name: TaskName
public readonly type = 'test'

private constructor(
item: vscode.TestItem,
parent: vscode.TestItem,
public readonly file: TestFile,
dynamic: boolean,
) {
super(item, parent)
this.nameResolver = new TaskName(this)
this.name = new TaskName(this, dynamic)
}

public static register(item: vscode.TestItem, parent: vscode.TestItem, file: TestFile) {
return addTestData(item, new TestCase(item, parent, file))
public static register(item: vscode.TestItem, parent: vscode.TestItem, file: TestFile, dynamic: boolean) {
return addTestData(item, new TestCase(item, parent, file, dynamic))
}

getTestNamePattern() {
return `^${this.nameResolver.getTestNamePattern()}$`
return `^${this.name.getTestNamePattern()}$`
}
}

export class TestSuite extends BaseTestData {
private nameResolver: TaskName
public name: TaskName
public readonly type = 'suite'

private constructor(
item: vscode.TestItem,
parent: vscode.TestItem,
public readonly file: TestFile,
dynamic: boolean,
) {
super(item, parent)
this.nameResolver = new TaskName(this)
this.name = new TaskName(this, dynamic)
}

public static register(item: vscode.TestItem, parent: vscode.TestItem, file: TestFile) {
return addTestData(item, new TestSuite(item, parent, file))
public static register(item: vscode.TestItem, parent: vscode.TestItem, file: TestFile, dynamic: boolean) {
return addTestData(item, new TestSuite(item, parent, file, dynamic))
}

getTestNamePattern() {
return `^${this.nameResolver.getTestNamePattern()}`
return `^${this.name.getTestNamePattern()}`
}
}

function escapeRegex(str: string) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}

const kReplacers = new Map<string, string>([
['%i', '\\d+?'],
['%#', '\\d+?'],
['%d', '[\\d.eE+-]+?'],
['%f', '[\\d.eE+-]+?'],
['%s', '.+?'],
['%j', '.+?'],
['%o', '.+?'],
['%%', '%'],
])

function escapeTestName(label: string, dynamic: boolean) {
if (!dynamic) {
return escapeRegex(label)
}

// Replace object access patterns ($value, $obj.a) with %s first
let pattern = label.replace(/\$[a-z_.]+/gi, '%s')
pattern = escapeRegex(pattern)
// Replace percent placeholders with their respective regex
pattern = pattern.replace(/%[i#dfsjo%]/g, m => kReplacers.get(m) || m)
return pattern
}
45 changes: 25 additions & 20 deletions src/worker/collect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,13 @@ interface ParsedFile extends RunnerTestFile {
interface ParsedTest extends RunnerTestCase {
start: number
end: number
dynamic: boolean
}

interface ParsedSuite extends RunnerTestSuite {
start: number
end: number
dynamic: boolean
}

interface LocalCallDefinition {
Expand All @@ -36,6 +38,7 @@ interface LocalCallDefinition {
type: 'suite' | 'test'
mode: 'run' | 'skip' | 'only' | 'todo'
task: ParsedSuite | ParsedFile | ParsedTest
dynamic: boolean
}

export interface FileInformation {
Expand Down Expand Up @@ -105,7 +108,6 @@ export function astParseFile(filepath: string, code: string) {
const name = getName(callee)
let unknown = false
if (!name) {
verbose?.('Unknown call', callee)
return
}
if (!['it', 'test', 'describe', 'suite'].includes(name)) {
Expand All @@ -114,12 +116,8 @@ export function astParseFile(filepath: string, code: string) {
}
const property = callee?.property?.name
let mode = !property || property === name ? 'run' : property
if (property === 'skipIf' || property === 'runIf') {
// skip, it will pick up the correct one by name later
return
}
if (mode === 'each') {
debug?.('Skipping `.each` (support not implemented yet)', name)
// they will be picked up in the next iteration
if (['each', 'for', 'skipIf', 'runIf'].includes(mode)) {
return
}

Expand Down Expand Up @@ -160,6 +158,8 @@ export function astParseFile(filepath: string, code: string) {
if (mode === 'skipIf' || mode === 'runIf') {
mode = 'skip'
}
const parentCalleeName = typeof callee?.callee === 'object' && callee?.callee.type === 'MemberExpression' && callee?.callee.property?.name
const isDynamicEach = parentCalleeName === 'each' || parentCalleeName === 'for'
debug?.('Found', name, message, `(${mode})`)
definitions.push({
start,
Expand All @@ -169,6 +169,7 @@ export function astParseFile(filepath: string, code: string) {
type: name === 'it' || name === 'test' ? 'test' : 'suite',
mode,
task: null as any,
dynamic: isDynamicEach,
} satisfies LocalCallDefinition)
},
})
Expand Down Expand Up @@ -205,15 +206,9 @@ export async function astCollectTests(
file: null!,
}
file.file = file
if (verbose) {
verbose('Collecing', testFilepath, request.code)
}
else {
debug?.('Collecting', testFilepath)
}
const indexMap = createIndexMap(request.code)
const map = request.map && new TraceMap(request.map as any)
let lastSuite: ParsedSuite = file
let lastSuite: ParsedSuite = file as any
const updateLatestSuite = (index: number) => {
while (lastSuite.suite && lastSuite.end < index) {
lastSuite = lastSuite.suite as ParsedSuite
Expand Down Expand Up @@ -276,9 +271,8 @@ export async function astCollectTests(
end: definition.end,
start: definition.start,
location,
meta: {
typecheck: true,
},
dynamic: definition.dynamic,
meta: {},
}
definition.task = task
latestSuite.tasks.push(task)
Expand All @@ -296,9 +290,8 @@ export async function astCollectTests(
end: definition.end,
start: definition.start,
location,
meta: {
typecheck: true,
},
dynamic: definition.dynamic,
meta: {},
}
definition.task = task
latestSuite.tasks.push(task)
Expand All @@ -312,6 +305,7 @@ export async function astCollectTests(
false,
ctx.config.allowOnly,
)
markDynamicTests(file.tasks)
if (!file.tasks.length) {
file.result = {
state: 'fail',
Expand Down Expand Up @@ -418,6 +412,17 @@ function interpretTaskModes(
}
}

function markDynamicTests(tasks: TaskBase[]) {
for (const task of tasks) {
if ((task as any).dynamic) {
task.id += '-dynamic'
}
if ('children' in task) {
markDynamicTests(task.children as TaskBase[])
}
}
}

function checkAllowOnly(task: TaskBase, allowOnly?: boolean) {
if (allowOnly) {
return
Expand Down
8 changes: 4 additions & 4 deletions test/TestData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,13 @@ describe('TestData', () => {
suiteItem.children.add(testItem2)
suiteItem.children.add(testItem3)

const suite = TestSuite.register(suiteItem, testItem, file)
const suite = TestSuite.register(suiteItem, testItem, file, false)

expect(suite.getTestNamePattern()).to.equal('^\\s?describe')

const test1 = TestCase.register(testItem1, suiteItem, file)
const test2 = TestCase.register(testItem2, suiteItem, file)
const test3 = TestCase.register(testItem3, suiteItem, file)
const test1 = TestCase.register(testItem1, suiteItem, file, false)
const test2 = TestCase.register(testItem2, suiteItem, file, false)
const test3 = TestCase.register(testItem3, suiteItem, file, false)

expect(testItem1.parent).to.exist

Expand Down

0 comments on commit b66b919

Please sign in to comment.