Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: html report #2444

Merged
merged 56 commits into from Dec 19, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ce4adb5
feat: vitest html report
poyoho Dec 6, 2022
d85c92c
chore: copy ui to vitest dist
poyoho Dec 6, 2022
a659179
feat: report copy ui dist
poyoho Dec 6, 2022
8c32d50
feat: ui report version builder
poyoho Dec 6, 2022
9208d4c
fix: copy file
poyoho Dec 6, 2022
758055c
chore: remove
poyoho Dec 6, 2022
3d77273
feat: html metadata path
poyoho Dec 6, 2022
2ddea5c
feat: add declare
poyoho Dec 6, 2022
7d374c9
feat: static file client
poyoho Dec 6, 2022
36bd86d
feat: mock rpc
poyoho Dec 6, 2022
cb29f14
fix: mock error
poyoho Dec 6, 2022
6b5bb57
chore: update meta
poyoho Dec 6, 2022
8c15597
chore: compress json
poyoho Dec 6, 2022
da5d3f2
chore: comment
poyoho Dec 7, 2022
9d4c650
chore: update merge config
poyoho Dec 7, 2022
35748fd
chore: update
poyoho Dec 7, 2022
d8d5680
chore: lock
poyoho Dec 7, 2022
b9511ca
feat: remove all control command from ui
poyoho Dec 7, 2022
936d109
chore: remove
poyoho Dec 7, 2022
a3baa19
feat: report command
poyoho Dec 7, 2022
bc7c2e9
feat: version tag
poyoho Dec 7, 2022
185db64
chore: reset lock file
poyoho Dec 7, 2022
dfa6073
feat: remove hooks
poyoho Dec 7, 2022
da71d8f
chore: update
poyoho Dec 7, 2022
9865a46
fix: runningPromise
poyoho Dec 7, 2022
6315cc1
Merge branch 'main' into html-report
poyoho Dec 7, 2022
7a3a382
chore: update debug mode and disable click on transform
poyoho Dec 7, 2022
5bfb5d6
docs: report
poyoho Dec 8, 2022
8f5bcc4
chore: remove the version mark
poyoho Dec 8, 2022
3a7b970
Merge branch 'main' into html-report
sheremet-va Dec 13, 2022
86bd94a
feat: report
poyoho Dec 13, 2022
5047e4b
fix: lint
poyoho Dec 13, 2022
9da35db
chore: copy ui from @vitest/ui
poyoho Dec 13, 2022
5969958
chore: export report from ui
poyoho Dec 13, 2022
dc8e406
chore: update
poyoho Dec 13, 2022
6c42ad7
fix: lint
poyoho Dec 13, 2022
d3f43da
docs: ui html report
poyoho Dec 13, 2022
ff951da
feat: ensurePackageInstalled
poyoho Dec 14, 2022
b807e7d
update
poyoho Dec 14, 2022
56a4285
feat: more info
poyoho Dec 14, 2022
da3263f
Merge branch 'main' into html-report
sheremet-va Dec 16, 2022
082f2b1
chore: improve documentation and tests
sheremet-va Dec 16, 2022
c94b627
chore: fix UI bundle size
sheremet-va Dec 16, 2022
78f344d
chore: ui tests
sheremet-va Dec 16, 2022
3bac646
perf: remove output report using the global variable to judge report …
poyoho Dec 17, 2022
966d59b
chore: update
poyoho Dec 17, 2022
e0c346d
fix: build
poyoho Dec 17, 2022
d941bf7
fix: report
poyoho Dec 17, 2022
8e41f66
fix: parse
poyoho Dec 17, 2022
0fcd30d
chore: fix html reporters test
sheremet-va Dec 17, 2022
6561232
chore: don't store config in html reporter test
sheremet-va Dec 17, 2022
9f6d669
chore: update ui docs
sheremet-va Dec 17, 2022
9e34e9a
feat: log
poyoho Dec 17, 2022
e03b564
chore: fix tests
sheremet-va Dec 17, 2022
1eb857f
test: fix html reporter tests
sheremet-va Dec 17, 2022
c1e04c2
docs add vitest fo UI reporter
sheremet-va Dec 19, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/config/index.md
Expand Up @@ -341,6 +341,7 @@ Custom reporters for output. Reporters can be [a Reporter instance](https://gith
- `'dot'` - show each task as a single dot
- `'junit'` - JUnit XML reporter (you can configure `testsuites` tag name with `VITEST_JUNIT_SUITE_NAME` environmental variable)
- `'json'` - give a simple JSON summary
- `'html'` - outputs HTML report based on [`@vitest/ui`](/guide/ui)
- path of a custom reporter (e.g. `'./path/to/reporter.ts'`, `'@scope/reporter'`)

### outputTruncateLength
Expand Down
26 changes: 26 additions & 0 deletions docs/guide/ui.md
Expand Up @@ -21,3 +21,29 @@ Then you can visit the Vitest UI at <a href="http://localhost:51204/__vitest__/"

<img alt="Vitest UI" img-light src="https://user-images.githubusercontent.com/11247099/171992267-5cae2fa0-b927-400a-8eb1-da776974cb61.png">
<img alt="Vitest UI" img-dark src="https://user-images.githubusercontent.com/11247099/171992272-7c6057e2-80c3-4b17-a7b6-0ac28e5a5e0b.png">

Since Vitest 0.26.0, UI can also be used as a reporter. Use `'html'` reporter in your Vitest configuration to generate HTML output and preview results of your tests:

```ts
// vitest.config.ts

export default {
test: {
reporters: ['html']
}
}
```

::: warning
If you still want to see how your tests are running in real time in the terminal, don't forget to add `default` reporter to `reporters` option: `['default', 'html']`.
:::

::: tip
To preview your HTML report, you can use [vite preview](https://vitejs.dev/guide/cli.html#vite-preview) command:

```sh
npx vite preview --base __vitest__ --outDir ./html
```

You can configure output with [`outputFile`](/config/#outputfile) config option. You need to specify `.html` path there. For example, `./html/index.html` is the default value.
:::
4 changes: 3 additions & 1 deletion packages/ui/client/components/FileDetails.vue
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { client, current, currentLogs } from '~/composables/client'
import { client, current, currentLogs, isReport } from '~/composables/client'
import type { Params } from '~/composables/params'
import { viewMode } from '~/composables/params'
import type { ModuleGraph } from '~/composables/module-graph'
Expand Down Expand Up @@ -52,6 +52,7 @@ function onDraft(value: boolean) {
</div>
<div class="flex text-lg">
<IconButton
v-if="!isReport"
v-tooltip.bottom="'Open in editor'"
title="Open in editor"
icon="i-carbon-launch"
Expand All @@ -76,6 +77,7 @@ function onDraft(value: boolean) {
Module Graph
</button>
<button
v-if="!isReport"
tab-button
:class="{ 'tab-button-active': viewMode === 'editor' }"
@click="changeViewMode('editor')"
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/client/components/Navigation.vue
Expand Up @@ -4,7 +4,7 @@ import { currentModule, dashboardVisible, showDashboard } from '../composables/n
import { client, findById } from '../composables/client'
import type { Task } from '#types'
import { isDark, toggleDark } from '~/composables'
import { files, runAll } from '~/composables/client'
import { files, isReport, runAll } from '~/composables/client'
import { activeFileId } from '~/composables/params'

const failedSnapshot = computed(() => files.value && hasFailedSnapshot(files.value))
Expand Down Expand Up @@ -34,12 +34,13 @@ const toggleMode = computed(() => isDark.value ? 'light' : 'dark')
@click="showDashboard(true)"
/>
<IconButton
v-if="failedSnapshot"
v-if="(failedSnapshot && !isReport)"
v-tooltip.bottom="'Update all failed snapshot(s)'"
icon="i-carbon-result-old"
@click="updateSnapshot()"
/>
<IconButton
v-if="!isReport"
v-tooltip.bottom="filteredTests ? (filteredTests.length === 0 ? 'No test to run (clear filter)' : 'Rerun filtered') : 'Rerun all'"
:disabled="filteredTests?.length === 0"
icon="i-carbon-play"
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/client/components/Suites.vue
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { hasFailedSnapshot } from '@vitest/ws-client'
import { client, current, runCurrent } from '~/composables/client'
import { client, current, isReport, runCurrent } from '~/composables/client'

const name = computed(() => current.value?.name.split(/\//g).pop())

Expand All @@ -16,12 +16,13 @@ const updateSnapshot = () => current.value && client.rpc.updateSnapshot(current.
<span font-bold text-sm flex-auto ws-nowrap overflow-hidden truncate>{{ name }}</span>
<div class="flex text-lg">
<IconButton
v-if="failedSnapshot"
v-if="(failedSnapshot && !isReport)"
v-tooltip.bottom="`Update failed snapshot(s) of ${current.name}`"
icon="i-carbon-result-old"
@click="updateSnapshot()"
/>
<IconButton
v-if="!isReport"
v-tooltip.bottom="'Rerun file'"
icon="i-carbon-play"
@click="runCurrent()"
Expand Down
3 changes: 3 additions & 0 deletions packages/ui/client/components/views/ViewModuleGraph.vue
Expand Up @@ -2,6 +2,7 @@
import type { ResizeContext } from 'd3-graph-controller'
import { GraphController, Markers, PositionInitializers, defineGraphConfig } from 'd3-graph-controller'
import type { Selection } from 'd3-selection'
import { isReport } from '~/composables/client'
import type { ModuleGraph, ModuleGraphController, ModuleLink, ModuleNode, ModuleType } from '~/composables/module-graph'

const props = defineProps<{
Expand Down Expand Up @@ -87,6 +88,8 @@ function resetGraphController() {
}

function bindOnClick(selection: Selection<SVGCircleElement, ModuleNode, SVGGElement, undefined>) {
if (isReport)
return
// Only trigger on left-click and primary touch
const isValidClick = (event: PointerEvent) => event.button === 0

Expand Down
Expand Up @@ -2,27 +2,35 @@ import { createClient, getTasks } from '@vitest/ws-client'
import type { WebSocketStatus } from '@vueuse/core'
import type { Ref } from 'vue'
import { reactive } from 'vue'
import type { RunState } from '../../types'
import { activeFileId } from './params'
import type { RunState } from '../../../types'
import { activeFileId } from '../params'
import { createStaticClient } from './static'
import type { File, ResolvedConfig } from '#types'

export const PORT = import.meta.hot ? '51204' : location.port
export const HOST = [location.hostname, PORT].filter(Boolean).join(':')
export const ENTRY_URL = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${HOST}/__vitest_api__`

export const isReport = !!window.METADATA_PATH
export const testRunState: Ref<RunState> = ref('idle')

export const client = createClient(ENTRY_URL, {
reactive: reactive as any,
handlers: {
onTaskUpdate() {
testRunState.value = 'running'
},
onFinished() {
testRunState.value = 'idle'
},
},
})
export const client = (function createVitestClient() {
if (isReport) {
return createStaticClient()
}
else {
return createClient(ENTRY_URL, {
reactive: reactive as any,
handlers: {
onTaskUpdate() {
testRunState.value = 'running'
},
onFinished() {
testRunState.value = 'idle'
},
},
})
}
})()

export const config = shallowRef<ResolvedConfig>({} as any)
export const status = ref<WebSocketStatus>('CONNECTING')
Expand Down Expand Up @@ -60,11 +68,15 @@ watch(
(ws) => {
status.value = 'CONNECTING'

ws.addEventListener('open', () => {
ws.addEventListener('open', async () => {
status.value = 'OPEN'
client.state.filesMap.clear()
client.rpc.getFiles().then(files => client.state.collectFiles(files))
client.rpc.getConfig().then(_config => config.value = _config)
const [files, _config] = await Promise.all([
client.rpc.getFiles(),
client.rpc.getConfig(),
])
client.state.collectFiles(files)
config.value = _config
})

ws.addEventListener('close', () => {
Expand Down
84 changes: 84 additions & 0 deletions packages/ui/client/composables/client/static.ts
@@ -0,0 +1,84 @@
import type { BirpcReturn } from 'birpc'
import type { VitestClient } from '@vitest/ws-client'
import type { WebSocketHandlers } from 'vitest/src/api/types'
import { parse } from 'flatted'
import type { File, ModuleGraphData, ResolvedConfig } from 'vitest/src/types'
import { StateManager } from '../../../../vitest/src/node/state'

interface HTMLReportMetadata {
paths: string[]
files: File[]
config: ResolvedConfig
moduleGraph: Record<string, ModuleGraphData>
}

const noop: any = () => {}
const asyncNoop: any = () => Promise.resolve()

export function createStaticClient(): VitestClient {
const ctx = reactive({
state: new StateManager(),
waitForConnection,
reconnect,
ws: new EventTarget(),
}) as VitestClient

ctx.state.filesMap = reactive(ctx.state.filesMap)
ctx.state.idMap = reactive(ctx.state.idMap)

let metadata!: HTMLReportMetadata

const rpc = {
getFiles: () => {
return metadata.files
},
getPaths: async () => {
return metadata.paths
},
getConfig: () => {
return metadata.config
},
getModuleGraph: async (id) => {
return metadata.moduleGraph[id]
},
getTransformResult: async (id) => {
return {
code: id,
source: '',
}
},
readFile: async (id) => {
return Promise.resolve(id)
},
onWatcherStart: asyncNoop,
onFinished: asyncNoop,
onCollected: asyncNoop,
onTaskUpdate: noop,
writeFile: asyncNoop,
rerun: asyncNoop,
updateSnapshot: asyncNoop,
} as WebSocketHandlers

ctx.rpc = rpc as any as BirpcReturn<WebSocketHandlers>

let openPromise: Promise<void>

function reconnect() {
registerMetadata()
}

async function registerMetadata() {
const res = await fetch(window.METADATA_PATH!)
metadata = parse(await res.text()) as HTMLReportMetadata
const event = new Event('open')
ctx.ws.dispatchEvent(event)
}

registerMetadata()

function waitForConnection() {
return openPromise
}

return ctx
}
6 changes: 6 additions & 0 deletions packages/ui/client/shim.d.ts
Expand Up @@ -7,3 +7,9 @@ declare module '*.vue' {
const component: DefineComponent<{}, {}, any>
export default component
}

const __REPORT__: boolean

declare interface Window {
METADATA_PATH?: string
}
3 changes: 2 additions & 1 deletion packages/ui/index.html
Expand Up @@ -9,13 +9,14 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Readex+Pro:wght@300;400&display=swap" rel="stylesheet">
<script>
(function() {
(function () {
const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches
const setting = localStorage.getItem('vueuse-color-scheme') || 'auto'
if (setting === 'dark' || (prefersDark && setting !== 'light'))
document.documentElement.classList.toggle('dark', true)
})()
</script>
<!-- !LOAD_METADATA! -->
</head>
<body>
<div id="app"></div>
Expand Down
77 changes: 77 additions & 0 deletions packages/ui/node/reporter.ts
@@ -0,0 +1,77 @@
import { existsSync, promises as fs } from 'node:fs'
import { fileURLToPath } from 'node:url'
import { basename, dirname, relative, resolve } from 'pathe'
import c from 'picocolors'
import fg from 'fast-glob'
import { stringify } from 'flatted'
// eslint-disable-next-line no-restricted-imports
import type { File, ModuleGraphData, Reporter, ResolvedConfig, Vitest } from 'vitest'
import { getModuleGraph } from '../../vitest/src/utils/graph'
import { getOutputFile } from '../../vitest/src/utils/config-helpers'

interface HTMLReportData {
paths: string[]
files: File[]
config: ResolvedConfig
moduleGraph: Record<string, ModuleGraphData>
}

const distDir = resolve(fileURLToPath(import.meta.url), '../../dist')

export default class HTMLReporter implements Reporter {
start = 0
ctx!: Vitest
reportUIPath!: string

async onInit(ctx: Vitest) {
this.ctx = ctx
this.start = Date.now()
}

async onFinished() {
const result: HTMLReportData = {
paths: await this.ctx.state.getPaths(),
files: this.ctx.state.getFiles(),
config: this.ctx.config,
moduleGraph: {},
}
await Promise.all(
result.files.map(async (file) => {
result.moduleGraph[file.filepath] = await getModuleGraph(this.ctx, file.filepath)
}),
)
await this.writeReport(stringify(result))
}

async writeReport(report: string) {
const htmlFile = getOutputFile(this.ctx.config, 'html') || 'html/index.html'
const htmlFileName = basename(htmlFile)
const htmlDir = resolve(this.ctx.config.root, dirname(htmlFile))

const metaFile = resolve(htmlDir, 'html.meta.json')

if (!existsSync(htmlDir))
await fs.mkdir(resolve(htmlDir, 'assets'), { recursive: true })

await fs.writeFile(metaFile, report, 'utf-8')
const ui = resolve(distDir, 'client')
// copy ui
const files = fg.sync('**/*', { cwd: ui })
await Promise.all(files.map(async (f) => {
if (f === 'index.html') {
const html = await fs.readFile(resolve(ui, f), 'utf-8')
const filePath = relative(htmlDir, metaFile)
await fs.writeFile(
resolve(htmlDir, htmlFileName),
html.replace('<!-- !LOAD_METADATA! -->', `<script>window.METADATA_PATH="${filePath}"</script>`),
)
}
else {
await fs.copyFile(resolve(ui, f), resolve(htmlDir, f))
}
}))

this.ctx.logger.log(`${c.bold(c.inverse(c.magenta(' HTML ')))} ${c.magenta('Report is generated')}`)
this.ctx.logger.log(`${c.dim(' You can run ')}${c.bold(`npx vite preview --base __vitest__ --outDir ${relative(this.ctx.config.root, htmlDir)}`)}${c.dim(' to see the test results.')}`)
}
}