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(watch): run test files when added to filesystem #3189

Merged
merged 3 commits into from
Apr 14, 2023
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
28 changes: 10 additions & 18 deletions packages/vitest/src/node/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export class Vitest {
this.pool = undefined
this.coverageProvider = undefined
this.runningPromise = undefined
this.projectsTestFiles.clear()

const resolved = resolveConfig(this.mode, options, server.config)

Expand Down Expand Up @@ -591,7 +592,15 @@ export class Vitest {
const onAdd = async (id: string) => {
id = slash(id)
updateLastChanged(id)
if (await this.isTargetFile(id)) {

const matchingProjects: WorkspaceProject[] = []
await Promise.all(this.projects.map(async (project) => {
if (await project.isTargetFile(id))
matchingProjects.push(project)
}))

if (matchingProjects.length > 0) {
this.projectsTestFiles.set(id, new Set(matchingProjects))
this.changedTests.add(id)
this.scheduleRerun([id])
}
Expand Down Expand Up @@ -738,28 +747,11 @@ export class Vitest {
return files
}

private async isTargetFile(id: string, source?: string): Promise<boolean> {
const relativeId = relative(this.config.dir || this.config.root, id)
if (mm.isMatch(relativeId, this.config.exclude))
return false
if (mm.isMatch(relativeId, this.config.include))
return true
if (this.config.includeSource?.length && mm.isMatch(relativeId, this.config.includeSource)) {
source = source || await fs.readFile(id, 'utf-8')
return this.isInSourceTestFile(source)
}
return false
}

// The server needs to be running for communication
shouldKeepServer() {
return !!this.config?.watch
}

isInSourceTestFile(code: string) {
return code.includes('import.meta.vitest')
}

onServerRestart(fn: OnServerRestartHandler) {
this._onRestartListeners.push(fn)
}
Expand Down
22 changes: 20 additions & 2 deletions packages/vitest/src/node/workspace.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { promises as fs } from 'node:fs'
import fg from 'fast-glob'
import { dirname, resolve, toNamespacedPath } from 'pathe'
import mm from 'micromatch'
import { dirname, relative, resolve, toNamespacedPath } from 'pathe'
import { createServer } from 'vite'
import type { ViteDevServer, InlineConfig as ViteInlineConfig } from 'vite'
import { ViteNodeRunner } from 'vite-node/client'
Expand Down Expand Up @@ -108,7 +109,7 @@ export class WorkspaceProject {
await Promise.all(files.map(async (file) => {
try {
const code = await fs.readFile(file, 'utf-8')
if (this.ctx.isInSourceTestFile(code))
if (this.isInSourceTestFile(code))
testFiles.push(file)
}
catch {
Expand All @@ -131,6 +132,23 @@ export class WorkspaceProject {
return fg(include, globOptions)
}

async isTargetFile(id: string, source?: string): Promise<boolean> {
const relativeId = relative(this.config.dir || this.config.root, id)
if (mm.isMatch(relativeId, this.config.exclude))
return false
if (mm.isMatch(relativeId, this.config.include))
return true
if (this.config.includeSource?.length && mm.isMatch(relativeId, this.config.includeSource)) {
source = source || await fs.readFile(id, 'utf-8')
return this.isInSourceTestFile(source)
}
return false
}

isInSourceTestFile(code: string) {
return code.includes('import.meta.vitest')
}

filterFiles(testFiles: string[], filters: string[] = []) {
if (filters.length && process.platform === 'win32')
filters = filters.map(f => toNamespacedPath(f))
Expand Down
25 changes: 24 additions & 1 deletion test/watch/test/file-watching.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { readFileSync, writeFileSync } from 'node:fs'
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
import { afterEach, describe, test } from 'vitest'

import { startWatchMode } from './utils'
Expand All @@ -12,6 +12,8 @@ const testFileContent = readFileSync(testFile, 'utf-8')
const configFile = 'fixtures/vitest.config.ts'
const configFileContent = readFileSync(configFile, 'utf-8')

const cleanups: (() => void)[] = []

function editFile(fileContent: string) {
return `// Modified by file-watching.test.ts
${fileContent}
Expand All @@ -23,6 +25,7 @@ afterEach(() => {
writeFileSync(sourceFile, sourceFileContent, 'utf8')
writeFileSync(testFile, testFileContent, 'utf8')
writeFileSync(configFile, configFileContent, 'utf8')
cleanups.splice(0).forEach(cleanup => cleanup())
})

test('editing source file triggers re-run', async () => {
Expand Down Expand Up @@ -64,6 +67,26 @@ test('editing config file reloads new changes', async () => {
await vitest.waitForOutput('ok 2')
})

test('adding a new test file triggers re-run', async () => {
const vitest = await startWatchMode()

const testFile = 'fixtures/new-dynamic.test.ts'
const testFileContent = `
import { expect, test } from "vitest";

test("dynamic test case", () => {
console.log("Running added dynamic test")
expect(true).toBeTruthy()
})
`
cleanups.push(() => rmSync(testFile))
writeFileSync(testFile, testFileContent, 'utf-8')

await vitest.waitForOutput('Running added dynamic test')
await vitest.waitForOutput('RERUN ../new-dynamic.test.ts')
await vitest.waitForOutput('1 passed')
})

describe('browser', () => {
test.runIf((process.platform !== 'win32'))('editing source file triggers re-run', async () => {
const vitest = await startWatchMode('--browser.enabled', '--browser.headless', '--browser.name=chrome')
Expand Down
63 changes: 60 additions & 3 deletions test/watch/test/workspaces.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,30 @@
import { fileURLToPath } from 'node:url'
import { readFileSync, writeFileSync } from 'node:fs'
import { afterAll, it } from 'vitest'
import { readFileSync, rmSync, writeFileSync } from 'node:fs'
import { afterAll, afterEach, expect, it } from 'vitest'
import { dirname, resolve } from 'pathe'
import { startWatchMode } from './utils'

const file = fileURLToPath(import.meta.url)
const dir = dirname(file)
const root = resolve(dir, '..', '..', 'workspaces')
const config = resolve(root, 'vitest.config.ts')
const cleanups: (() => void)[] = []

const srcMathFile = resolve(root, 'src', 'math.ts')
const specSpace2File = resolve(root, 'space_2', 'test', 'node.spec.ts')

const srcMathContent = readFileSync(srcMathFile, 'utf-8')
const specSpace2Content = readFileSync(specSpace2File, 'utf-8')

const dynamicTestContent = `// Dynamic test added by test/watch/test/workspaces.test.ts
import { expect, test } from "vitest";

test("dynamic test case", () => {
console.log("Running added dynamic test")
expect(true).toBeTruthy()
})
`

function startVitest() {
return startWatchMode(
{ cwd: root, env: { TEST_WATCH: 'true' } },
Expand All @@ -27,6 +37,10 @@ function startVitest() {
)
}

afterEach(() => {
cleanups.splice(0).forEach(cleanup => cleanup())
})

afterAll(() => {
writeFileSync(srcMathFile, srcMathContent, 'utf8')
writeFileSync(specSpace2File, specSpace2Content, 'utf8')
Expand All @@ -48,7 +62,7 @@ it('editing a file that is imported in different workspaces reruns both files',
writeFileSync(srcMathFile, `${srcMathContent}\n`, 'utf8')

await vitest.waitForOutput('RERUN src/math.ts')
await vitest.waitForOutput('|space_3| math.space-test.ts')
await vitest.waitForOutput('|space_3| math.space-3-test.ts')
await vitest.waitForOutput('|space_1| test/math.spec.ts')
await vitest.waitForOutput('Test Files 2 passed')
})
Expand All @@ -65,3 +79,46 @@ it('filters by test name inside a workspace', async () => {
await vitest.waitForOutput('Test name pattern: /2 x 2 = 4/')
await vitest.waitForOutput('Test Files 1 passed')
})

it('adding a new test file matching core project config triggers re-run', async () => {
const vitest = await startVitest()

const testFile = resolve(root, 'space_2', 'test', 'new-dynamic.test.ts')

cleanups.push(() => rmSync(testFile))
writeFileSync(testFile, dynamicTestContent, 'utf-8')

await vitest.waitForOutput('Running added dynamic test')
await vitest.waitForOutput('RERUN space_2/test/new-dynamic.test.ts')
await vitest.waitForOutput('|space_2| test/new-dynamic.test.ts')

// Wait for tests to end
await vitest.waitForOutput('Waiting for file changes')

// Test case should not be run by other projects
expect(vitest.output).not.include('|space_1|')
expect(vitest.output).not.include('|space_3|')
expect(vitest.output).not.include('|node|')
expect(vitest.output).not.include('|happy-dom|')
})

it('adding a new test file matching project specific config triggers re-run', async () => {
const vitest = await startVitest()

const testFile = resolve(root, 'space_3', 'new-dynamic.space-3-test.ts')
cleanups.push(() => rmSync(testFile))
writeFileSync(testFile, dynamicTestContent, 'utf-8')

await vitest.waitForOutput('Running added dynamic test')
await vitest.waitForOutput('RERUN space_3/new-dynamic.space-3-test.ts')
await vitest.waitForOutput('|space_3| new-dynamic.space-3-test.ts')

// Wait for tests to end
await vitest.waitForOutput('Waiting for file changes')

// Test case should not be run by other projects
expect(vitest.output).not.include('|space_1|')
expect(vitest.output).not.include('|space_2|')
expect(vitest.output).not.include('|node|')
expect(vitest.output).not.include('|happy-dom|')
})
2 changes: 1 addition & 1 deletion test/workspaces/space_3/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { defineProject } from 'vitest/config'

export default defineProject({
test: {
include: ['**/*.space-test.ts'],
include: ['**/*.space-3-test.ts'],
name: 'space_3',
environment: 'node',
},
Expand Down