Skip to content

Commit

Permalink
feat: add browser frame to UI (#5808)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Jun 1, 2024
1 parent 7900f9f commit 3796dd7
Show file tree
Hide file tree
Showing 19 changed files with 252 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,19 @@
padding: 0;
margin: 0;
}
#vitest-ui {
width: 100vw;
height: 100vh;
border: none;
html,
body,
iframe[data-vitest],
#vitest-tester {
width: 100%;
height: 100%;
}
</style>
<script>{__VITEST_INJECTOR__}</script>
{__VITEST_SCRIPTS__}
</head>
<body>
{__VITEST_UI__}
<script type="module" src="/main.ts"></script>
<script type="module" src="/orchestrator.ts"></script>
<div id="vitest-tester"></div>
</body>
</html>
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import type { ResolvedConfig } from 'vitest'
import { generateHash } from '@vitest/runner/utils'
import { relative } from 'pathe'
import { channel, client } from './client'
import { rpcDone } from './rpc'
import { getBrowserState, getConfig } from './utils'
import { getUiAPI } from './ui'

const url = new URL(location.href)

Expand All @@ -23,6 +27,14 @@ function createIframe(container: HTMLDivElement, file: string) {
const iframe = document.createElement('iframe')
iframe.setAttribute('loading', 'eager')
iframe.setAttribute('src', `${url.pathname}__vitest_test__/__test__/${encodeURIComponent(file)}`)
iframe.setAttribute('data-vitest', 'true')

iframe.style.display = 'block'
iframe.style.border = 'none'
iframe.style.pointerEvents = 'none'
iframe.setAttribute('allowfullscreen', 'true')
iframe.setAttribute('allow', 'clipboard-write;')

iframes.set(file, iframe)
container.appendChild(iframe)
return iframe
Expand All @@ -47,9 +59,23 @@ interface IframeErrorEvent {

type IframeChannelEvent = IframeDoneEvent | IframeErrorEvent

async function getContainer(config: ResolvedConfig): Promise<HTMLDivElement> {
if (config.browser.ui) {
const element = document.querySelector('#tester-ui')
if (!element) {
return new Promise<HTMLDivElement>((resolve) => {
setTimeout(() => {
resolve(getContainer(config))
}, 30)
})
}
return element as HTMLDivElement
}
return document.querySelector('#vitest-tester') as HTMLDivElement
}

client.ws.addEventListener('open', async () => {
const config = getConfig()
const container = document.querySelector('#vitest-tester') as HTMLDivElement
const testFiles = getBrowserState().files

debug('test files', testFiles.join(', '))
Expand All @@ -60,6 +86,7 @@ client.ws.addEventListener('open', async () => {
return
}

const container = await getContainer(config)
const runningFiles = new Set<string>(testFiles)

channel.addEventListener('message', async (e: MessageEvent<IframeChannelEvent>): Promise<void> => {
Expand All @@ -70,6 +97,13 @@ client.ws.addEventListener('open', async () => {
filenames.forEach(filename => runningFiles.delete(filename))

if (!runningFiles.size) {
const ui = getUiAPI()
// in isolated mode we don't change UI because it will slow down tests,
// so we only select it when the run is done
if (ui && filenames.length > 1) {
const id = generateFileId(filenames[filenames.length - 1])
ui.setCurrentById(id)
}
await done()
}
else {
Expand Down Expand Up @@ -103,6 +137,11 @@ client.ws.addEventListener('open', async () => {
}
})

if (config.browser.ui) {
container.className = ''
container.textContent = ''
}

if (config.isolate === false) {
createIframe(
container,
Expand All @@ -113,6 +152,13 @@ client.ws.addEventListener('open', async () => {
// otherwise, we need to wait for each iframe to finish before creating the next one
// this is the most stable way to run tests in the browser
for (const file of testFiles) {
const ui = getUiAPI()

if (ui) {
const id = generateFileId(file)
ui.setCurrentById(id)
}

createIframe(
container,
file,
Expand All @@ -129,3 +175,10 @@ client.ws.addEventListener('open', async () => {
}
}
})

function generateFileId(file: string) {
const config = getConfig()
const project = config.name || ''
const path = relative(config.root, file)
return generateHash(`${path}${project}`)
}
3 changes: 2 additions & 1 deletion packages/browser/src/client/tester.html
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@
body {
padding: 0;
margin: 0;
min-height: 100vh;
}
</style>
<script>{__VITEST_INJECTOR__}</script>
{__VITEST_SCRIPTS__}
</head>
<body>
<body style="width: 100%; height: 100%; transform: scale(1); transform-origin: left top;">
<script type="module" src="/tester.ts"></script>
{__VITEST_APPEND__}
</body>
Expand Down
11 changes: 11 additions & 0 deletions packages/browser/src/client/ui.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { File } from '@vitest/runner'

interface UiAPI {
currentModule: File
setCurrentById: (fileId: string) => void
}

export function getUiAPI(): UiAPI | undefined {
// @ts-expect-error not typed global
return window.__vitest_ui_api__
}
3 changes: 2 additions & 1 deletion packages/browser/src/client/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ export default defineConfig({
outDir: '../../dist/client',
emptyOutDir: false,
assetsDir: '__vitest_browser__',
manifest: true,
rollupOptions: {
input: {
main: resolve(__dirname, './index.html'),
orchestrator: resolve(__dirname, './orchestrator.html'),
tester: resolve(__dirname, './tester.html'),
},
},
Expand Down
28 changes: 23 additions & 5 deletions packages/browser/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => {
},
async configureServer(server) {
const testerHtml = readFile(resolve(distRoot, 'client/tester.html'), 'utf8')
const runnerHtml = readFile(resolve(distRoot, 'client/index.html'), 'utf8')
const orchestratorHtml = project.config.browser.ui
? readFile(resolve(distRoot, 'client/__vitest__/index.html'), 'utf8')
: readFile(resolve(distRoot, 'client/orchestrator.html'), 'utf8')
const injectorJs = readFile(resolve(distRoot, 'client/esm-client-injector.js'), 'utf8')
const manifest = (async () => {
return JSON.parse(await readFile(`${distRoot}/client/.vite/manifest.json`, 'utf8'))
})()
const favicon = `${base}favicon.svg`
const testerPrefix = `${base}__vitest_test__/__test__/`
server.middlewares.use((_req, res, next) => {
Expand Down Expand Up @@ -71,14 +76,27 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => {
if (!indexScripts)
indexScripts = await formatScripts(project.config.browser.indexScripts, server)

const html = replacer(await runnerHtml, {
let baseHtml = await orchestratorHtml

// if UI is enabled, use UI HTML and inject the orchestrator script
if (project.config.browser.ui) {
const manifestContent = await manifest
const jsEntry = manifestContent['orchestrator.html'].file
baseHtml = baseHtml.replaceAll('./assets/', `${base}__vitest__/assets/`).replace(
'<!-- !LOAD_METADATA! -->',
[
'<script>{__VITEST_INJECTOR__}</script>',
'{__VITEST_SCRIPTS__}',
`<script type="module" crossorigin src="${jsEntry}"></script>`,
].join('\n'),
)
}

const html = replacer(baseHtml, {
__VITEST_FAVICON__: favicon,
__VITEST_TITLE__: 'Vitest Browser Runner',
__VITEST_SCRIPTS__: indexScripts,
__VITEST_INJECTOR__: injector,
__VITEST_UI__: project.config.browser.ui
? '<iframe id="vitest-ui" src="/__vitest__/"></iframe>'
: '',
})
res.write(html, 'utf-8')
res.end()
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 @@ -7,6 +7,7 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
BrowserIframe: typeof import('./components/BrowserIframe.vue')['default']
CodeMirror: typeof import('./components/CodeMirror.vue')['default']
ConnectionOverlay: typeof import('./components/ConnectionOverlay.vue')['default']
Coverage: typeof import('./components/Coverage.vue')['default']
Expand Down
92 changes: 92 additions & 0 deletions packages/ui/client/components/BrowserIframe.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<script setup lang="ts">
const viewport = ref('custom')
function changeViewport(name: string) {
if (viewport.value === name) {
viewport.value = 'custom'
} else {
viewport.value = name
}
}
</script>

<template>
<div h="full" flex="~ col">
<div
p="3"
h-10
flex="~ gap-2"
items-center
bg-header
border="b base"
>
<div class="i-carbon-content-delivery-network" />
<span
pl-1
font-bold
text-sm
flex-auto
ws-nowrap
overflow-hidden
truncate
>Browser UI</span>
</div>
<div
p="l3 y2 r2"
flex="~ gap-2"
items-center
bg-header
border="b-2 base"
>
<!-- TODO: these are only for preview (thank you Storybook!), we need to support more different and custom sizes (as a dropdown) -->
<IconButton
v-tooltip.bottom="'Small mobile'"
title="Small mobile"
icon="i-carbon:mobile"
:active="viewport === 'small-mobile'"
@click="changeViewport('small-mobile')"
/>
<IconButton
v-tooltip.bottom="'Large mobile'"
title="Large mobile"
icon="i-carbon:mobile-add"
:active="viewport === 'large-mobile'"
@click="changeViewport('large-mobile')"
/>
<IconButton
v-tooltip.bottom="'Tablet'"
title="Tablet"
icon="i-carbon:tablet"
:active="viewport === 'tablet'"
@click="changeViewport('tablet')"
/>
</div>
<div flex-auto overflow-auto>
<div id="tester-ui" class="flex h-full justify-center items-center font-light op70" style="overflow: auto; width: 100%; height: 100%" :data-viewport="viewport">
Select a test to run
</div>
</div>
</div>
</template>

<style>
[data-viewport="custom"] iframe {
width: 100%;
height: 100%;
}
[data-viewport="small-mobile"] iframe {
width: 320px;
height: 568px;
}
[data-viewport="large-mobile"] iframe {
width: 414px;
height: 896px;
}
[data-viewport="tablet"] iframe {
width: 834px;
height: 1112px;
}
</style>
4 changes: 2 additions & 2 deletions packages/ui/client/components/ConnectionOverlay.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { client, isConnected, isConnecting } from '~/composables/client'
import { client, isConnected, isConnecting, browserState } from '~/composables/client'
</script>

<template>
Expand Down Expand Up @@ -27,7 +27,7 @@ import { client, isConnected, isConnecting } from '~/composables/client'
{{ isConnecting ? 'Connecting...' : 'Disconnected' }}
</div>
<div text-lg op50>
Check your terminal or start a new server with `vitest --ui`
Check your terminal or start a new server with `{{ browserState ? `vitest --browser=${browserState.config.browser.name}` : 'vitest --ui' }}`
</div>
</div>
</div>
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/client/components/FileDetails.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import type { ModuleGraphData } from 'vitest'
import { client, current, currentLogs, isReport } from '~/composables/client'
import { client, current, currentLogs, isReport, browserState } from '~/composables/client'
import type { Params } from '~/composables/params'
import { viewMode } from '~/composables/params'
import type { ModuleGraph } from '~/composables/module-graph'
Expand All @@ -17,7 +17,7 @@ debouncedWatch(
async (c, o) => {
if (c && c.filepath !== o?.filepath) {
const project = c.file.projectName || ''
data.value = await client.rpc.getModuleGraph(project, c.filepath)
data.value = await client.rpc.getModuleGraph(project, c.filepath, !!browserState)
graph.value = getModuleGraph(data.value, c.filepath)
}
},
Expand Down Expand Up @@ -50,7 +50,7 @@ function onDraft(value: boolean) {
<div>
<div p="2" h-10 flex="~ gap-2" items-center bg-header border="b base">
<StatusIcon :task="current" />
<div font-light op-50 text-sm :style="{ color: getProjectNameColor(current?.file.projectName) }">
<div v-if="current?.file.projectName" font-light op-50 text-sm :style="{ color: getProjectNameColor(current?.file.projectName) }">
[{{ current?.file.projectName || '' }}]
</div>
<div flex-1 font-light op-50 ws-nowrap truncate text-sm>
Expand Down
6 changes: 3 additions & 3 deletions packages/ui/client/components/IconButton.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
defineProps<{ icon?: `i-${string}` | `dark:i-${string}`; title?: string; disabled?: boolean }>()
defineProps<{ icon?: `i-${string}` | `dark:i-${string}`; title?: string; disabled?: boolean; active?: boolean }>()
</script>

<template>
Expand All @@ -9,8 +9,8 @@ defineProps<{ icon?: `i-${string}` | `dark:i-${string}`; title?: string; disable
:opacity="disabled ? 10 : 70"
rounded
:disabled="disabled"
:hover="disabled ? '' : 'bg-active op100'"
class="w-1.4em h-1.4em flex"
:hover="disabled || active ? '' : 'bg-active op100'"
:class="['w-1.4em h-1.4em flex', { 'bg-gray-500:35 op100': active }]"
>
<slot>
<div :class="icon" ma />
Expand Down
Loading

0 comments on commit 3796dd7

Please sign in to comment.