Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit c69be1f

Browse filesBrowse files
authored
feat(ui): show test annotations and metadata in the test report tab (#8093)
1 parent 0f33506 commit c69be1f
Copy full SHA for c69be1f
Expand file treeCollapse file tree

21 files changed

+649
-184
lines changed

‎packages/ui/client/auto-imports.d.ts

Copy file name to clipboardExpand all lines: packages/ui/client/auto-imports.d.ts
+11Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ declare global {
1212
const autoResetRef: typeof import('@vueuse/core')['autoResetRef']
1313
const calcExternalLabels: typeof import('./composables/module-graph')['calcExternalLabels']
1414
const codemirrorRef: typeof import('./composables/codemirror')['codemirrorRef']
15+
const columnNumber: typeof import('./composables/params')['columnNumber']
1516
const computed: typeof import('vue')['computed']
1617
const computedAsync: typeof import('@vueuse/core')['computedAsync']
1718
const computedEager: typeof import('@vueuse/core')['computedEager']
@@ -49,6 +50,7 @@ declare global {
4950
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
5051
const effectScope: typeof import('vue')['effectScope']
5152
const extendRef: typeof import('@vueuse/core')['extendRef']
53+
const getAttachmentUrl: typeof import('./composables/attachments')['getAttachmentUrl']
5254
const getCurrentBrowserIframe: typeof import('./composables/api')['getCurrentBrowserIframe']
5355
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
5456
const getCurrentScope: typeof import('vue')['getCurrentScope']
@@ -61,13 +63,16 @@ declare global {
6163
const injectLocal: typeof import('@vueuse/core')['injectLocal']
6264
const isDark: typeof import('./composables/dark')['isDark']
6365
const isDefined: typeof import('@vueuse/core')['isDefined']
66+
const isExternalAttachment: typeof import('./composables/attachments')['isExternalAttachment']
6467
const isProxy: typeof import('vue')['isProxy']
6568
const isReactive: typeof import('vue')['isReactive']
6669
const isReadonly: typeof import('vue')['isReadonly']
6770
const isRef: typeof import('vue')['isRef']
71+
const isTestFile: typeof import('./composables/error')['isTestFile']
6872
const lineNumber: typeof import('./composables/params')['lineNumber']
6973
const mainSizes: typeof import('./composables/navigation')['mainSizes']
7074
const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable']
75+
const mapLeveledTaskStacks: typeof import('./composables/error')['mapLeveledTaskStacks']
7176
const markRaw: typeof import('vue')['markRaw']
7277
const navigateTo: typeof import('./composables/navigation')['navigateTo']
7378
const nextTick: typeof import('vue')['nextTick']
@@ -94,6 +99,7 @@ declare global {
9499
const onUpdated: typeof import('vue')['onUpdated']
95100
const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
96101
const openInEditor: typeof import('./composables/error')['openInEditor']
102+
const openScreenshot: typeof import('./composables/screenshot')['openScreenshot']
97103
const openedTreeItems: typeof import('./composables/navigation')['openedTreeItems']
98104
const panels: typeof import('./composables/navigation')['panels']
99105
const params: typeof import('./composables/params')['params']
@@ -120,19 +126,23 @@ declare global {
120126
const resolveComponent: typeof import('vue')['resolveComponent']
121127
const resolveRef: typeof import('@vueuse/core')['resolveRef']
122128
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
129+
const sanitizeFilePath: typeof import('./composables/attachments')['sanitizeFilePath']
123130
const selectedTest: typeof import('./composables/params')['selectedTest']
124131
const setIframeViewport: typeof import('./composables/api')['setIframeViewport']
125132
const shallowReactive: typeof import('vue')['shallowReactive']
126133
const shallowReadonly: typeof import('vue')['shallowReadonly']
127134
const shallowRef: typeof import('vue')['shallowRef']
128135
const shouldOpenInEditor: typeof import('./composables/error')['shouldOpenInEditor']
136+
const showAnnotationSource: typeof import('./composables/codemirror')['showAnnotationSource']
129137
const showCoverage: typeof import('./composables/navigation')['showCoverage']
130138
const showDashboard: typeof import('./composables/navigation')['showDashboard']
131139
const showLine: typeof import('./composables/codemirror')['showLine']
140+
const showLocationSource: typeof import('./composables/codemirror')['showLocationSource']
132141
const showNavigationPanel: typeof import('./composables/navigation')['showNavigationPanel']
133142
const showReport: typeof import('./composables/navigation')['showReport']
134143
const showRightPanel: typeof import('./composables/navigation')['showRightPanel']
135144
const showSource: typeof import('./composables/codemirror')['showSource']
145+
const showTaskSource: typeof import('./composables/codemirror')['showTaskSource']
136146
const syncRef: typeof import('@vueuse/core')['syncRef']
137147
const syncRefs: typeof import('@vueuse/core')['syncRefs']
138148
const templateRef: typeof import('@vueuse/core')['templateRef']
@@ -279,6 +289,7 @@ declare global {
279289
const useSSRWidth: typeof import('@vueuse/core')['useSSRWidth']
280290
const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation']
281291
const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea']
292+
const useScreenshot: typeof import('./composables/screenshot')['useScreenshot']
282293
const useScriptTag: typeof import('@vueuse/core')['useScriptTag']
283294
const useScroll: typeof import('@vueuse/core')['useScroll']
284295
const useScrollLock: typeof import('@vueuse/core')['useScrollLock']

‎packages/ui/client/components.d.ts

Copy file name to clipboardExpand all lines: packages/ui/client/components.d.ts
+2Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export {}
77
/* prettier-ignore */
88
declare module 'vue' {
99
export interface GlobalComponents {
10+
AnnotationAttachmentImage: typeof import('./components/AnnotationAttachmentImage.vue')['default']
1011
BrowserIframe: typeof import('./components/BrowserIframe.vue')['default']
1112
CodeMirrorContainer: typeof import('./components/CodeMirrorContainer.vue')['default']
1213
ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default']
@@ -38,5 +39,6 @@ declare module 'vue' {
3839
ViewModuleGraph: typeof import('./components/views/ViewModuleGraph.vue')['default']
3940
ViewReport: typeof import('./components/views/ViewReport.vue')['default']
4041
ViewReportError: typeof import('./components/views/ViewReportError.vue')['default']
42+
ViewTestReport: typeof import('./components/views/ViewTestReport.vue')['default']
4143
}
4244
}
+34Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script setup lang="ts">
2+
import type { TestAnnotation } from 'vitest'
3+
import { getAttachmentUrl, isExternalAttachment } from '~/composables/attachments'
4+
5+
const props = defineProps<{
6+
annotation: TestAnnotation
7+
}>()
8+
9+
const href = computed<string>(() => {
10+
const attachment = props.annotation.attachment!
11+
const potentialUrl = attachment.path || attachment.body
12+
if (typeof potentialUrl === 'string' && (potentialUrl.startsWith('http://') || potentialUrl.startsWith('https://'))) {
13+
return potentialUrl
14+
}
15+
else {
16+
return getAttachmentUrl(attachment)
17+
}
18+
})
19+
</script>
20+
21+
<template>
22+
<a
23+
v-if="annotation.attachment && annotation.attachment.contentType?.startsWith('image/')"
24+
target="_blank"
25+
class="inline-block mt-2"
26+
:style="{ maxWidth: '600px' }"
27+
:href="href"
28+
:referrerPolicy="isExternalAttachment(annotation.attachment) ? 'no-referrer' : undefined"
29+
>
30+
<img
31+
:src="href"
32+
>
33+
</a>
34+
</template>

‎packages/ui/client/components/FileDetails.vue

Copy file name to clipboardExpand all lines: packages/ui/client/components/FileDetails.vue
+29-12Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<script setup lang="ts">
2+
import type { RunnerTask, RunnerTestCase } from 'vitest'
23
import type { ModuleGraph } from '~/composables/module-graph'
34
import type { Params } from '~/composables/params'
45
import { toJSON } from 'flatted'
@@ -9,10 +10,11 @@ import {
910
currentLogs,
1011
isReport,
1112
} from '~/composables/client'
13+
import { explorerTree } from '~/composables/explorer'
1214
import { hasFailedSnapshot } from '~/composables/explorer/collector'
1315
import { getModuleGraph } from '~/composables/module-graph'
1416
import { viewMode } from '~/composables/params'
15-
import { getProjectNameColor } from '~/utils/task'
17+
import { getProjectNameColor, getProjectTextColor } from '~/utils/task'
1618
1719
const graph = ref<ModuleGraph>({ nodes: [], links: [] })
1820
const draft = ref(false)
@@ -21,6 +23,12 @@ const loadingModuleGraph = ref(false)
2123
const currentFilepath = ref<string | undefined>(undefined)
2224
const hideNodeModules = ref(true)
2325
26+
const test = computed(() => {
27+
return selectedTest.value
28+
? client.state.idMap.get(selectedTest.value) as RunnerTestCase
29+
: undefined
30+
})
31+
2432
const graphData = computed(() => {
2533
const c = current.value
2634
if (!c || !c.filepath) {
@@ -137,18 +145,26 @@ debouncedWatch(
137145
)
138146
139147
const projectNameColor = computed(() => {
140-
return getProjectNameColor(current.value?.file.projectName)
148+
const projectName = current.value?.file.projectName || ''
149+
return explorerTree.colors.get(projectName) || getProjectNameColor(current.value?.file.projectName)
141150
})
142151
143-
const projectNameTextColor = computed(() => {
144-
switch (projectNameColor.value) {
145-
case 'blue':
146-
case 'green':
147-
case 'magenta':
148-
return 'white'
149-
default:
150-
return 'black'
152+
const projectNameTextColor = computed(() => getProjectTextColor(projectNameColor.value))
153+
154+
const testTitle = computed(() => {
155+
const testId = selectedTest.value
156+
if (!testId) {
157+
return current.value?.name
158+
}
159+
const names: string[] = []
160+
let node: RunnerTask | undefined = client.state.idMap.get(testId)
161+
while (node) {
162+
names.push(node.name)
163+
node = node.suite
164+
? node.suite
165+
: (node === node.file ? undefined : node.file)
151166
}
167+
return names.reverse().join(' > ')
152168
})
153169
</script>
154170

@@ -174,7 +190,7 @@ const projectNameTextColor = computed(() => {
174190
{{ current.file.projectName }}
175191
</span>
176192
<div flex-1 font-light op-50 ws-nowrap truncate text-sm>
177-
{{ current?.name }}
193+
{{ testTitle }}
178194
</div>
179195
<div class="flex text-lg">
180196
<IconButton
@@ -263,7 +279,8 @@ const projectNameTextColor = computed(() => {
263279
:file="current"
264280
data-testid="console"
265281
/>
266-
<ViewReport v-else-if="!viewMode" :file="current" data-testid="report" />
282+
<ViewReport v-else-if="!viewMode && !test && current" :file="current" data-testid="report" />
283+
<ViewTestReport v-else-if="!viewMode && test" :test="test" data-testid="report" />
267284
</div>
268285
</div>
269286
</template>

‎packages/ui/client/components/explorer/ExplorerItem.vue

Copy file name to clipboardExpand all lines: packages/ui/client/components/explorer/ExplorerItem.vue
+5-19Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import type { TaskTreeNodeType } from '~/composables/explorer/types'
44
import { Tooltip as VueTooltip } from 'floating-vue'
55
import { nextTick } from 'vue'
66
import { client, isReport, runFiles, runTask } from '~/composables/client'
7-
import { showSource } from '~/composables/codemirror'
7+
import { showTaskSource } from '~/composables/codemirror'
88
import { explorerTree } from '~/composables/explorer'
99
import { hasFailedSnapshot } from '~/composables/explorer/collector'
1010
import { escapeHtml, highlightRegex } from '~/composables/explorer/state'
1111
import { coverageEnabled } from '~/composables/navigation'
12+
import { getProjectTextColor } from '~/utils/task'
1213
1314
// TODO: better handling of "opened" - it means to forcefully open the tree item and set in TasksList right now
1415
const {
@@ -149,26 +150,11 @@ function showDetails() {
149150
onItemClick?.(t)
150151
}
151152
else {
152-
showSource(t)
153+
showTaskSource(t)
153154
}
154155
}
155156
156-
const projectNameTextColor = computed(() => {
157-
switch (projectNameColor) {
158-
case 'blue':
159-
case 'green':
160-
case 'magenta':
161-
case 'black':
162-
case 'red':
163-
return 'white'
164-
165-
case 'yellow':
166-
case 'cyan':
167-
case 'white':
168-
default:
169-
return 'black'
170-
}
171-
})
157+
const projectNameTextColor = computed(() => getProjectTextColor(projectNameColor))
172158
</script>
173159

174160
<template>
@@ -223,7 +209,7 @@ const projectNameTextColor = computed(() => {
223209
>
224210
<IconButton
225211
data-testid="btn-open-details"
226-
icon="i-carbon:intrusion-prevention"
212+
:icon="type === 'file' ? 'i-carbon:intrusion-prevention' : 'i-carbon:code-reference'"
227213
@click.prevent.stop="showDetails"
228214
/>
229215
<template #popper>

‎packages/ui/client/components/views/ViewEditor.vue

Copy file name to clipboardExpand all lines: packages/ui/client/components/views/ViewEditor.vue
+9-28Lines changed: 9 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
<script setup lang="ts">
2-
import type { Task, TestAttachment } from '@vitest/runner'
2+
import type { Task } from '@vitest/runner'
33
import type CodeMirror from 'codemirror'
44
import type { ErrorWithDiff, File, TestAnnotation, TestError } from 'vitest'
55
import { createTooltip, destroyTooltip } from 'floating-vue'
6+
import { getAttachmentUrl, sanitizeFilePath } from '~/composables/attachments'
67
import { client, isReport } from '~/composables/client'
78
import { finished } from '~/composables/client/state'
89
import { codemirrorRef } from '~/composables/codemirror'
910
import { openInEditor } from '~/composables/error'
10-
import { lineNumber } from '~/composables/params'
11+
import { columnNumber, lineNumber } from '~/composables/params'
1112
1213
const props = defineProps<{
1314
file?: File
@@ -56,12 +57,12 @@ watch(
5657
{ immediate: true },
5758
)
5859
59-
watch(() => [loading.value, saving.value, props.file, lineNumber.value] as const, ([loadingFile, s, _, l]) => {
60+
watch(() => [loading.value, saving.value, props.file, lineNumber.value, columnNumber.value] as const, ([loadingFile, s, _, l, c]) => {
6061
if (!loadingFile && !s) {
6162
if (l != null) {
6263
nextTick(() => {
6364
const cp = currentPosition.value
64-
const line = cp ?? { line: l ?? 0, ch: 0 }
65+
const line = cp ?? { line: (l ?? 1) - 1, ch: c ?? 0 }
6566
// restore caret position: the watchDebounced below will use old value
6667
if (cp) {
6768
currentPosition.value = undefined
@@ -155,7 +156,7 @@ function createErrorElement(e: ErrorWithDiff) {
155156
div.className = 'op80 flex gap-x-2 items-center'
156157
const pre = document.createElement('pre')
157158
pre.className = 'c-red-600 dark:c-red-400'
158-
pre.textContent = `${' '.repeat(stack.column)}^ ${e?.nameStr || e.name}: ${
159+
pre.textContent = `${' '.repeat(stack.column)}^ ${e.name}: ${
159160
e?.message || ''
160161
}`
161162
div.appendChild(pre)
@@ -184,7 +185,6 @@ function createErrorElement(e: ErrorWithDiff) {
184185
185186
function createAnnotationElement(annotation: TestAnnotation) {
186187
if (!annotation.location) {
187-
// TODO(v4): print unknown annotations somewhere
188188
return
189189
}
190190
@@ -222,9 +222,8 @@ function createAnnotationElement(annotation: TestAnnotation) {
222222
if (attachment.contentType?.startsWith('image/')) {
223223
const link = document.createElement('a')
224224
const img = document.createElement('img')
225-
img.classList.add('mt-3', 'inline-block')
226-
img.width = 600
227-
img.width = 400
225+
link.classList.add('inline-block', 'mt-3')
226+
link.style.maxWidth = '50vw'
228227
const potentialUrl = attachment.path || attachment.body
229228
if (typeof potentialUrl === 'string' && (potentialUrl.startsWith('http://') || potentialUrl.startsWith('https://'))) {
230229
img.setAttribute('src', potentialUrl)
@@ -241,7 +240,7 @@ function createAnnotationElement(annotation: TestAnnotation) {
241240
else {
242241
const download = document.createElement('a')
243242
download.href = getAttachmentUrl(attachment)
244-
download.download = sanitizeFilePath(annotation.message)
243+
download.download = sanitizeFilePath(annotation.message, attachment.contentType)
245244
download.classList.add('flex', 'w-min', 'gap-2', 'items-center', 'font-sans', 'underline', 'cursor-pointer')
246245
const icon = document.createElement('div')
247246
icon.classList.add('i-carbon:download', 'block')
@@ -254,19 +253,6 @@ function createAnnotationElement(annotation: TestAnnotation) {
254253
widgets.push(codemirrorRef.value!.addLineWidget(line - 1, notice))
255254
}
256255
257-
function getAttachmentUrl(attachment: TestAttachment) {
258-
// html reporter always saves files into /data/ folder
259-
if (isReport) {
260-
return `/data/${attachment.path}`
261-
}
262-
const contentType = attachment.contentType ?? 'application/octet-stream'
263-
if (attachment.path) {
264-
return `/__vitest_attachment__?path=${encodeURIComponent(attachment.path)}&contentType=${contentType}&token=${(window as any).VITEST_API_TOKEN}`
265-
}
266-
// attachment.body is always a string outside of the test frame
267-
return `data:${contentType};base64,${attachment.body}`
268-
}
269-
270256
const { pause, resume } = watch(
271257
[codemirrorRef, errors, annotations, finished] as const,
272258
([cmValue, errors, annotations, end]) => {
@@ -389,11 +375,6 @@ async function onSave(content: string) {
389375
390376
// we need to remove listeners before unmounting the component: the watcher will not be called
391377
onBeforeUnmount(clearListeners)
392-
393-
function sanitizeFilePath(s: string): string {
394-
// eslint-disable-next-line no-control-regex
395-
return s.replace(/[\x00-\x2C\x2E\x2F\x3A-\x40\x5B-\x60\x7B-\x7F]+/g, '-')
396-
}
397378
</script>
398379

399380
<template>

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.