[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

fix(browser): restore the original viewport when unselecting the preset viewport (#5821)

authored by

Vladimir and committed by
GitHub
(Jun 2, 2024, 9:10 PM +0200) 5ebb3abf 2e874f88

+114 -125
+1 -1
packages/browser/context.d.ts
··· 87 87 /** 88 88 * Change the size of iframe's viewport. 89 89 */ 90 - viewport: (width: number | string, height: number | string) => Promise<void> 90 + viewport: (width: number, height: number) => Promise<void> 91 91 }
+5
packages/ui/client/auto-imports.d.ts
··· 35 35 const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 36 36 const currentModule: typeof import('./composables/navigation')['currentModule'] 37 37 const customRef: typeof import('vue')['customRef'] 38 + const customViewport: typeof import('./composables/browser')['customViewport'] 38 39 const dashboardVisible: typeof import('./composables/navigation')['dashboardVisible'] 39 40 const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 40 41 const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] ··· 53 54 const filesSuccess: typeof import('./composables/summary')['filesSuccess'] 54 55 const filesTodo: typeof import('./composables/summary')['filesTodo'] 55 56 const finished: typeof import('./composables/summary')['finished'] 57 + const getCurrentBrowserIframe: typeof import('./composables/api')['getCurrentBrowserIframe'] 56 58 const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 57 59 const getCurrentScope: typeof import('vue')['getCurrentScope'] 58 60 const getModuleGraph: typeof import('./composables/module-graph')['getModuleGraph'] ··· 76 78 const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] 77 79 const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 78 80 const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 81 + const onBrowserPanelResizing: typeof import('./composables/browser')['onBrowserPanelResizing'] 79 82 const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 80 83 const onDeactivated: typeof import('vue')['onDeactivated'] 81 84 const onErrorCaptured: typeof import('vue')['onErrorCaptured'] ··· 115 118 const resolveComponent: typeof import('vue')['resolveComponent'] 116 119 const resolveRef: typeof import('@vueuse/core')['resolveRef'] 117 120 const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 121 + const setIframeViewport: typeof import('./composables/api')['setIframeViewport'] 118 122 const shallowReactive: typeof import('vue')['shallowReactive'] 119 123 const shallowReadonly: typeof import('vue')['shallowReadonly'] 120 124 const shallowRef: typeof import('vue')['shallowRef'] ··· 315 319 const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 316 320 const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 317 321 const viewMode: typeof import('./composables/params')['viewMode'] 322 + const viewport: typeof import('./composables/browser')['viewport'] 318 323 const watch: typeof import('vue')['watch'] 319 324 const watchArray: typeof import('@vueuse/core')['watchArray'] 320 325 const watchAtMost: typeof import('@vueuse/core')['watchAtMost']
+22 -32
packages/browser/src/client/orchestrator.ts
··· 38 38 iframe.setAttribute('src', `${url.pathname}__vitest_test__/__test__/${encodeURIComponent(file)}`) 39 39 iframe.setAttribute('data-vitest', 'true') 40 40 41 - const config = getConfig().browser 42 - iframe.style.width = `${config.viewport.width}px` 43 - iframe.style.height = `${config.viewport.height}px` 44 - 45 41 iframe.style.display = 'block' 46 42 iframe.style.border = 'none' 43 + iframe.style.zIndex = '1' 44 + iframe.style.position = 'relative' 47 45 iframe.setAttribute('allowfullscreen', 'true') 48 46 iframe.setAttribute('allow', 'clipboard-write;') 49 47 ··· 71 69 72 70 interface IframeViewportEvent { 73 71 type: 'viewport' 74 - width: number | string 75 - height: number | string 72 + width: number 73 + height: number 76 74 id: string 77 75 } 78 76 ··· 111 109 switch (e.data.type) { 112 110 case 'viewport': { 113 111 const { width, height, id } = e.data 114 - const widthStr = typeof width === 'number' ? `${width}px` : width 115 - const heightStr = typeof height === 'number' ? `${height}px` : height 116 112 const iframe = iframes.get(id) 117 113 if (!iframe) { 118 114 const error = new Error(`Cannot find iframe with id ${id}`) ··· 123 119 }, 'Teardown Error') 124 120 return 125 121 } 126 - iframe.style.width = widthStr 127 - iframe.style.height = heightStr 128 - const ui = getUiAPI() 129 - if (ui) { 130 - await new Promise(r => requestAnimationFrame(r)) 131 - ui.recalculateDetailPanels() 132 - } 122 + await setIframeViewport(iframe, width, height) 133 123 channel.postMessage({ type: 'viewport:done', id }) 134 124 break 135 125 } ··· 143 133 // so we only select it when the run is done 144 134 if (ui && filenames.length > 1) { 145 135 const id = generateFileId(filenames[filenames.length - 1]) 146 - ui.setCurrentById(id) 136 + ui.setCurrentFileId(id) 147 137 } 148 138 await done() 149 139 } ··· 189 179 container.className = 'scrolls' 190 180 container.textContent = '' 191 181 } 182 + const { width, height } = config.browser.viewport 192 183 193 184 if (config.isolate === false) { 194 - createIframe( 185 + const iframe = createIframe( 195 186 container, 196 187 ID_ALL, 197 188 ) 198 189 199 - const ui = getUiAPI() 200 - 201 - if (ui) { 202 - await new Promise(r => requestAnimationFrame(r)) 203 - ui.recalculateDetailPanels() 204 - } 190 + await setIframeViewport(iframe, width, height) 205 191 } 206 192 else { 207 193 // otherwise, we need to wait for each iframe to finish before creating the next one 208 194 // this is the most stable way to run tests in the browser 209 195 for (const file of testFiles) { 210 - const ui = getUiAPI() 211 - 212 - createIframe( 196 + const iframe = createIframe( 213 197 container, 214 198 file, 215 199 ) 216 200 217 - if (ui) { 218 - const id = generateFileId(file) 219 - ui.setCurrentById(id) 220 - await new Promise(r => requestAnimationFrame(r)) 221 - ui.recalculateDetailPanels() 222 - } 201 + await setIframeViewport(iframe, width, height) 223 202 224 203 await new Promise<void>((resolve) => { 225 204 channel.addEventListener('message', function handler(e: MessageEvent<IframeChannelEvent>) { ··· 239 218 const project = config.name || '' 240 219 const path = relative(config.root, file) 241 220 return generateHash(`${path}${project}`) 221 + } 222 + 223 + async function setIframeViewport(iframe: HTMLIFrameElement, width: number, height: number) { 224 + const ui = getUiAPI() 225 + if (ui) { 226 + await ui.setIframeViewport(width, height) 227 + } 228 + else { 229 + iframe.style.width = `${width}px` 230 + iframe.style.height = `${height}px` 231 + } 242 232 }
+2 -9
packages/browser/src/client/ui.ts
··· 1 - import type { File } from '@vitest/runner' 1 + import type { BrowserUI } from 'vitest' 2 2 3 - interface UiAPI { 4 - currentModule: File 5 - setCurrentById: (fileId: string) => void 6 - resetDetailSizes: () => void 7 - recalculateDetailPanels: () => void 8 - } 9 - 10 - export function getUiAPI(): UiAPI | undefined { 3 + export function getUiAPI(): BrowserUI | undefined { 11 4 // @ts-expect-error not typed global 12 5 return window.__vitest_ui_api__ 13 6 }
+20 -20
packages/ui/client/components/BrowserIframe.vue
··· 1 1 <script setup lang="ts"> 2 - import { useResizing } from '~/composables/browser' 2 + import { viewport, customViewport } from '~/composables/browser' 3 + import type { ViewportSize } from '~/composables/browser' 4 + import { setIframeViewport, getCurrentBrowserIframe } from '~/composables/api' 3 5 4 - type ViewportSize = 'small-mobile' | 'large-mobile' | 'tablet' | 'custom' 5 - 6 - const sizes: Record<ViewportSize, [width: string, height: string]> = { 6 + const sizes: Record<ViewportSize, [width: string, height: string] | null> = { 7 7 'small-mobile': ['320px', '568px'], 8 8 'large-mobile': ['414px', '896px'], 9 9 tablet: ['834px', '1112px'], 10 - custom: ['100%', '100%'], 10 + full: ['100%', '100%'], 11 + // should not be used manually, this is just 12 + // a fallback for the case when the viewport is not set correctly 13 + custom: null, 11 14 } 12 - 13 - const testerRef = ref<HTMLDivElement | undefined>() 14 - const viewport = ref<ViewportSize>('custom') 15 - 16 - const { recalculateDetailPanels } = useResizing(testerRef) 17 15 18 16 async function changeViewport(name: ViewportSize) { 19 17 if (viewport.value === name) { 20 - viewport.value = 'custom' 18 + viewport.value = customViewport.value ? 'custom' : 'full' 21 19 } else { 22 20 viewport.value = name 23 21 } 24 22 25 - const iframe = document.querySelector<HTMLIFrameElement>('#tester-ui iframe[data-vitest]') 23 + const iframe = getCurrentBrowserIframe() 26 24 if (!iframe) { 27 25 console.warn('Iframe not found') 28 26 return 29 27 } 30 28 31 - const [width, height] = sizes[viewport.value] 29 + const [width, height] = sizes[viewport.value] || customViewport.value || sizes.full 32 30 33 - iframe.style.width = width 34 - iframe.style.height = height 35 - 36 - await new Promise(r => requestAnimationFrame(r)) 37 - 38 - recalculateDetailPanels() 31 + await setIframeViewport(width, height) 39 32 } 40 33 </script> 41 34 ··· 69 62 > 70 63 <!-- TODO: these are only for preview (thank you Storybook!), we need to support more different and custom sizes (as a dropdown) --> 71 64 <IconButton 65 + v-tooltip.bottom="'Flexible'" 66 + title="Flexible" 67 + icon="i-carbon:fit-to-screen" 68 + :active="viewport === 'full'" 69 + @click="changeViewport('full')" 70 + /> 71 + <IconButton 72 72 v-tooltip.bottom="'Small mobile'" 73 73 title="Small mobile" 74 74 icon="i-carbon:mobile" ··· 91 91 /> 92 92 </div> 93 93 <div flex-auto class="scrolls"> 94 - <div id="tester-ui" ref="testerRef" class="flex h-full justify-center items-center font-light op70" style="overflow: auto; width: 100%; height: 100%"> 94 + <div id="tester-ui" class="flex h-full justify-center items-center font-light op70" style="overflow: auto; width: 100%; height: 100%"> 95 95 Select a test to run 96 96 </div> 97 97 </div>
+46
packages/ui/client/composables/api.ts
··· 1 + import type { BrowserUI } from 'vitest' 2 + import { findById } from './client' 3 + import { customViewport, viewport } from './browser' 4 + import { detailSizes } from '~/composables/navigation' 5 + 6 + const ui: BrowserUI = { 7 + setCurrentFileId(fileId: string) { 8 + activeFileId.value = fileId 9 + currentModule.value = findById(fileId) 10 + showDashboard(false) 11 + }, 12 + async setIframeViewport(width: number, height: number) { 13 + // reset the button before setting a custom viewport 14 + viewport.value = 'custom' 15 + customViewport.value = [width, height] 16 + await setIframeViewport(width, height) 17 + }, 18 + } 19 + 20 + // @ts-expect-error not typed global 21 + window.__vitest_ui_api__ = ui 22 + 23 + function recalculateDetailPanels() { 24 + const iframe = getCurrentBrowserIframe() 25 + const panel = document.querySelector<HTMLDivElement>('#details-splitpanes')! 26 + const panelWidth = panel.clientWidth 27 + const iframeWidth = iframe.clientWidth 28 + const iframePercent = Math.min((iframeWidth / panelWidth) * 100, 95) 29 + const detailsPercent = 100 - iframePercent 30 + detailSizes.value = [iframePercent, detailsPercent] 31 + } 32 + 33 + export function getCurrentBrowserIframe() { 34 + return document.querySelector<HTMLIFrameElement>('#tester-ui iframe[data-vitest]')! 35 + } 36 + 37 + export async function setIframeViewport(width: number | string, height: number | string) { 38 + const iframe = getCurrentBrowserIframe() 39 + // change the viewport of the iframe 40 + iframe.style.width = typeof width === 'string' ? width : `${width}px` 41 + iframe.style.height = typeof height === 'string' ? height : `${height}px` 42 + // wait until it renders the new size and resize the panel to make the iframe visible 43 + // this will not make it fully visible if viewport is too wide, but it's better than nothing 44 + await new Promise(r => requestAnimationFrame(r)) 45 + recalculateDetailPanels() 46 + }
+8 -42
packages/ui/client/composables/browser.ts
··· 1 - import type { Ref } from 'vue' 2 - import { detailSizes } from '~/composables/navigation' 1 + export type ViewportSize = 'small-mobile' | 'large-mobile' | 'tablet' | 'full' | 'custom' 2 + export const viewport = ref<ViewportSize>('full') 3 + export const customViewport = ref<[number, number]>() 3 4 4 - type ResizingListener = (isResizing: boolean) => void 5 + export function onBrowserPanelResizing(isResizing: boolean) { 6 + const tester = document.querySelector<HTMLDivElement>('#tester-ui') 7 + if (!tester) 8 + return 5 9 6 - const resizingListeners = new Set<ResizingListener>() 7 - 8 - export function recalculateDetailPanels() { 9 - const iframe = document.querySelector('#tester-ui iframe[data-vitest]')! 10 - const panel = document.querySelector('#details-splitpanes')! 11 - const panelWidth = panel.clientWidth 12 - const iframeWidth = iframe.clientWidth 13 - const iframePercent = Math.min((iframeWidth / panelWidth) * 100, 95) 14 - const detailsPercent = 100 - iframePercent 15 - detailSizes.value = [iframePercent, detailsPercent] 16 - } 17 - 18 - export function useResizing(testerRef: Ref<HTMLDivElement | undefined>) { 19 - function onResizing(isResizing: boolean) { 20 - const tester = testerRef.value 21 - if (!tester) 22 - return 23 - 24 - tester.style.pointerEvents = isResizing ? 'none' : '' 25 - } 26 - 27 - onMounted(() => { 28 - resizingListeners.add(onResizing) 29 - }) 30 - 31 - onUnmounted(() => { 32 - resizingListeners.delete(onResizing) 33 - }) 34 - 35 - return { recalculateDetailPanels } 36 - } 37 - 38 - export function useNotifyResizing() { 39 - function notifyResizing(isResizing: boolean) { 40 - for (const listener of resizingListeners) 41 - listener(isResizing) 42 - } 43 - 44 - return { notifyResizing } 10 + tester.style.pointerEvents = isResizing ? 'none' : '' 45 11 }
-15
packages/ui/client/composables/navigation.ts
··· 16 16 initOnMounted: true, 17 17 }) 18 18 19 - // @ts-expect-error not typed global 20 - window.__vitest_ui_api__ = { 21 - get currentModule() { 22 - return currentModule.value 23 - }, 24 - setCurrentById(fileId: string) { 25 - activeFileId.value = fileId 26 - currentModule.value = findById(fileId) 27 - showDashboard(false) 28 - }, 29 - resetDetailSizes() { 30 - detailSizes.value = [33, 67] 31 - }, 32 - recalculateDetailPanels, 33 - } 34 19 export const openedTreeItems = useLocalStorage<string[]>('vitest-ui_task-tree-opened', []) 35 20 // TODO 36 21 // For html report preview, "coverage.reportsDirectory" must be explicitly set as a subdirectory of html report.
+4 -5
packages/ui/client/pages/index.vue
··· 3 3 import { Pane, Splitpanes } from 'splitpanes' 4 4 import { browserState } from '~/composables/client'; 5 5 import { coverageUrl, coverageVisible, initializeNavigation, detailSizes } from '~/composables/navigation' 6 - import { useNotifyResizing } from '~/composables/browser' 6 + import { onBrowserPanelResizing } from '~/composables/browser' 7 7 8 - const { notifyResizing } = useNotifyResizing() 9 8 const dashboardVisible = initializeNavigation() 10 9 11 10 const mainSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-mainSizes', [33, 67], { ··· 21 20 event.forEach((e, i) => { 22 21 detailSizes.value[i] = e.size 23 22 }) 24 - notifyResizing(false) 23 + onBrowserPanelResizing(false) 25 24 }, 0) 26 25 27 26 function resizeMain() { ··· 45 44 <Coverage v-else-if="coverageVisible" key="coverage" :src="coverageUrl" /> 46 45 <FileDetails v-else /> 47 46 </transition> 48 - <Splitpanes v-else key="detail" id="details-splitpanes" @resize="notifyResizing(true)" @resized="onModuleResized"> 49 - <Pane :size="detailSizes[0]"> 47 + <Splitpanes v-else key="detail" id="details-splitpanes" @resize="onBrowserPanelResizing(true)" @resized="onModuleResized"> 48 + <Pane :size="detailSizes[0]" min-size="10"> 50 49 <BrowserIframe v-once /> 51 50 </Pane> 52 51 <Pane :size="detailSizes[1]" min-size="5">
+1
packages/vitest/src/types/index.ts
··· 24 24 Mocked, 25 25 MockedClass, 26 26 } from '../integrations/spy' 27 + export type { BrowserUI } from './ui' 27 28 28 29 export type { 29 30 ExpectStatic,
+4
packages/vitest/src/types/ui.ts
··· 1 + export interface BrowserUI { 2 + setCurrentFileId: (fileId: string) => void 3 + setIframeViewport: (width: number, height: number) => Promise<void> 4 + }
+1 -1
packages/browser/src/node/plugins/pluginContext.ts
··· 59 59 viewport(width, height) { 60 60 const id = __vitest_browser_runner__.iframeId 61 61 channel.postMessage({ type: 'viewport', width, height, id }) 62 - return new Promise((resolve) => { 62 + return new Promise((resolve, reject) => { 63 63 channel.addEventListener('message', function handler(e) { 64 64 if (e.data.type === 'viewport:done' && e.data.id === id) { 65 65 channel.removeEventListener('message', handler)