diff --git a/packages/browser/src/node/plugin.ts b/packages/browser/src/node/plugin.ts index e4dc57e83cac..4d434c014a8e 100644 --- a/packages/browser/src/node/plugin.ts +++ b/packages/browser/src/node/plugin.ts @@ -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' @@ -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) + }) }, }, { diff --git a/packages/ui/client/components.d.ts b/packages/ui/client/components.d.ts index a7a4abf4326f..0a7fe8fa8abb 100644 --- a/packages/ui/client/components.d.ts +++ b/packages/ui/client/components.d.ts @@ -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'] diff --git a/packages/ui/client/components/views/ScreenshotError.vue b/packages/ui/client/components/views/ScreenshotError.vue new file mode 100644 index 000000000000..b9c8ca6ab569 --- /dev/null +++ b/packages/ui/client/components/views/ScreenshotError.vue @@ -0,0 +1,53 @@ + + + + + diff --git a/packages/ui/client/components/views/ViewReport.vue b/packages/ui/client/components/views/ViewReport.vue index f55508a2f0da..231b33c0b5e6 100644 --- a/packages/ui/client/components/views/ViewReport.vue +++ b/packages/ui/client/components/views/ViewReport.vue @@ -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<{ @@ -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() +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 +} +