Skip to content
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
51 changes: 49 additions & 2 deletions packages/browser/src/node/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { fileURLToPath } from 'node:url'
import { createRequire } from 'node:module'
import { readFileSync } from 'node:fs'
import { basename, resolve } from 'pathe'
import { lstatSync, readFileSync } from 'node:fs'
import type { Stats } from 'node:fs'
import { basename, extname, resolve } from 'pathe'
import sirv from 'sirv'
import type { WorkspaceProject } from 'vitest/node'
import { getFilePoolName, resolveApiServerConfig, resolveFsAllow, distDir as vitestDist } from 'vitest/node'
Expand Down Expand Up @@ -100,6 +101,52 @@ export default (browserServer: BrowserServer, base = '/'): Plugin[] => {
},
}),
)

const screenshotFailures = project.config.browser.ui && project.config.browser.screenshotFailures

// eslint-disable-next-line prefer-arrow-callback
screenshotFailures && server.middlewares.use(`${base}__screenshot-error`, function vitestBrowserScreenshotError(req, res) {
if (!req.url || !browserServer.provider) {
res.statusCode = 404
res.end()
return
}

const url = new URL(req.url, 'http://localhost')
const file = url.searchParams.get('file')
if (!file) {
res.statusCode = 404
res.end()
return
}

let stat: Stats | undefined
try {
stat = lstatSync(file)
}
catch (_) {
}

if (!stat?.isFile()) {
res.statusCode = 404
res.end()
return
}

const ext = extname(file)
const buffer = readFileSync(file)
res.setHeader(
'Cache-Control',
'public,max-age=0,must-revalidate',
)
res.setHeader('Content-Length', buffer.length)
res.setHeader('Content-Type', ext === 'jpeg' || ext === 'jpg'
? 'image/jpeg'
: ext === 'webp'
? 'image/webp'
: 'image/png')
res.end(buffer)
})
},
},
{
Expand Down
1 change: 1 addition & 0 deletions packages/ui/client/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ declare module 'vue' {
ProgressBar: typeof import('./components/ProgressBar.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ScreenshotError: typeof import('./components/views/ScreenshotError.vue')['default']
StatusIcon: typeof import('./components/StatusIcon.vue')['default']
TestFilesEntry: typeof import('./components/dashboard/TestFilesEntry.vue')['default']
TestsEntry: typeof import('./components/dashboard/TestsEntry.vue')['default']
Expand Down
53 changes: 53 additions & 0 deletions packages/ui/client/components/views/ScreenshotError.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<script setup lang="ts">
defineProps<{
file: string
name: string
url?: string
}>()
const emit = defineEmits<{ (e: 'close'): void }>()
onKeyStroke('Escape', () => {
emit('close')
})
</script>

<template>
<div w-350 max-w-screen h-full flex flex-col>
<div p-4 relative border="base b">
<p>Screenshot error</p>
<p op50 font-mono text-sm>
{{ file }}
</p>
<p op50 font-mono text-sm>
{{ name }}
</p>
<IconButton
icon="i-carbon:close"
title="Close"
absolute
top-5px
right-5px
text-2xl
@click="emit('close')"
/>
</div>

<div class="scrolls" grid="~ cols-1 rows-[min-content]" p-4>
<img
v-if="url"
:src="url"
:alt="`Screenshot error for '${name}' test in file '${file}'`"
border="base t r b l dotted red-500"
>
<div v-else>
Something was wrong, the image cannot be resolved.
</div>
</div>
</div>
</template>

<style scoped>
.scrolls {
place-items: center;
}
</style>
60 changes: 58 additions & 2 deletions packages/ui/client/components/views/ViewReport.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ErrorWithDiff, File, Suite, Task } from 'vitest'
import type Convert from 'ansi-to-html'
import { isDark } from '~/composables/dark'
import { createAnsiToHtmlFilter } from '~/composables/error'
import { config } from '~/composables/client'
import { browserState, config } from '~/composables/client'
import { escapeHtml } from '~/utils/escape'

const props = defineProps<{
Expand Down Expand Up @@ -103,6 +103,30 @@ const failed = computed(() => {
? mapLeveledTaskStacks(isDark.value, failedFlatMap)
: failedFlatMap
})

function open(task: Task) {
const filePath = task.meta?.failScreenshotPath
if (filePath) {
fetch(`/__open-in-editor?file=${encodeURIComponent(filePath)}`)
}
}

const showScreenshot = ref(false)
const timestamp = ref(Date.now())
const currentTask = ref<Task | undefined>()
const currentScreenshotUrl = computed(() => {
const file = currentTask.value?.meta.failScreenshotPath
// force refresh
const t = timestamp.value
// browser plugin using /, change this if base can be modified
return file ? `/__screenshot-error?file=${encodeURIComponent(file)}&t=${t}` : undefined
})

function showScreenshotModal(task: Task) {
currentTask.value = task
timestamp.value = Date.now()
showScreenshot.value = true
}
</script>

<template>
Expand All @@ -121,7 +145,25 @@ const failed = computed(() => {
}rem`,
}"
>
{{ task.name }}
<div flex="~ gap-2 items-center">
<span>{{ task.name }}</span>
<template v-if="browserState && task.meta?.failScreenshotPath">
<IconButton
v-tooltip.bottom="'View screenshot error'"
class="!op-100"
icon="i-carbon:image"
title="View screenshot error"
@click="showScreenshotModal(task)"
/>
<IconButton
v-tooltip.bottom="'Open screenshot error in editor'"
class="!op-100"
icon="i-carbon:image-reference"
title="Open screenshot error in editor"
@click="open(task)"
/>
</template>
</div>
<div
v-if="task.result?.htmlError"
class="scrolls scrolls-rounded task-error"
Expand All @@ -146,6 +188,20 @@ const failed = computed(() => {
All tests passed in this file
</div>
</template>
<template v-if="browserState">
<Modal v-model="showScreenshot" direction="right">
<template v-if="currentTask">
<Suspense>
<ScreenshotError
:file="currentTask.file.filepath"
:name="currentTask.name"
:url="currentScreenshotUrl"
@close="showScreenshot = false"
/>
</Suspense>
</template>
</Modal>
</template>
</div>
</template>

Expand Down