Skip to content

Commit 2d62051

Browse files
authored
feat(browser): allow preview and open in the editor screenshot error from ui (#6113)
1 parent 3826941 commit 2d62051

File tree

4 files changed

+161
-4
lines changed

4 files changed

+161
-4
lines changed

packages/browser/src/node/plugin.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { fileURLToPath } from 'node:url'
22
import { createRequire } from 'node:module'
3-
import { readFileSync } from 'node:fs'
4-
import { basename, resolve } from 'pathe'
3+
import { lstatSync, readFileSync } from 'node:fs'
4+
import type { Stats } from 'node:fs'
5+
import { basename, extname, resolve } from 'pathe'
56
import sirv from 'sirv'
67
import type { WorkspaceProject } from 'vitest/node'
78
import { getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir as vitestDist } from 'vitest/node'
@@ -100,6 +101,52 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
100101
},
101102
}),
102103
)
104+
105+
const screenshotFailures = project.config.browser.ui && project.config.browser.screenshotFailures
106+
107+
// eslint-disable-next-line prefer-arrow-callback
108+
screenshotFailures && server.middlewares.use(`${base}__screenshot-error`, function vitestBrowserScreenshotError(req, res) {
109+
if (!req.url || !browserServer.provider) {
110+
res.statusCode = 404
111+
res.end()
112+
return
113+
}
114+
115+
const url = new URL(req.url, 'http://localhost')
116+
const file = url.searchParams.get('file')
117+
if (!file) {
118+
res.statusCode = 404
119+
res.end()
120+
return
121+
}
122+
123+
let stat: Stats | undefined
124+
try {
125+
stat = lstatSync(file)
126+
}
127+
catch (_) {
128+
}
129+
130+
if (!stat?.isFile()) {
131+
res.statusCode = 404
132+
res.end()
133+
return
134+
}
135+
136+
const ext = extname(file)
137+
const buffer = readFileSync(file)
138+
res.setHeader(
139+
'Cache-Control',
140+
'public,max-age=0,must-revalidate',
141+
)
142+
res.setHeader('Content-Length', buffer.length)
143+
res.setHeader('Content-Type', ext === 'jpeg' || ext === 'jpg'
144+
? 'image/jpeg'
145+
: ext === 'webp'
146+
? 'image/webp'
147+
: 'image/png')
148+
res.end(buffer)
149+
})
103150
},
104151
},
105152
{

packages/ui/client/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ declare module 'vue' {
2727
ProgressBar: typeof import('./components/ProgressBar.vue')['default']
2828
RouterLink: typeof import('vue-router')['RouterLink']
2929
RouterView: typeof import('vue-router')['RouterView']
30+
ScreenshotError: typeof import('./components/views/ScreenshotError.vue')['default']
3031
StatusIcon: typeof import('./components/StatusIcon.vue')['default']
3132
TestFilesEntry: typeof import('./components/dashboard/TestFilesEntry.vue')['default']
3233
TestsEntry: typeof import('./components/dashboard/TestsEntry.vue')['default']
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<script setup lang="ts">
2+
defineProps<{
3+
file: string
4+
name: string
5+
url?: string
6+
}>()
7+
const emit = defineEmits<{ (e: 'close'): void }>()
8+
9+
onKeyStroke('Escape', () => {
10+
emit('close')
11+
})
12+
</script>
13+
14+
<template>
15+
<div w-350 max-w-screen h-full flex flex-col>
16+
<div p-4 relative border="base b">
17+
<p>Screenshot error</p>
18+
<p op50 font-mono text-sm>
19+
{{ file }}
20+
</p>
21+
<p op50 font-mono text-sm>
22+
{{ name }}
23+
</p>
24+
<IconButton
25+
icon="i-carbon:close"
26+
title="Close"
27+
absolute
28+
top-5px
29+
right-5px
30+
text-2xl
31+
@click="emit('close')"
32+
/>
33+
</div>
34+
35+
<div class="scrolls" grid="~ cols-1 rows-[min-content]" p-4>
36+
<img
37+
v-if="url"
38+
:src="url"
39+
:alt="`Screenshot error for '${name}' test in file '${file}'`"
40+
border="base t r b l dotted red-500"
41+
>
42+
<div v-else>
43+
Something was wrong, the image cannot be resolved.
44+
</div>
45+
</div>
46+
</div>
47+
</template>
48+
49+
<style scoped>
50+
.scrolls {
51+
place-items: center;
52+
}
53+
</style>

packages/ui/client/components/views/ViewReport.vue

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ErrorWithDiff, File, Suite, Task } from 'vitest'
33
import type Convert from 'ansi-to-html'
44
import { isDark } from '~/composables/dark'
55
import { createAnsiToHtmlFilter } from '~/composables/error'
6-
import { config } from '~/composables/client'
6+
import { browserState, config } from '~/composables/client'
77
import { escapeHtml } from '~/utils/escape'
88
99
const props = defineProps<{
@@ -103,6 +103,30 @@ const failed = computed(() => {
103103
? mapLeveledTaskStacks(isDark.value, failedFlatMap)
104104
: failedFlatMap
105105
})
106+
107+
function open(task: Task) {
108+
const filePath = task.meta?.failScreenshotPath
109+
if (filePath) {
110+
fetch(`/__open-in-editor?file=${encodeURIComponent(filePath)}`)
111+
}
112+
}
113+
114+
const showScreenshot = ref(false)
115+
const timestamp = ref(Date.now())
116+
const currentTask = ref<Task | undefined>()
117+
const currentScreenshotUrl = computed(() => {
118+
const file = currentTask.value?.meta.failScreenshotPath
119+
// force refresh
120+
const t = timestamp.value
121+
// browser plugin using /, change this if base can be modified
122+
return file ? `/__screenshot-error?file=${encodeURIComponent(file)}&t=${t}` : undefined
123+
})
124+
125+
function showScreenshotModal(task: Task) {
126+
currentTask.value = task
127+
timestamp.value = Date.now()
128+
showScreenshot.value = true
129+
}
106130
</script>
107131

108132
<template>
@@ -121,7 +145,25 @@ const failed = computed(() => {
121145
}rem`,
122146
}"
123147
>
124-
{{ task.name }}
148+
<div flex="~ gap-2 items-center">
149+
<span>{{ task.name }}</span>
150+
<template v-if="browserState && task.meta?.failScreenshotPath">
151+
<IconButton
152+
v-tooltip.bottom="'View screenshot error'"
153+
class="!op-100"
154+
icon="i-carbon:image"
155+
title="View screenshot error"
156+
@click="showScreenshotModal(task)"
157+
/>
158+
<IconButton
159+
v-tooltip.bottom="'Open screenshot error in editor'"
160+
class="!op-100"
161+
icon="i-carbon:image-reference"
162+
title="Open screenshot error in editor"
163+
@click="open(task)"
164+
/>
165+
</template>
166+
</div>
125167
<div
126168
v-if="task.result?.htmlError"
127169
class="scrolls scrolls-rounded task-error"
@@ -146,6 +188,20 @@ const failed = computed(() => {
146188
All tests passed in this file
147189
</div>
148190
</template>
191+
<template v-if="browserState">
192+
<Modal v-model="showScreenshot" direction="right">
193+
<template v-if="currentTask">
194+
<Suspense>
195+
<ScreenshotError
196+
:file="currentTask.file.filepath"
197+
:name="currentTask.name"
198+
:url="currentScreenshotUrl"
199+
@close="showScreenshot = false"
200+
/>
201+
</Suspense>
202+
</template>
203+
</Modal>
204+
</template>
149205
</div>
150206
</template>
151207

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy