Skip to content

Commit

Permalink
fix(browser): restore the original viewport when unselecting the pres…
Browse files Browse the repository at this point in the history
…et viewport (#5821)
  • Loading branch information
sheremet-va authored Jun 2, 2024
1 parent 2e874f8 commit 5ebb3ab
Show file tree
Hide file tree
Showing 12 changed files with 114 additions and 125 deletions.
2 changes: 1 addition & 1 deletion packages/browser/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,5 +87,5 @@ export const page: {
/**
* Change the size of iframe's viewport.
*/
viewport: (width: number | string, height: number | string) => Promise<void>
viewport: (width: number, height: number) => Promise<void>
}
54 changes: 22 additions & 32 deletions packages/browser/src/client/orchestrator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +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.zIndex = '1'
iframe.style.position = 'relative'
iframe.setAttribute('allowfullscreen', 'true')
iframe.setAttribute('allow', 'clipboard-write;')

Expand Down Expand Up @@ -71,8 +69,8 @@ interface IframeErrorEvent {

interface IframeViewportEvent {
type: 'viewport'
width: number | string
height: number | string
width: number
height: number
id: string
}

Expand Down Expand Up @@ -111,8 +109,6 @@ client.ws.addEventListener('open', async () => {
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}`)
Expand All @@ -123,13 +119,7 @@ client.ws.addEventListener('open', async () => {
}, 'Teardown Error')
return
}
iframe.style.width = widthStr
iframe.style.height = heightStr
const ui = getUiAPI()
if (ui) {
await new Promise(r => requestAnimationFrame(r))
ui.recalculateDetailPanels()
}
await setIframeViewport(iframe, width, height)
channel.postMessage({ type: 'viewport:done', id })
break
}
Expand All @@ -143,7 +133,7 @@ client.ws.addEventListener('open', async () => {
// 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)
ui.setCurrentFileId(id)
}
await done()
}
Expand Down Expand Up @@ -189,37 +179,26 @@ async function createTesters(testFiles: string[]) {
container.className = 'scrolls'
container.textContent = ''
}
const { width, height } = config.browser.viewport

if (config.isolate === false) {
createIframe(
const iframe = createIframe(
container,
ID_ALL,
)

const ui = getUiAPI()

if (ui) {
await new Promise(r => requestAnimationFrame(r))
ui.recalculateDetailPanels()
}
await setIframeViewport(iframe, width, height)
}
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(
const iframe = createIframe(
container,
file,
)

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

await new Promise<void>((resolve) => {
channel.addEventListener('message', function handler(e: MessageEvent<IframeChannelEvent>) {
Expand All @@ -240,3 +219,14 @@ function generateFileId(file: string) {
const path = relative(config.root, file)
return generateHash(`${path}${project}`)
}

async function setIframeViewport(iframe: HTMLIFrameElement, width: number, height: number) {
const ui = getUiAPI()
if (ui) {
await ui.setIframeViewport(width, height)
}
else {
iframe.style.width = `${width}px`
iframe.style.height = `${height}px`
}
}
11 changes: 2 additions & 9 deletions packages/browser/src/client/ui.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import type { File } from '@vitest/runner'
import type { BrowserUI } from 'vitest'

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

export function getUiAPI(): UiAPI | undefined {
export function getUiAPI(): BrowserUI | undefined {
// @ts-expect-error not typed global
return window.__vitest_ui_api__
}
2 changes: 1 addition & 1 deletion packages/browser/src/node/plugins/pluginContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export const page = {
viewport(width, height) {
const id = __vitest_browser_runner__.iframeId
channel.postMessage({ type: 'viewport', width, height, id })
return new Promise((resolve) => {
return new Promise((resolve, reject) => {
channel.addEventListener('message', function handler(e) {
if (e.data.type === 'viewport:done' && e.data.id === id) {
channel.removeEventListener('message', handler)
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/client/auto-imports.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare global {
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
const currentModule: typeof import('./composables/navigation')['currentModule']
const customRef: typeof import('vue')['customRef']
const customViewport: typeof import('./composables/browser')['customViewport']
const dashboardVisible: typeof import('./composables/navigation')['dashboardVisible']
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
Expand All @@ -53,6 +54,7 @@ declare global {
const filesSuccess: typeof import('./composables/summary')['filesSuccess']
const filesTodo: typeof import('./composables/summary')['filesTodo']
const finished: typeof import('./composables/summary')['finished']
const getCurrentBrowserIframe: typeof import('./composables/api')['getCurrentBrowserIframe']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const getModuleGraph: typeof import('./composables/module-graph')['getModuleGraph']
Expand All @@ -76,6 +78,7 @@ declare global {
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onBrowserPanelResizing: typeof import('./composables/browser')['onBrowserPanelResizing']
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
Expand Down Expand Up @@ -115,6 +118,7 @@ declare global {
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveRef: typeof import('@vueuse/core')['resolveRef']
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
const setIframeViewport: typeof import('./composables/api')['setIframeViewport']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
Expand Down Expand Up @@ -315,6 +319,7 @@ declare global {
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
const viewMode: typeof import('./composables/params')['viewMode']
const viewport: typeof import('./composables/browser')['viewport']
const watch: typeof import('vue')['watch']
const watchArray: typeof import('@vueuse/core')['watchArray']
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
Expand Down
40 changes: 20 additions & 20 deletions packages/ui/client/components/BrowserIframe.vue
Original file line number Diff line number Diff line change
@@ -1,41 +1,34 @@
<script setup lang="ts">
import { useResizing } from '~/composables/browser'
import { viewport, customViewport } from '~/composables/browser'
import type { ViewportSize } from '~/composables/browser'
import { setIframeViewport, getCurrentBrowserIframe } from '~/composables/api'
type ViewportSize = 'small-mobile' | 'large-mobile' | 'tablet' | 'custom'
const sizes: Record<ViewportSize, [width: string, height: string]> = {
const sizes: Record<ViewportSize, [width: string, height: string] | null> = {
'small-mobile': ['320px', '568px'],
'large-mobile': ['414px', '896px'],
tablet: ['834px', '1112px'],
custom: ['100%', '100%'],
full: ['100%', '100%'],
// should not be used manually, this is just
// a fallback for the case when the viewport is not set correctly
custom: null,
}
const testerRef = ref<HTMLDivElement | undefined>()
const viewport = ref<ViewportSize>('custom')
const { recalculateDetailPanels } = useResizing(testerRef)
async function changeViewport(name: ViewportSize) {
if (viewport.value === name) {
viewport.value = 'custom'
viewport.value = customViewport.value ? 'custom' : 'full'
} else {
viewport.value = name
}
const iframe = document.querySelector<HTMLIFrameElement>('#tester-ui iframe[data-vitest]')
const iframe = getCurrentBrowserIframe()
if (!iframe) {
console.warn('Iframe not found')
return
}
const [width, height] = sizes[viewport.value]
iframe.style.width = width
iframe.style.height = height
const [width, height] = sizes[viewport.value] || customViewport.value || sizes.full
await new Promise(r => requestAnimationFrame(r))
recalculateDetailPanels()
await setIframeViewport(width, height)
}
</script>

Expand Down Expand Up @@ -68,6 +61,13 @@ async function changeViewport(name: ViewportSize) {
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="'Flexible'"
title="Flexible"
icon="i-carbon:fit-to-screen"
:active="viewport === 'full'"
@click="changeViewport('full')"
/>
<IconButton
v-tooltip.bottom="'Small mobile'"
title="Small mobile"
Expand All @@ -91,7 +91,7 @@ async function changeViewport(name: ViewportSize) {
/>
</div>
<div flex-auto class="scrolls">
<div id="tester-ui" ref="testerRef" class="flex h-full justify-center items-center font-light op70" style="overflow: auto; width: 100%; height: 100%">
<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>
Expand Down
46 changes: 46 additions & 0 deletions packages/ui/client/composables/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { BrowserUI } from 'vitest'
import { findById } from './client'
import { customViewport, viewport } from './browser'
import { detailSizes } from '~/composables/navigation'

const ui: BrowserUI = {
setCurrentFileId(fileId: string) {
activeFileId.value = fileId
currentModule.value = findById(fileId)
showDashboard(false)
},
async setIframeViewport(width: number, height: number) {
// reset the button before setting a custom viewport
viewport.value = 'custom'
customViewport.value = [width, height]
await setIframeViewport(width, height)
},
}

// @ts-expect-error not typed global
window.__vitest_ui_api__ = ui

function recalculateDetailPanels() {
const iframe = getCurrentBrowserIframe()
const panel = document.querySelector<HTMLDivElement>('#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]
}

export function getCurrentBrowserIframe() {
return document.querySelector<HTMLIFrameElement>('#tester-ui iframe[data-vitest]')!
}

export async function setIframeViewport(width: number | string, height: number | string) {
const iframe = getCurrentBrowserIframe()
// change the viewport of the iframe
iframe.style.width = typeof width === 'string' ? width : `${width}px`
iframe.style.height = typeof height === 'string' ? height : `${height}px`
// wait until it renders the new size and resize the panel to make the iframe visible
// this will not make it fully visible if viewport is too wide, but it's better than nothing
await new Promise(r => requestAnimationFrame(r))
recalculateDetailPanels()
}
50 changes: 8 additions & 42 deletions packages/ui/client/composables/browser.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,11 @@
import type { Ref } from 'vue'
import { detailSizes } from '~/composables/navigation'
export type ViewportSize = 'small-mobile' | 'large-mobile' | 'tablet' | 'full' | 'custom'
export const viewport = ref<ViewportSize>('full')
export const customViewport = ref<[number, number]>()

type ResizingListener = (isResizing: boolean) => void
export function onBrowserPanelResizing(isResizing: boolean) {
const tester = document.querySelector<HTMLDivElement>('#tester-ui')
if (!tester)
return

const resizingListeners = new Set<ResizingListener>()

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]
}

export function useResizing(testerRef: Ref<HTMLDivElement | undefined>) {
function onResizing(isResizing: boolean) {
const tester = testerRef.value
if (!tester)
return

tester.style.pointerEvents = isResizing ? 'none' : ''
}

onMounted(() => {
resizingListeners.add(onResizing)
})

onUnmounted(() => {
resizingListeners.delete(onResizing)
})

return { recalculateDetailPanels }
}

export function useNotifyResizing() {
function notifyResizing(isResizing: boolean) {
for (const listener of resizingListeners)
listener(isResizing)
}

return { notifyResizing }
tester.style.pointerEvents = isResizing ? 'none' : ''
}
15 changes: 0 additions & 15 deletions packages/ui/client/composables/navigation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,6 @@ export const detailSizes = useLocalStorage<[left: number, right: number]>('vites
initOnMounted: true,
})

// @ts-expect-error not typed global
window.__vitest_ui_api__ = {
get currentModule() {
return currentModule.value
},
setCurrentById(fileId: string) {
activeFileId.value = fileId
currentModule.value = findById(fileId)
showDashboard(false)
},
resetDetailSizes() {
detailSizes.value = [33, 67]
},
recalculateDetailPanels,
}
export const openedTreeItems = useLocalStorage<string[]>('vitest-ui_task-tree-opened', [])
// TODO
// For html report preview, "coverage.reportsDirectory" must be explicitly set as a subdirectory of html report.
Expand Down
Loading

0 comments on commit 5ebb3ab

Please sign in to comment.