Skip to content

Commit

Permalink
feat(browser): support changing the viewport (#5811)
Browse files Browse the repository at this point in the history
  • Loading branch information
sheremet-va committed Jun 1, 2024
1 parent 4bea1ca commit 718512d
Show file tree
Hide file tree
Showing 16 changed files with 178 additions and 51 deletions.
7 changes: 7 additions & 0 deletions docs/config/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -1609,6 +1609,13 @@ To have a better type safety when using built-in providers, you can add one of t
Should Vitest UI be injected into the page. By default, injects UI iframe during development.
#### browser.viewport {#browser-viewport}
- **Type:** `{ width, height }`
- **Default:** `414x896`
Default iframe's viewport.
#### browser.indexScripts {#browser-indexscripts}
- **Type:** `BrowserScript[]`
Expand Down
14 changes: 12 additions & 2 deletions docs/guide/browser.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,17 +135,23 @@ export const server: {
* The same as calling `process.version` on the server.
*/
version: string
/**
* Name of the browser provider.
*/
provider: string
/**
* Name of the current browser.
*/
browser: string
/**
* Available commands for the browser.
* @see {@link https://vitest.dev/guide/browser#commands}
*/
commands: BrowserCommands
}

/**
* Available commands for the browser.
* A shortcut to `server.commands`.
* @see {@link https://vitest.dev/guide/browser#commands}
*/
export const commands: BrowserCommands

Expand All @@ -154,6 +160,10 @@ export const page: {
* Serialized test config.
*/
config: ResolvedConfig
/**
* Change the size of iframe's viewport.
*/
viewport: (width: number | string, height: number | string) => Promise<void>
}
```
Expand Down
4 changes: 4 additions & 0 deletions packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,8 @@ export const page: {
* Serialized test config.
*/
config: ResolvedConfig
/**
* Change the size of iframe's viewport.
*/
viewport: (width: number | string, height: number | string) => Promise<void>
}
55 changes: 50 additions & 5 deletions packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ function createIframe(container: HTMLDivElement, file: string) {
iframe.setAttribute('src', `${url.pathname}__vitest_test__/__test__/${encodeURIComponent(file)}`)
iframe.setAttribute('data-vitest', 'true')

const config = getConfig().browser
iframe.style.width = `${config.viewport.width}px`
iframe.style.height = `${config.viewport.height}px`

iframe.style.display = 'block'
iframe.style.border = 'none'
iframe.style.pointerEvents = 'none'
Expand Down Expand Up @@ -66,7 +70,14 @@ interface IframeErrorEvent {
files: string[]
}

type IframeChannelEvent = IframeDoneEvent | IframeErrorEvent
interface IframeViewportEvent {
type: 'viewport'
width: number | string
height: number | string
id: string
}

type IframeChannelEvent = IframeDoneEvent | IframeErrorEvent | IframeViewportEvent

async function getContainer(config: ResolvedConfig): Promise<HTMLDivElement> {
if (config.browser.ui) {
Expand Down Expand Up @@ -99,6 +110,30 @@ client.ws.addEventListener('open', async () => {
channel.addEventListener('message', async (e: MessageEvent<IframeChannelEvent>): Promise<void> => {
debug('channel event', JSON.stringify(e.data))
switch (e.data.type) {
case 'viewport': {
const { width, height, id } = e.data
const widthStr = typeof width === 'number' ? `${width}px` : width
const heightStr = typeof height === 'number' ? `${height}px` : height
const iframe = iframes.get(id)
if (!iframe) {
const error = new Error(`Cannot find iframe with id ${id}`)
channel.postMessage({ type: 'viewport:fail', id, error: error.message })
await client.rpc.onUnhandledError({
name: 'Teardown Error',
message: error.message,
}, 'Teardown Error')
return
}
iframe.style.width = widthStr
iframe.style.height = heightStr
const ui = getUiAPI()
if (ui) {
await new Promise(r => requestAnimationFrame(r))
ui.recalculateDetailPanels()
}
channel.postMessage({ type: 'viewport:done', id })
break
}
case 'done': {
const filenames = e.data.filenames
filenames.forEach(filename => runningFiles.delete(filename))
Expand Down Expand Up @@ -161,22 +196,32 @@ async function createTesters(testFiles: string[]) {
container,
ID_ALL,
)

const ui = getUiAPI()

if (ui) {
await new Promise(r => requestAnimationFrame(r))
ui.recalculateDetailPanels()
}
}
else {
// 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()

createIframe(
container,
file,
)

if (ui) {
const id = generateFileId(file)
ui.setCurrentById(id)
await new Promise(r => requestAnimationFrame(r))
ui.recalculateDetailPanels()
}

createIframe(
container,
file,
)
await new Promise<void>((resolve) => {
channel.addEventListener('message', function handler(e: MessageEvent<IframeChannelEvent>) {
// done and error can only be triggered by the previous iframe
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/client/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { File } from '@vitest/runner'
interface UiAPI {
currentModule: File
setCurrentById: (fileId: string) => void
resetDetailSizes: () => void
recalculateDetailPanels: () => void
}

export function getUiAPI(): UiAPI | undefined {
Expand Down
1 change: 1 addition & 0 deletions packages/browser/src/client/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface BrowserRunnerState {
config: ResolvedConfig
type: 'tester' | 'orchestrator'
wrapModule: <T>(module: () => T) => T
iframeId?: string
runTests?: (tests: string[]) => Promise<void>
createTesters?: (files: string[]) => Promise<void>
}
Expand Down
2 changes: 2 additions & 0 deletions packages/browser/src/node/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => {
const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length))
// if decoded test file is "__vitest_all__" or not in the list of known files, run all tests
const tests = decodedTestFile === '__vitest_all__' || !files.includes(decodedTestFile) ? '__vitest_browser_runner__.files' : JSON.stringify([decodedTestFile])
const iframeId = decodedTestFile === '__vitest_all__' ? '"__vitest_all__"' : JSON.stringify(decodedTestFile)

if (!testerScripts)
testerScripts = await formatScripts(project.config.browser.testerScripts, server)
Expand All @@ -119,6 +120,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => {
// TODO: have only a single global variable to not pollute the global scope
`<script type="module">
__vitest_browser_runner__.runningFiles = ${tests}
__vitest_browser_runner__.iframeId = ${iframeId}
__vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles)
</script>`,
})
Expand Down
17 changes: 17 additions & 0 deletions packages/browser/src/node/plugins/pluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function generateContextFile(project: WorkspaceProject) {

return `
const rpc = () => __vitest_worker__.rpc
const channel = new BroadcastChannel('vitest')
export const server = {
platform: ${JSON.stringify(process.platform)},
Expand All @@ -54,6 +55,22 @@ export const commands = server.commands
export const page = {
get config() {
return __vitest_browser_runner__.config
},
viewport(width, height) {
const id = __vitest_browser_runner__.iframeId
channel.postMessage({ type: 'viewport', width, height, id })
return new Promise((resolve) => {
channel.addEventListener('message', function handler(e) {
if (e.data.type === 'viewport:done' && e.data.id === id) {
channel.removeEventListener('message', handler)
resolve()
}
if (e.data.type === 'viewport:fail' && e.data.id === id) {
channel.removeEventListener('message', handler)
reject(new Error(e.data.error))
}
})
})
}
}
`
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/client/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ declare global {
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const detailSizes: typeof import('./composables/navigation')['detailSizes']
const disableCoverage: typeof import('./composables/navigation')['disableCoverage']
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
const effectScope: typeof import('vue')['effectScope']
Expand Down Expand Up @@ -102,6 +103,7 @@ declare global {
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
const reactivePick: typeof import('@vueuse/core')['reactivePick']
const readonly: typeof import('vue')['readonly']
const recalculateDetailPanels: typeof import('./composables/navigation')['recalculateDetailPanels']
const ref: typeof import('vue')['ref']
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
const refDebounced: typeof import('@vueuse/core')['refDebounced']
Expand Down
49 changes: 25 additions & 24 deletions packages/ui/client/components/BrowserIframe.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,35 @@
<script setup lang="ts">
const viewport = ref('custom')
import { recalculateDetailPanels } from '~/composables/navigation'
function changeViewport(name: string) {
const sizes = {
'small-mobile': ['320px', '568px'],
'large-mobile': ['414px', '896px'],
tablet: ['834px', '1112px'],
custom: ['100%', '100%'],
}
async function changeViewport(name: string) {
if (viewport.value === name) {
viewport.value = 'custom'
} else {
viewport.value = name
}
const iframe = document.querySelector('#tester-ui iframe[data-vitest]')
if (!iframe) {
console.warn('Iframe not found')
return
}
const [width, height] = sizes[viewport.value]
iframe.style.width = width
iframe.style.height = height
await new Promise(r => requestAnimationFrame(r))
recalculateDetailPanels()
}
</script>

Expand Down Expand Up @@ -62,31 +85,9 @@ function changeViewport(name: string) {
/>
</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">
<div id="tester-ui" class="flex h-full justify-center items-center font-light op70" style="overflow: auto; width: 100%; height: 100%">
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>
17 changes: 17 additions & 0 deletions packages/ui/client/composables/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ export const coverageEnabled = computed(() => {
return coverageConfigured.value
&& coverage.value.reporter.map(([reporterName]) => reporterName).includes('html')
})
export const detailSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-detailSizes', [33, 67], {
initOnMounted: true,
})

export function recalculateDetailPanels() {
const iframe = document.querySelector('#tester-ui iframe[data-vitest]')!
const panel = document.querySelector('#details-splitpanes')!
const panelWidth = panel.clientWidth
const iframeWidth = iframe.clientWidth
const iframePercent = Math.min((iframeWidth / panelWidth) * 100, 95)
const detailsPercent = 100 - iframePercent
detailSizes.value = [iframePercent, detailsPercent]
}

// @ts-expect-error not typed global
window.__vitest_ui_api__ = {
Expand All @@ -23,6 +36,10 @@ window.__vitest_ui_api__ = {
currentModule.value = findById(fileId)
showDashboard(false)
},
resetDetailSizes() {
detailSizes.value = [33, 67]
},
recalculateDetailPanels,
}
export const openedTreeItems = useLocalStorage<string[]>('vitest-ui_task-tree-opened', [])
// TODO
Expand Down
30 changes: 11 additions & 19 deletions packages/ui/client/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,12 @@
// @ts-expect-error missing types
import { Pane, Splitpanes } from 'splitpanes'
import { browserState } from '~/composables/client';
import { coverageUrl, coverageVisible, initializeNavigation } from '../composables/navigation'
import { coverageUrl, coverageVisible, initializeNavigation, detailSizes } from '../composables/navigation'
const dashboardVisible = initializeNavigation()
const mainSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-mainSizes', [33, 67], {
initOnMounted: true,
})
const detailSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-detailSizes', [33, 67], {
initOnMounted: true,
})
const onMainResized = useDebounceFn((event: { size: number }[]) => {
event.forEach((e, i) => {
Expand All @@ -28,9 +25,6 @@ function resizeMain() {
const panelWidth = Math.min(width / 3, 300)
mainSizes.value[0] = (100 * panelWidth) / width
mainSizes.value[1] = 100 - mainSizes.value[0]
// initialize suite width with the same navigation panel width in pixels (adjust its % inside detail's split pane)
detailSizes.value[0] = (100 * panelWidth) / (width - panelWidth)
detailSizes.value[1] = 100 - detailSizes.value[0]
}
</script>

Expand All @@ -47,18 +41,16 @@ function resizeMain() {
<Coverage v-else-if="coverageVisible" key="coverage" :src="coverageUrl" />
<FileDetails v-else />
</transition>
<transition v-else>
<Splitpanes key="detail" @resized="onModuleResized">
<Pane :size="detailSizes[0]">
<BrowserIframe v-once />
</Pane>
<Pane :size="detailSizes[1]">
<Dashboard v-if="dashboardVisible" key="summary" />
<Coverage v-else-if="coverageVisible" key="coverage" :src="coverageUrl" />
<FileDetails v-else />
</Pane>
</Splitpanes>
</transition>
<Splitpanes v-else key="detail" id="details-splitpanes" @resized="onModuleResized">
<Pane :size="detailSizes[0]">
<BrowserIframe v-once />
</Pane>
<Pane :size="detailSizes[1]" min-size="5">
<Dashboard v-if="dashboardVisible" key="summary" />
<Coverage v-else-if="coverageVisible" key="coverage" :src="coverageUrl" />
<FileDetails v-else />
</Pane>
</Splitpanes>
</Pane>
</Splitpanes>
</div>
Expand Down
1 change: 1 addition & 0 deletions packages/vitest/src/node/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,7 @@ export const cliOptionsConfig: VitestCLIOptions = {
indexScripts: null,
testerScripts: null,
commands: null,
viewport: null,
},
},
pool: {
Expand Down
Loading

0 comments on commit 718512d

Please sign in to comment.