Skip to content

Commit 718512d

Browse files
authored
feat(browser): support changing the viewport (#5811)
1 parent 4bea1ca commit 718512d

File tree

16 files changed

+178
-51
lines changed

16 files changed

+178
-51
lines changed

docs/config/index.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1609,6 +1609,13 @@ To have a better type safety when using built-in providers, you can add one of t
16091609

16101610
Should Vitest UI be injected into the page. By default, injects UI iframe during development.
16111611

1612+
#### browser.viewport {#browser-viewport}
1613+
1614+
- **Type:** `{ width, height }`
1615+
- **Default:** `414x896`
1616+
1617+
Default iframe's viewport.
1618+
16121619
#### browser.indexScripts {#browser-indexscripts}
16131620

16141621
- **Type:** `BrowserScript[]`

docs/guide/browser.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,17 +135,23 @@ export const server: {
135135
* The same as calling `process.version` on the server.
136136
*/
137137
version: string
138+
/**
139+
* Name of the browser provider.
140+
*/
141+
provider: string
142+
/**
143+
* Name of the current browser.
144+
*/
145+
browser: string
138146
/**
139147
* Available commands for the browser.
140-
* @see {@link https://vitest.dev/guide/browser#commands}
141148
*/
142149
commands: BrowserCommands
143150
}
144151

145152
/**
146153
* Available commands for the browser.
147154
* A shortcut to `server.commands`.
148-
* @see {@link https://vitest.dev/guide/browser#commands}
149155
*/
150156
export const commands: BrowserCommands
151157

@@ -154,6 +160,10 @@ export const page: {
154160
* Serialized test config.
155161
*/
156162
config: ResolvedConfig
163+
/**
164+
* Change the size of iframe's viewport.
165+
*/
166+
viewport: (width: number | string, height: number | string) => Promise<void>
157167
}
158168
```
159169

packages/browser/context.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,4 +84,8 @@ export const page: {
8484
* Serialized test config.
8585
*/
8686
config: ResolvedConfig
87+
/**
88+
* Change the size of iframe's viewport.
89+
*/
90+
viewport: (width: number | string, height: number | string) => Promise<void>
8791
}

packages/browser/src/client/orchestrator.ts

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +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+
4145
iframe.style.display = 'block'
4246
iframe.style.border = 'none'
4347
iframe.style.pointerEvents = 'none'
@@ -66,7 +70,14 @@ interface IframeErrorEvent {
6670
files: string[]
6771
}
6872

69-
type IframeChannelEvent = IframeDoneEvent | IframeErrorEvent
73+
interface IframeViewportEvent {
74+
type: 'viewport'
75+
width: number | string
76+
height: number | string
77+
id: string
78+
}
79+
80+
type IframeChannelEvent = IframeDoneEvent | IframeErrorEvent | IframeViewportEvent
7081

7182
async function getContainer(config: ResolvedConfig): Promise<HTMLDivElement> {
7283
if (config.browser.ui) {
@@ -99,6 +110,30 @@ client.ws.addEventListener('open', async () => {
99110
channel.addEventListener('message', async (e: MessageEvent<IframeChannelEvent>): Promise<void> => {
100111
debug('channel event', JSON.stringify(e.data))
101112
switch (e.data.type) {
113+
case 'viewport': {
114+
const { width, height, id } = e.data
115+
const widthStr = typeof width === 'number' ? `${width}px` : width
116+
const heightStr = typeof height === 'number' ? `${height}px` : height
117+
const iframe = iframes.get(id)
118+
if (!iframe) {
119+
const error = new Error(`Cannot find iframe with id ${id}`)
120+
channel.postMessage({ type: 'viewport:fail', id, error: error.message })
121+
await client.rpc.onUnhandledError({
122+
name: 'Teardown Error',
123+
message: error.message,
124+
}, 'Teardown Error')
125+
return
126+
}
127+
iframe.style.width = widthStr
128+
iframe.style.height = heightStr
129+
const ui = getUiAPI()
130+
if (ui) {
131+
await new Promise(r => requestAnimationFrame(r))
132+
ui.recalculateDetailPanels()
133+
}
134+
channel.postMessage({ type: 'viewport:done', id })
135+
break
136+
}
102137
case 'done': {
103138
const filenames = e.data.filenames
104139
filenames.forEach(filename => runningFiles.delete(filename))
@@ -161,22 +196,32 @@ async function createTesters(testFiles: string[]) {
161196
container,
162197
ID_ALL,
163198
)
199+
200+
const ui = getUiAPI()
201+
202+
if (ui) {
203+
await new Promise(r => requestAnimationFrame(r))
204+
ui.recalculateDetailPanels()
205+
}
164206
}
165207
else {
166208
// otherwise, we need to wait for each iframe to finish before creating the next one
167209
// this is the most stable way to run tests in the browser
168210
for (const file of testFiles) {
169211
const ui = getUiAPI()
170212

213+
createIframe(
214+
container,
215+
file,
216+
)
217+
171218
if (ui) {
172219
const id = generateFileId(file)
173220
ui.setCurrentById(id)
221+
await new Promise(r => requestAnimationFrame(r))
222+
ui.recalculateDetailPanels()
174223
}
175224

176-
createIframe(
177-
container,
178-
file,
179-
)
180225
await new Promise<void>((resolve) => {
181226
channel.addEventListener('message', function handler(e: MessageEvent<IframeChannelEvent>) {
182227
// done and error can only be triggered by the previous iframe

packages/browser/src/client/ui.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import type { File } from '@vitest/runner'
33
interface UiAPI {
44
currentModule: File
55
setCurrentById: (fileId: string) => void
6+
resetDetailSizes: () => void
7+
recalculateDetailPanels: () => void
68
}
79

810
export function getUiAPI(): UiAPI | undefined {

packages/browser/src/client/utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ interface BrowserRunnerState {
1616
config: ResolvedConfig
1717
type: 'tester' | 'orchestrator'
1818
wrapModule: <T>(module: () => T) => T
19+
iframeId?: string
1920
runTests?: (tests: string[]) => Promise<void>
2021
createTesters?: (files: string[]) => Promise<void>
2122
}

packages/browser/src/node/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => {
106106
const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length))
107107
// if decoded test file is "__vitest_all__" or not in the list of known files, run all tests
108108
const tests = decodedTestFile === '__vitest_all__' || !files.includes(decodedTestFile) ? '__vitest_browser_runner__.files' : JSON.stringify([decodedTestFile])
109+
const iframeId = decodedTestFile === '__vitest_all__' ? '"__vitest_all__"' : JSON.stringify(decodedTestFile)
109110

110111
if (!testerScripts)
111112
testerScripts = await formatScripts(project.config.browser.testerScripts, server)
@@ -119,6 +120,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => {
119120
// TODO: have only a single global variable to not pollute the global scope
120121
`<script type="module">
121122
__vitest_browser_runner__.runningFiles = ${tests}
123+
__vitest_browser_runner__.iframeId = ${iframeId}
122124
__vitest_browser_runner__.runTests(__vitest_browser_runner__.runningFiles)
123125
</script>`,
124126
})

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ function generateContextFile(project: WorkspaceProject) {
4040

4141
return `
4242
const rpc = () => __vitest_worker__.rpc
43+
const channel = new BroadcastChannel('vitest')
4344
4445
export const server = {
4546
platform: ${JSON.stringify(process.platform)},
@@ -54,6 +55,22 @@ export const commands = server.commands
5455
export const page = {
5556
get config() {
5657
return __vitest_browser_runner__.config
58+
},
59+
viewport(width, height) {
60+
const id = __vitest_browser_runner__.iframeId
61+
channel.postMessage({ type: 'viewport', width, height, id })
62+
return new Promise((resolve) => {
63+
channel.addEventListener('message', function handler(e) {
64+
if (e.data.type === 'viewport:done' && e.data.id === id) {
65+
channel.removeEventListener('message', handler)
66+
resolve()
67+
}
68+
if (e.data.type === 'viewport:fail' && e.data.id === id) {
69+
channel.removeEventListener('message', handler)
70+
reject(new Error(e.data.error))
71+
}
72+
})
73+
})
5774
}
5875
}
5976
`

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ declare global {
4040
const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch']
4141
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
4242
const defineComponent: typeof import('vue')['defineComponent']
43+
const detailSizes: typeof import('./composables/navigation')['detailSizes']
4344
const disableCoverage: typeof import('./composables/navigation')['disableCoverage']
4445
const eagerComputed: typeof import('@vueuse/core')['eagerComputed']
4546
const effectScope: typeof import('vue')['effectScope']
@@ -102,6 +103,7 @@ declare global {
102103
const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit']
103104
const reactivePick: typeof import('@vueuse/core')['reactivePick']
104105
const readonly: typeof import('vue')['readonly']
106+
const recalculateDetailPanels: typeof import('./composables/navigation')['recalculateDetailPanels']
105107
const ref: typeof import('vue')['ref']
106108
const refAutoReset: typeof import('@vueuse/core')['refAutoReset']
107109
const refDebounced: typeof import('@vueuse/core')['refDebounced']

packages/ui/client/components/BrowserIframe.vue

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,35 @@
11
<script setup lang="ts">
22
const viewport = ref('custom')
3+
import { recalculateDetailPanels } from '~/composables/navigation'
34
4-
function changeViewport(name: string) {
5+
const sizes = {
6+
'small-mobile': ['320px', '568px'],
7+
'large-mobile': ['414px', '896px'],
8+
tablet: ['834px', '1112px'],
9+
custom: ['100%', '100%'],
10+
}
11+
12+
async function changeViewport(name: string) {
513
if (viewport.value === name) {
614
viewport.value = 'custom'
715
} else {
816
viewport.value = name
917
}
18+
19+
const iframe = document.querySelector('#tester-ui iframe[data-vitest]')
20+
if (!iframe) {
21+
console.warn('Iframe not found')
22+
return
23+
}
24+
25+
const [width, height] = sizes[viewport.value]
26+
27+
iframe.style.width = width
28+
iframe.style.height = height
29+
30+
await new Promise(r => requestAnimationFrame(r))
31+
32+
recalculateDetailPanels()
1033
}
1134
</script>
1235

@@ -62,31 +85,9 @@ function changeViewport(name: string) {
6285
/>
6386
</div>
6487
<div flex-auto overflow-auto>
65-
<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">
88+
<div id="tester-ui" class="flex h-full justify-center items-center font-light op70" style="overflow: auto; width: 100%; height: 100%">
6689
Select a test to run
6790
</div>
6891
</div>
6992
</div>
7093
</template>
71-
72-
<style>
73-
[data-viewport="custom"] iframe {
74-
width: 100%;
75-
height: 100%;
76-
}
77-
78-
[data-viewport="small-mobile"] iframe {
79-
width: 320px;
80-
height: 568px;
81-
}
82-
83-
[data-viewport="large-mobile"] iframe {
84-
width: 414px;
85-
height: 896px;
86-
}
87-
88-
[data-viewport="tablet"] iframe {
89-
width: 834px;
90-
height: 1112px;
91-
}
92-
</style>

0 commit comments

Comments
 (0)