+
-
-
+
+
+
+
+
{{ showDetailsTooltip }}: this feature is not available, you have disabled includeTaskLocation in your configuration file.
+
+ Clicking this button the code tab will position the cursor at first line in the source code since the UI doesn't have the information available.
+
+
+
+ {{ showDetailsTooltip }}
+
+
+
+
@@ -185,8 +232,7 @@ const highlighted = computed(() => {
.test-actions {
display: none;
}
-.item-wrapper:hover .test-actions,
-.item-wrapper[data-current="true"] .test-actions {
+.item-wrapper:hover .test-actions {
display: flex;
}
diff --git a/packages/ui/client/components/views/ViewEditor.vue b/packages/ui/client/components/views/ViewEditor.vue
index 2a6d41865..c223a7e02 100644
--- a/packages/ui/client/components/views/ViewEditor.vue
+++ b/packages/ui/client/components/views/ViewEditor.vue
@@ -3,7 +3,9 @@ import type CodeMirror from 'codemirror'
import type { ErrorWithDiff, File } from 'vitest'
import { createTooltip, destroyTooltip } from 'floating-vue'
import { openInEditor } from '~/composables/error'
-import { client } from '~/composables/client'
+import { client, isReport } from '~/composables/client'
+import { codemirrorRef } from '~/composables/codemirror'
+import { lineNumber } from '~/composables/params'
const props = defineProps<{
file?: File
@@ -14,23 +16,52 @@ const emit = defineEmits<{ (event: 'draft', value: boolean): void }>()
const code = ref('')
const serverCode = shallowRef
(undefined)
const draft = ref(false)
+const loading = ref(true)
watch(
() => props.file,
async () => {
- if (!props.file || !props.file?.filepath) {
- code.value = ''
+ loading.value = true
+ try {
+ if (!props.file || !props.file?.filepath) {
+ code.value = ''
+ serverCode.value = code.value
+ draft.value = false
+ return
+ }
+
+ code.value = (await client.rpc.readTestFile(props.file.filepath)) || ''
serverCode.value = code.value
draft.value = false
- return
}
- code.value = (await client.rpc.readTestFile(props.file.filepath)) || ''
- serverCode.value = code.value
- draft.value = false
+ finally {
+ // fire focusing editor after loading
+ nextTick(() => (loading.value = false))
+ }
},
{ immediate: true },
)
+watch(() => [loading.value, props.file, lineNumber.value] as const, ([loadingFile, _, l]) => {
+ if (!loadingFile) {
+ if (l != null) {
+ nextTick(() => {
+ const line = { line: l ?? 0, ch: 0 }
+ codemirrorRef.value?.scrollIntoView(line, 100)
+ nextTick(() => {
+ codemirrorRef.value?.focus()
+ codemirrorRef.value?.setCursor(line)
+ })
+ })
+ }
+ else {
+ nextTick(() => {
+ codemirrorRef.value?.focus()
+ })
+ }
+ }
+}, { flush: 'post' })
+
const ext = computed(() => props.file?.filepath?.split(/\./g).pop() || 'js')
const editor = ref()
@@ -56,11 +87,11 @@ function clearListeners() {
}
useResizeObserver(editor, () => {
- cm.value?.refresh()
+ codemirrorRef.value?.refresh()
})
function codemirrorChanges() {
- draft.value = serverCode.value !== cm.value!.getValue()
+ draft.value = serverCode.value !== codemirrorRef.value!.getValue()
}
watch(
@@ -105,8 +136,8 @@ function createErrorElement(e: ErrorWithDiff) {
}
div.appendChild(span)
listeners.push([span, el, () => destroyTooltip(span)])
- handles.push(cm.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
- widgets.push(cm.value!.addLineWidget(stack.line - 1, div))
+ handles.push(codemirrorRef.value!.addLineClass(stack.line - 1, 'wrap', 'bg-red-500/10'))
+ widgets.push(codemirrorRef.value!.addLineWidget(stack.line - 1, div))
}
watch(
@@ -120,7 +151,7 @@ watch(
setTimeout(() => {
clearListeners()
widgets.forEach(widget => widget.clear())
- handles.forEach(h => cm.value?.removeLineClass(h, 'wrap'))
+ handles.forEach(h => codemirrorRef.value?.removeLineClass(h, 'wrap'))
widgets.length = 0
handles.length = 0
@@ -146,11 +177,11 @@ async function onSave(content: string) {
- >
unhandledErrors: unknown[]
+ // filename -> source
+ sources: Record
}
const noop: any = () => {}
@@ -51,13 +53,7 @@ export function createStaticClient(): VitestClient {
getUnhandledErrors: () => {
return metadata.unhandledErrors
},
- getTransformResult: async (id) => {
- return {
- code: id,
- source: '',
- map: null,
- }
- },
+ getTransformResult: asyncNoop,
onDone: noop,
onCollected: asyncNoop,
onTaskUpdate: noop,
@@ -73,7 +69,9 @@ export function createStaticClient(): VitestClient {
resolveSnapshotRawPath: asyncNoop,
readSnapshotFile: asyncNoop,
saveSnapshotFile: asyncNoop,
- readTestFile: asyncNoop,
+ readTestFile: async (id: string) => {
+ return metadata.sources[id]
+ },
removeSnapshotFile: asyncNoop,
onUnhandledError: noop,
saveTestFile: asyncNoop,
diff --git a/packages/ui/client/composables/codemirror.ts b/packages/ui/client/composables/codemirror.ts
index 8ddcc4bd6..f26d8a310 100644
--- a/packages/ui/client/composables/codemirror.ts
+++ b/packages/ui/client/composables/codemirror.ts
@@ -11,6 +11,10 @@ import 'codemirror/mode/jsx/jsx'
import 'codemirror/addon/display/placeholder'
import 'codemirror/addon/scroll/simplescrollbars'
import 'codemirror/addon/scroll/simplescrollbars.css'
+import type { Task } from '@vitest/runner'
+import { navigateTo } from '~/composables/navigation'
+
+export const codemirrorRef = shallowRef()
export function useCodeMirror(
textarea: Ref,
@@ -50,5 +54,13 @@ export function useCodeMirror(
{ immediate: true },
)
+ onUnmounted(() => {
+ codemirrorRef.value = undefined
+ })
+
return markRaw(cm)
}
+
+export async function showSource(task: Task) {
+ navigateTo(task, task.location?.line ?? 0)
+}
diff --git a/packages/ui/client/composables/navigation.ts b/packages/ui/client/composables/navigation.ts
index 2a25ca42b..c5cf8e28a 100644
--- a/packages/ui/client/composables/navigation.ts
+++ b/packages/ui/client/composables/navigation.ts
@@ -1,7 +1,7 @@
-import type { File } from '@vitest/runner'
+import type { File, Task } from '@vitest/runner'
import { client, config, findById } from './client'
import { testRunState } from './client/state'
-import { activeFileId } from './params'
+import { activeFileId, lineNumber, viewMode } from './params'
export const currentModule = ref()
export const dashboardVisible = ref(true)
@@ -47,6 +47,7 @@ export const coverageUrl = computed(() => {
return undefined
})
+
watch(
testRunState,
(state) => {
@@ -54,6 +55,7 @@ watch(
},
{ immediate: true },
)
+
export function initializeNavigation() {
const file = activeFileId.value
if (file && file.length > 0) {
@@ -87,6 +89,20 @@ export function showDashboard(show: boolean) {
}
}
+export function navigateTo(task: Task, line: number | null = null) {
+ activeFileId.value = task.file.id
+ // reset line number
+ lineNumber.value = null
+ if (line != null) {
+ nextTick(() => {
+ lineNumber.value = line
+ })
+ viewMode.value = 'editor'
+ }
+ currentModule.value = findById(task.file.id)
+ showDashboard(false)
+}
+
export function showCoverage() {
coverageVisible.value = true
dashboardVisible.value = false
diff --git a/packages/ui/client/composables/params.ts b/packages/ui/client/composables/params.ts
index 33ad473f8..07218439a 100644
--- a/packages/ui/client/composables/params.ts
+++ b/packages/ui/client/composables/params.ts
@@ -1,14 +1,17 @@
export interface Params {
file: string
view: null | 'graph' | 'editor' | 'console'
+ line: null | number
}
export const params = useUrlSearchParams('hash', {
initialValue: {
file: '',
view: null,
+ line: null,
},
})
export const activeFileId = toRef(params, 'file')
export const viewMode = toRef(params, 'view')
+export const lineNumber = toRef(params, 'line')
diff --git a/packages/ui/node/reporter.ts b/packages/ui/node/reporter.ts
index 47add940c..2dffe8bcd 100644
--- a/packages/ui/node/reporter.ts
+++ b/packages/ui/node/reporter.ts
@@ -38,6 +38,8 @@ interface HTMLReportData {
config: ResolvedConfig
moduleGraph: Record>
unhandledErrors: unknown[]
+ // filename -> source
+ sources: Record
}
const distDir = resolve(fileURLToPath(import.meta.url), '../../dist')
@@ -64,6 +66,7 @@ export default class HTMLReporter implements Reporter {
config: this.ctx.config,
unhandledErrors: this.ctx.state.getUnhandledErrors(),
moduleGraph: {},
+ sources: {},
}
await Promise.all(
result.files.map(async (file) => {
@@ -74,6 +77,16 @@ export default class HTMLReporter implements Reporter {
projectName,
file.filepath,
)
+ if (!result.sources[file.filepath]) {
+ try {
+ result.sources[file.filepath] = await fs.readFile(file.filepath, {
+ encoding: 'utf-8',
+ })
+ }
+ catch (_) {
+ // just ignore
+ }
+ }
}),
)
await this.writeReport(stringify(result))
diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts
index b009e7294..f07163183 100644
--- a/packages/vitest/src/node/config.ts
+++ b/packages/vitest/src/node/config.ts
@@ -720,6 +720,28 @@ export function resolveConfig(
port: defaultBrowserPort,
}
+ // enable includeTaskLocation by default in UI mode
+ if (resolved.browser.enabled) {
+ if (resolved.browser.ui) {
+ resolved.includeTaskLocation ??= true
+ }
+ }
+ else if (resolved.ui) {
+ resolved.includeTaskLocation ??= true
+ }
+
+ const htmlReporter = toArray(resolved.reporters).some((reporter) => {
+ if (Array.isArray(reporter)) {
+ return reporter[0] === 'html'
+ }
+
+ return false
+ })
+
+ if (htmlReporter) {
+ resolved.includeTaskLocation ??= true
+ }
+
resolved.testTransformMode ??= {}
resolved.testTimeout ??= resolved.browser.enabled ? 15000 : 5000
diff --git a/test/reporters/tests/__snapshots__/html.test.ts.snap b/test/reporters/tests/__snapshots__/html.test.ts.snap
index 24b03826e..6774077b8 100644
--- a/test/reporters/tests/__snapshots__/html.test.ts.snap
+++ b/test/reporters/tests/__snapshots__/html.test.ts.snap
@@ -28,6 +28,10 @@ exports[`html reporter > resolves to "failing" status for test file "json-fail"
{
"file": [Circular],
"id": 0,
+ "location": {
+ "column": 1,
+ "line": 5,
+ },
"logs": [
{
"content": "json-fail>should fail
@@ -93,6 +97,18 @@ exports[`html reporter > resolves to "failing" status for test file "json-fail"
"paths": [
"/test/reporters/fixtures/json-fail.test.ts",
],
+ "sources": {
+ "/test/reporters/fixtures/json-fail.test.ts": "import { expect, test } from 'vitest'
+
+// I am comment1
+// I am comment2
+test('should fail', () => {
+ // eslint-disable-next-line no-console
+ console.log('json-fail>should fail')
+ expect(2).toEqual(1)
+})
+",
+ },
"unhandledErrors": [],
}
`;
@@ -125,6 +141,10 @@ exports[`html reporter > resolves to "passing" status for test file "all-passing
{
"file": [Circular],
"id": 0,
+ "location": {
+ "column": 1,
+ "line": 3,
+ },
"meta": {},
"mode": "run",
"name": "2 + 3 = 5",
@@ -144,6 +164,10 @@ exports[`html reporter > resolves to "passing" status for test file "all-passing
{
"file": [Circular],
"id": "1111755131_1",
+ "location": {
+ "column": 6,
+ "line": 7,
+ },
"meta": {},
"mode": "skip",
"name": "3 + 3 = 6",
@@ -169,6 +193,18 @@ exports[`html reporter > resolves to "passing" status for test file "all-passing
"paths": [
"/test/reporters/fixtures/all-passing-or-skipped.test.ts",
],
+ "sources": {
+ "/test/reporters/fixtures/all-passing-or-skipped.test.ts": "import { expect, test } from 'vitest'
+
+test('2 + 3 = 5', () => {
+ expect(2 + 3).toBe(5)
+})
+
+test.skip('3 + 3 = 6', () => {
+ expect(3 + 3).toBe(6)
+})
+",
+ },
"unhandledErrors": [],
}
`;