Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: support test.each filtering in browser mode #500

Merged
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
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
Loading