Skip to content

Commit 97acf09

Browse files
committed
feat(browser): add browser.detailsPanelPosition config option and button
1 parent db2d0b5 commit 97acf09

File tree

17 files changed

+275
-65
lines changed

17 files changed

+275
-65
lines changed

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -644,6 +644,10 @@ export default ({ mode }: { mode: string }) => {
644644
text: 'browser.ui',
645645
link: '/config/browser/ui',
646646
},
647+
{
648+
text: 'browser.detailsPanelPosition',
649+
link: '/config/browser/detailspanelposition',
650+
},
647651
{
648652
text: 'browser.viewport',
649653
link: '/config/browser/viewport',

docs/config/browser.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ List of available `browser` options:
9393
- [`browser.screenshotDirectory`](#browser-screenshotdirectory)
9494
- [`browser.screenshotFailures`](#browser-screenshotfailures)
9595
- [`browser.provider`](#browser-provider)
96+
- [`browser.detailsPanelPosition`](#browser-detailspanelposition)
9697

9798
Under the hood, Vitest transforms these instances into separate [test projects](/api/advanced/test-project) sharing a single Vite server for better caching performance.
9899

@@ -220,6 +221,14 @@ export interface BrowserProvider {
220221

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

224+
## browser.detailsPanelPosition
225+
226+
- **Type:** `'right' | 'bottom'`
227+
- **Default:** `'right'`
228+
- **CLI:** `--browser.detailsPanelPosition=bottom`, `--browser.detailsPanelPosition=right`
229+
230+
Controls the default position of the details panel in the Vitest UI when running browser tests. See [`browser.detailsPanelPosition`](/config/browser/detailspanelposition) for more details.
231+
223232
## browser.viewport
224233

225234
- **Type:** `{ width, height }`
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
title: browser.detailsPanelPosition | Config
3+
outline: deep
4+
---
5+
6+
# browser.detailsPanelPosition
7+
8+
- **Type:** `'right' | 'bottom'`
9+
- **Default:** `'right'`
10+
- **CLI:** `--browser.detailsPanelPosition=bottom`, `--browser.detailsPanelPosition=right`
11+
12+
Controls the default position of the details panel in the Vitest UI when running browser tests.
13+
14+
- `'right'` - Shows the details panel on the right side with a horizontal split between the browser viewport and the details panel.
15+
- `'bottom'` - Shows the details panel at the bottom with a vertical split between the browser viewport and the details panel.
16+
17+
```ts [vitest.config.ts]
18+
import { defineConfig } from 'vitest/config'
19+
20+
export default defineConfig({
21+
test: {
22+
browser: {
23+
enabled: true,
24+
detailsPanelPosition: 'bottom', // or 'right'
25+
},
26+
},
27+
})
28+
```

packages/ui/client/components/BrowserIframe.vue

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@ import { computed } from 'vue'
55
import { viewport } from '~/composables/browser'
66
import { browserState } from '~/composables/client'
77
import {
8-
hideRightPanel,
8+
detailsPanelVisible,
9+
detailsPosition,
910
panels,
1011
showNavigationPanel,
11-
showRightPanel,
1212
updateBrowserPanel,
1313
} from '~/composables/navigation'
1414
import IconButton from './IconButton.vue'
@@ -86,19 +86,11 @@ const marginLeft = computed(() => {
8686
<div class="i-carbon-content-delivery-network" />
8787
<span pl-1 font-bold text-sm flex-auto ws-nowrap overflow-hidden truncate>Browser UI</span>
8888
<IconButton
89-
v-show="panels.details.main > 0"
90-
v-tooltip.bottom="'Hide Right Panel'"
91-
title="Hide Right Panel"
89+
v-show="detailsPosition === 'right' && !detailsPanelVisible"
90+
v-tooltip.bottom="'Show Details Panel'"
91+
title="Show Details Panel"
9292
icon="i-carbon:side-panel-close"
93-
rotate-180
94-
@click="hideRightPanel()"
95-
/>
96-
<IconButton
97-
v-show="panels.details.main === 0"
98-
v-tooltip.bottom="'Show Right Panel'"
99-
title="Show Right Panel"
100-
icon="i-carbon:side-panel-close"
101-
@click="showRightPanel()"
93+
@click="detailsPanelVisible = true"
10294
/>
10395
</div>
10496
<div p="l3 y2 r2" flex="~ gap-2" items-center bg-header border="b-2 base">
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<script setup lang="ts">
2+
import IconButton from '~/components/IconButton.vue'
3+
import {
4+
detailsPanelVisible,
5+
detailsPosition,
6+
hideDetailsPanel,
7+
showDetailsPanel,
8+
toggleDetailsPosition,
9+
} from '~/composables/navigation'
10+
11+
function getDetailsPanelToggleRotation(action: 'show' | 'hide') {
12+
// `i-carbon:side-panel-close` is treated as "pointing right" by default.
13+
// We rotate it based on where the details panel is positioned.
14+
if (detailsPosition.value === 'right') {
15+
return action === 'hide' ? 'rotate-180' : ''
16+
}
17+
// detailsPosition === 'bottom'
18+
return action === 'hide' ? '-rotate-90' : 'rotate-90'
19+
}
20+
</script>
21+
22+
<template>
23+
<div
24+
p="2"
25+
flex="~ gap-2"
26+
items-center
27+
bg-header
28+
border="b base"
29+
>
30+
<IconButton
31+
v-if="detailsPanelVisible"
32+
v-tooltip.bottom="detailsPosition === 'right' ? 'Hide Right Panel' : 'Hide Bottom Panel'"
33+
:title="detailsPosition === 'right' ? 'Hide Right Panel' : 'Hide Bottom Panel'"
34+
icon="i-carbon:side-panel-close"
35+
:class="getDetailsPanelToggleRotation('hide')"
36+
@click="hideDetailsPanel"
37+
/>
38+
<IconButton
39+
v-show="!detailsPanelVisible"
40+
v-tooltip.bottom="detailsPosition === 'right' ? 'Show Right Panel' : 'Show Bottom Panel'"
41+
:title="detailsPosition === 'right' ? 'Show Right Panel' : 'Show Bottom Panel'"
42+
icon="i-carbon:side-panel-close"
43+
:class="getDetailsPanelToggleRotation('show')"
44+
@click="showDetailsPanel"
45+
/>
46+
<div flex-1 />
47+
<IconButton
48+
v-tooltip.bottom="`Switch panel position (${detailsPosition === 'right' ? 'right' : 'bottom'})`"
49+
:title="`Switch panel position (${detailsPosition === 'right' ? 'right' : 'bottom'})`"
50+
icon="i-carbon-split-screen"
51+
:class="{ 'rotate-90': detailsPosition === 'right' }"
52+
@click="toggleDetailsPosition"
53+
/>
54+
</div>
55+
</template>

packages/ui/client/composables/navigation.ts

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { File, Task } from '@vitest/runner'
22
import type { Params } from './params'
33
import { useLocalStorage, watchOnce } from '@vueuse/core'
4-
import { computed, reactive, ref, watch } from 'vue'
4+
import { computed, nextTick, reactive, ref, watch } from 'vue'
55
import { viewport } from './browser'
66
import { browserState, client, config, findById } from './client'
77
import { testRunState } from './client/state'
@@ -35,6 +35,31 @@ export const detailSizes = useLocalStorage<[left: number, right: number]>(
3535
],
3636
)
3737

38+
export const detailsPanelVisible = useLocalStorage<boolean>(
39+
'vitest-ui_details-panel-visible',
40+
true,
41+
)
42+
43+
export const detailsPosition = ref<'right' | 'bottom'>('right')
44+
45+
nextTick(() => {
46+
watch(config, () => {
47+
if (config.value?.browser?.detailsPanelPosition) {
48+
detailsPosition.value = config.value.browser.detailsPanelPosition
49+
}
50+
})
51+
})
52+
53+
export function hideDetailsPanel() {
54+
// setTimeout is used to avoid splitpanes throwing a race condition error
55+
setTimeout(() => {
56+
detailsPanelVisible.value = false
57+
}, 0)
58+
}
59+
export function showDetailsPanel() {
60+
detailsPanelVisible.value = true
61+
}
62+
3863
// live sizes of panels in percentage
3964
export const panels = reactive({
4065
navigation: mainSizes.value[0],
@@ -147,12 +172,6 @@ export function showCoverage() {
147172
activeFileId.value = ''
148173
}
149174

150-
export function hideRightPanel() {
151-
panels.details.browser = 100
152-
panels.details.main = 0
153-
detailSizes.value = [100, 0]
154-
}
155-
156175
function calculateBrowserPanel() {
157176
// we don't scale webdriverio provider because it doesn't support scaling
158177
// TODO: find a way to make this universal - maybe show browser separately(?)
@@ -165,15 +184,6 @@ function calculateBrowserPanel() {
165184
return 33
166185
}
167186

168-
export function showRightPanel() {
169-
panels.details.browser = calculateBrowserPanel()
170-
panels.details.main = 100 - panels.details.browser
171-
detailSizes.value = [
172-
panels.details.browser,
173-
panels.details.main,
174-
]
175-
}
176-
177187
export function showNavigationPanel() {
178188
panels.navigation = 33
179189
panels.details.size = 67
@@ -195,3 +205,12 @@ export function updateBrowserPanel() {
195205
panels.details.main,
196206
]
197207
}
208+
209+
export function toggleDetailsPosition() {
210+
detailsPosition.value = detailsPosition.value === 'right' ? 'bottom' : 'right'
211+
// Reset to default sizes when changing orientation
212+
const defaultSize = detailsPosition.value === 'bottom' ? 33 : 50
213+
detailSizes.value = [defaultSize, 100 - defaultSize]
214+
panels.details.browser = defaultSize
215+
panels.details.main = 100 - defaultSize
216+
}

packages/ui/client/constants.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
// @ts-expect-error not typed global
2-
const browserState = window.__vitest_browser_runner__
3-
export const PORT = import.meta.hot && !browserState ? (import.meta.env.VITE_PORT || '51204') : location.port
1+
export const PORT = import.meta.hot ? (import.meta.env.VITE_PORT || '51204') : location.port
42
export const HOST = [location.hostname, PORT].filter(Boolean).join(':')
53
export const ENTRY_URL = `${
64
location.protocol === 'https:' ? 'wss:' : 'ws:'

packages/ui/client/pages/index.vue

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import BrowserIframe from '~/components/BrowserIframe.vue'
66
import ConnectionOverlay from '~/components/ConnectionOverlay.vue'
77
import Coverage from '~/components/Coverage.vue'
88
import Dashboard from '~/components/Dashboard.vue'
9+
import DetailsHeader from '~/components/DetailsHeader.vue'
910
import FileDetails from '~/components/FileDetails.vue'
1011
import Navigation from '~/components/Navigation.vue'
1112
import ProgressBar from '~/components/ProgressBar.vue'
@@ -14,6 +15,8 @@ import {
1415
coverageUrl,
1516
coverageVisible,
1617
detailSizes,
18+
detailsPanelVisible,
19+
detailsPosition,
1720
initializeNavigation,
1821
mainSizes,
1922
panels,
@@ -50,12 +53,16 @@ const resizingMain = useDebounceFn(({ panes }: { panes: { size: number }[] }) =>
5053
5154
function recordMainResize(panes: { size: number }[]) {
5255
panels.navigation = panes[0].size
53-
panels.details.size = panes[1].size
56+
if (panes[1]) {
57+
panels.details.size = panes[1].size
58+
}
5459
}
5560
5661
function recordDetailsResize(panes: { size: number }[]) {
5762
panels.details.browser = panes[0].size
58-
panels.details.main = panes[1].size
63+
if (panes[1]) {
64+
panels.details.main = panes[1].size
65+
}
5966
}
6067
6168
function preventBrowserEvents() {
@@ -94,26 +101,51 @@ function allowBrowserEvents() {
94101
/>
95102
<FileDetails v-else key="details" />
96103
</transition>
97-
<Splitpanes
98-
v-else
99-
id="details-splitpanes"
100-
key="browser-detail"
101-
@resize="onBrowserPanelResizing"
102-
@resized="onModuleResized"
103-
>
104-
<Pane :size="detailSizes[0]" min-size="10">
105-
<BrowserIframe v-once />
106-
</Pane>
107-
<Pane :size="detailSizes[1]">
108-
<Dashboard v-if="dashboardVisible" key="summary" />
109-
<Coverage
110-
v-else-if="coverageVisible"
111-
key="coverage"
112-
:src="coverageUrl!"
104+
<template v-else>
105+
<div
106+
flex="~ col"
107+
h-full
108+
>
109+
<Splitpanes
110+
id="details-splitpanes"
111+
key="browser-detail"
112+
:horizontal="detailsPosition === 'bottom'"
113+
:class="detailsPosition === 'bottom' && !detailsPanelVisible ? 'flex-1 min-h-0 overflow-hidden' : 'h-full'"
114+
@resize="onBrowserPanelResizing"
115+
@resized="onModuleResized"
116+
>
117+
<Pane :size="detailSizes[0]" min-size="10">
118+
<BrowserIframe v-once />
119+
</Pane>
120+
<Pane
121+
v-if="detailsPanelVisible"
122+
:size="detailSizes[1]"
123+
min-size="10"
124+
>
125+
<DetailsHeader />
126+
<div
127+
h-full
128+
flex="~ col"
129+
>
130+
<div
131+
flex-1 overflow-hidden
132+
>
133+
<Dashboard v-if="dashboardVisible" key="summary" />
134+
<Coverage
135+
v-else-if="coverageVisible"
136+
key="coverage"
137+
:src="coverageUrl!"
138+
/>
139+
<FileDetails v-else key="details" />
140+
</div>
141+
</div>
142+
</Pane>
143+
</Splitpanes>
144+
<DetailsHeader
145+
v-if="detailsPosition === 'bottom' && !detailsPanelVisible"
113146
/>
114-
<FileDetails v-else key="details" />
115-
</Pane>
116-
</Splitpanes>
147+
</div>
148+
</template>
117149
</Pane>
118150
</Splitpanes>
119151
</div>

packages/vitest/src/node/cli/cli-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,11 @@ export const cliOptionsConfig: VitestCLIOptions = {
356356
description:
357357
'Show Vitest UI when running tests (default: `!process.env.CI`)',
358358
},
359+
detailsPanelPosition: {
360+
description:
361+
'Default position for the details panel in browser mode. Either `right` (horizontal split) or `bottom` (vertical split) (default: `right`)',
362+
argument: '<position>',
363+
},
359364
fileParallelism: {
360365
description:
361366
'Should browser test files run in parallel. Use `--browser.fileParallelism=false` to disable (default: `true`)',

packages/vitest/src/node/config/resolveConfig.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ export function resolveConfig(
742742
// disable in headless mode by default, and if CI is detected
743743
resolved.browser.ui ??= resolved.browser.headless === true ? false : !isCI
744744
resolved.browser.commands ??= {}
745+
resolved.browser.detailsPanelPosition ??= 'right'
745746
if (resolved.browser.screenshotDirectory) {
746747
resolved.browser.screenshotDirectory = resolve(
747748
resolved.root,

0 commit comments

Comments
 (0)