Skip to content

Commit 5ebb3ab

Browse files
authored
fix(browser): restore the original viewport when unselecting the preset viewport (#5821)
1 parent 2e874f8 commit 5ebb3ab

File tree

12 files changed

+114
-125
lines changed

12 files changed

+114
-125
lines changed

packages/browser/context.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,5 +87,5 @@ export const page: {
8787
/**
8888
* Change the size of iframe's viewport.
8989
*/
90-
viewport: (width: number | string, height: number | string) => Promise<void>
90+
viewport: (width: number, height: number) => Promise<void>
9191
}

packages/browser/src/client/orchestrator.ts

Lines changed: 22 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,10 @@ function createIframe(container: HTMLDivElement, file: string) {
3838
iframe.setAttribute('src', `${url.pathname}__vitest_test__/__test__/${encodeURIComponent(file)}`)
3939
iframe.setAttribute('data-vitest', 'true')
4040

41-
const config = getConfig().browser
42-
iframe.style.width = `${config.viewport.width}px`
43-
iframe.style.height = `${config.viewport.height}px`
44-
4541
iframe.style.display = 'block'
4642
iframe.style.border = 'none'
43+
iframe.style.zIndex = '1'
44+
iframe.style.position = 'relative'
4745
iframe.setAttribute('allowfullscreen', 'true')
4846
iframe.setAttribute('allow', 'clipboard-write;')
4947

@@ -71,8 +69,8 @@ interface IframeErrorEvent {
7169

7270
interface IframeViewportEvent {
7371
type: 'viewport'
74-
width: number | string
75-
height: number | string
72+
width: number
73+
height: number
7674
id: string
7775
}
7876

@@ -111,8 +109,6 @@ client.ws.addEventListener('open', async () => {
111109
switch (e.data.type) {
112110
case 'viewport': {
113111
const { width, height, id } = e.data
114-
const widthStr = typeof width === 'number' ? `${width}px` : width
115-
const heightStr = typeof height === 'number' ? `${height}px` : height
116112
const iframe = iframes.get(id)
117113
if (!iframe) {
118114
const error = new Error(`Cannot find iframe with id ${id}`)
@@ -123,13 +119,7 @@ client.ws.addEventListener('open', async () => {
123119
}, 'Teardown Error')
124120
return
125121
}
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)
133123
channel.postMessage({ type: 'viewport:done', id })
134124
break
135125
}
@@ -143,7 +133,7 @@ client.ws.addEventListener('open', async () => {
143133
// so we only select it when the run is done
144134
if (ui && filenames.length > 1) {
145135
const id = generateFileId(filenames[filenames.length - 1])
146-
ui.setCurrentById(id)
136+
ui.setCurrentFileId(id)
147137
}
148138
await done()
149139
}
@@ -189,37 +179,26 @@ async function createTesters(testFiles: string[]) {
189179
container.className = 'scrolls'
190180
container.textContent = ''
191181
}
182+
const { width, height } = config.browser.viewport
192183

193184
if (config.isolate === false) {
194-
createIframe(
185+
const iframe = createIframe(
195186
container,
196187
ID_ALL,
197188
)
198189

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)
205191
}
206192
else {
207193
// otherwise, we need to wait for each iframe to finish before creating the next one
208194
// this is the most stable way to run tests in the browser
209195
for (const file of testFiles) {
210-
const ui = getUiAPI()
211-
212-
createIframe(
196+
const iframe = createIframe(
213197
container,
214198
file,
215199
)
216200

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)
223202

224203
await new Promise<void>((resolve) => {
225204
channel.addEventListener('message', function handler(e: MessageEvent<IframeChannelEvent>) {
@@ -240,3 +219,14 @@ function generateFileId(file: string) {
240219
const path = relative(config.root, file)
241220
return generateHash(`${path}${project}`)
242221
}
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+
}
232+
}

packages/browser/src/client/ui.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,6 @@
1-
import type { File } from '@vitest/runner'
1+
import type { BrowserUI } from 'vitest'
22

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 {
114
// @ts-expect-error not typed global
125
return window.__vitest_ui_api__
136
}

packages/browser/src/node/plugins/pluginContext.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const page = {
5959
viewport(width, height) {
6060
const id = __vitest_browser_runner__.iframeId
6161
channel.postMessage({ type: 'viewport', width, height, id })
62-
return new Promise((resolve) => {
62+
return new Promise((resolve, reject) => {
6363
channel.addEventListener('message', function handler(e) {
6464
if (e.data.type === 'viewport:done' && e.data.id === id) {
6565
channel.removeEventListener('message', handler)

packages/ui/client/auto-imports.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ declare global {
3535
const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn']
3636
const currentModule: typeof import('./composables/navigation')['currentModule']
3737
const customRef: typeof import('vue')['customRef']
38+
const customViewport: typeof import('./composables/browser')['customViewport']
3839
const dashboardVisible: typeof import('./composables/navigation')['dashboardVisible']
3940
const debouncedRef: typeof import('@vueuse/core')['debouncedRef']
4041
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
@@ -53,6 +54,7 @@ declare global {
5354
const filesSuccess: typeof import('./composables/summary')['filesSuccess']
5455
const filesTodo: typeof import('./composables/summary')['filesTodo']
5556
const finished: typeof import('./composables/summary')['finished']
57+
const getCurrentBrowserIframe: typeof import('./composables/api')['getCurrentBrowserIframe']
5658
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
5759
const getCurrentScope: typeof import('vue')['getCurrentScope']
5860
const getModuleGraph: typeof import('./composables/module-graph')['getModuleGraph']
@@ -76,6 +78,7 @@ declare global {
7678
const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate']
7779
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
7880
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
81+
const onBrowserPanelResizing: typeof import('./composables/browser')['onBrowserPanelResizing']
7982
const onClickOutside: typeof import('@vueuse/core')['onClickOutside']
8083
const onDeactivated: typeof import('vue')['onDeactivated']
8184
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
@@ -115,6 +118,7 @@ declare global {
115118
const resolveComponent: typeof import('vue')['resolveComponent']
116119
const resolveRef: typeof import('@vueuse/core')['resolveRef']
117120
const resolveUnref: typeof import('@vueuse/core')['resolveUnref']
121+
const setIframeViewport: typeof import('./composables/api')['setIframeViewport']
118122
const shallowReactive: typeof import('vue')['shallowReactive']
119123
const shallowReadonly: typeof import('vue')['shallowReadonly']
120124
const shallowRef: typeof import('vue')['shallowRef']
@@ -315,6 +319,7 @@ declare global {
315319
const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll']
316320
const useWindowSize: typeof import('@vueuse/core')['useWindowSize']
317321
const viewMode: typeof import('./composables/params')['viewMode']
322+
const viewport: typeof import('./composables/browser')['viewport']
318323
const watch: typeof import('vue')['watch']
319324
const watchArray: typeof import('@vueuse/core')['watchArray']
320325
const watchAtMost: typeof import('@vueuse/core')['watchAtMost']

packages/ui/client/components/BrowserIframe.vue

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,34 @@
11
<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'
35
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> = {
77
'small-mobile': ['320px', '568px'],
88
'large-mobile': ['414px', '896px'],
99
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,
1114
}
1215
13-
const testerRef = ref<HTMLDivElement | undefined>()
14-
const viewport = ref<ViewportSize>('custom')
15-
16-
const { recalculateDetailPanels } = useResizing(testerRef)
17-
1816
async function changeViewport(name: ViewportSize) {
1917
if (viewport.value === name) {
20-
viewport.value = 'custom'
18+
viewport.value = customViewport.value ? 'custom' : 'full'
2119
} else {
2220
viewport.value = name
2321
}
2422
25-
const iframe = document.querySelector<HTMLIFrameElement>('#tester-ui iframe[data-vitest]')
23+
const iframe = getCurrentBrowserIframe()
2624
if (!iframe) {
2725
console.warn('Iframe not found')
2826
return
2927
}
3028
31-
const [width, height] = sizes[viewport.value]
32-
33-
iframe.style.width = width
34-
iframe.style.height = height
29+
const [width, height] = sizes[viewport.value] || customViewport.value || sizes.full
3530
36-
await new Promise(r => requestAnimationFrame(r))
37-
38-
recalculateDetailPanels()
31+
await setIframeViewport(width, height)
3932
}
4033
</script>
4134

@@ -68,6 +61,13 @@ async function changeViewport(name: ViewportSize) {
6861
border="b-2 base"
6962
>
7063
<!-- TODO: these are only for preview (thank you Storybook!), we need to support more different and custom sizes (as a dropdown) -->
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+
/>
7171
<IconButton
7272
v-tooltip.bottom="'Small mobile'"
7373
title="Small mobile"
@@ -91,7 +91,7 @@ async function changeViewport(name: ViewportSize) {
9191
/>
9292
</div>
9393
<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%">
9595
Select a test to run
9696
</div>
9797
</div>
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
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+
}
Lines changed: 8 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,11 @@
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]>()
34

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
59

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' : ''
4511
}

packages/ui/client/composables/navigation.ts

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,6 @@ export const detailSizes = useLocalStorage<[left: number, right: number]>('vites
1616
initOnMounted: true,
1717
})
1818

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-
}
3419
export const openedTreeItems = useLocalStorage<string[]>('vitest-ui_task-tree-opened', [])
3520
// TODO
3621
// For html report preview, "coverage.reportsDirectory" must be explicitly set as a subdirectory of html report.

0 commit comments

Comments
 (0)