diff --git a/docs/config/index.md b/docs/config/index.md index 41c09cefac76..7b42d09f80a1 100644 --- a/docs/config/index.md +++ b/docs/config/index.md @@ -1609,6 +1609,13 @@ To have a better type safety when using built-in providers, you can add one of t Should Vitest UI be injected into the page. By default, injects UI iframe during development. +#### browser.viewport {#browser-viewport} + +- **Type:** `{ width, height }` +- **Default:** `414x896` + +Default iframe's viewport. + #### browser.indexScripts {#browser-indexscripts} - **Type:** `BrowserScript[]` diff --git a/docs/guide/browser.md b/docs/guide/browser.md index 009b944241de..79c28791cc52 100644 --- a/docs/guide/browser.md +++ b/docs/guide/browser.md @@ -135,9 +135,16 @@ export const server: { * The same as calling `process.version` on the server. */ version: string + /** + * Name of the browser provider. + */ + provider: string + /** + * Name of the current browser. + */ + browser: string /** * Available commands for the browser. - * @see {@link https://vitest.dev/guide/browser#commands} */ commands: BrowserCommands } @@ -145,7 +152,6 @@ export const server: { /** * Available commands for the browser. * A shortcut to `server.commands`. - * @see {@link https://vitest.dev/guide/browser#commands} */ export const commands: BrowserCommands @@ -154,6 +160,10 @@ export const page: { * Serialized test config. */ config: ResolvedConfig + /** + * Change the size of iframe's viewport. + */ + viewport: (width: number | string, height: number | string) => Promise } ``` diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index 259b194943da..44cac03274e2 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -84,4 +84,8 @@ export const page: { * Serialized test config. */ config: ResolvedConfig + /** + * Change the size of iframe's viewport. + */ + viewport: (width: number | string, height: number | string) => Promise } diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 36d783dca777..92f2a0c840f2 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -38,6 +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.pointerEvents = 'none' @@ -66,7 +70,14 @@ interface IframeErrorEvent { files: string[] } -type IframeChannelEvent = IframeDoneEvent | IframeErrorEvent +interface IframeViewportEvent { + type: 'viewport' + width: number | string + height: number | string + id: string +} + +type IframeChannelEvent = IframeDoneEvent | IframeErrorEvent | IframeViewportEvent async function getContainer(config: ResolvedConfig): Promise { if (config.browser.ui) { @@ -99,6 +110,30 @@ client.ws.addEventListener('open', async () => { channel.addEventListener('message', async (e: MessageEvent): Promise => { debug('channel event', JSON.stringify(e.data)) 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}`) + channel.postMessage({ type: 'viewport:fail', id, error: error.message }) + await client.rpc.onUnhandledError({ + name: 'Teardown Error', + message: error.message, + }, 'Teardown Error') + return + } + iframe.style.width = widthStr + iframe.style.height = heightStr + const ui = getUiAPI() + if (ui) { + await new Promise(r => requestAnimationFrame(r)) + ui.recalculateDetailPanels() + } + channel.postMessage({ type: 'viewport:done', id }) + break + } case 'done': { const filenames = e.data.filenames filenames.forEach(filename => runningFiles.delete(filename)) @@ -161,6 +196,13 @@ async function createTesters(testFiles: string[]) { container, ID_ALL, ) + + const ui = getUiAPI() + + if (ui) { + await new Promise(r => requestAnimationFrame(r)) + ui.recalculateDetailPanels() + } } else { // otherwise, we need to wait for each iframe to finish before creating the next one @@ -168,15 +210,18 @@ async function createTesters(testFiles: string[]) { for (const file of testFiles) { const ui = getUiAPI() + createIframe( + container, + file, + ) + if (ui) { const id = generateFileId(file) ui.setCurrentById(id) + await new Promise(r => requestAnimationFrame(r)) + ui.recalculateDetailPanels() } - createIframe( - container, - file, - ) await new Promise((resolve) => { channel.addEventListener('message', function handler(e: MessageEvent) { // done and error can only be triggered by the previous iframe diff --git a/packages/browser/src/client/ui.ts b/packages/browser/src/client/ui.ts index 00117274751a..213528dff761 100644 --- a/packages/browser/src/client/ui.ts +++ b/packages/browser/src/client/ui.ts @@ -3,6 +3,8 @@ import type { File } from '@vitest/runner' interface UiAPI { currentModule: File setCurrentById: (fileId: string) => void + resetDetailSizes: () => void + recalculateDetailPanels: () => void } export function getUiAPI(): UiAPI | undefined { diff --git a/packages/browser/src/client/utils.ts b/packages/browser/src/client/utils.ts index d26abf5157c8..f8fedcc2f517 100644 --- a/packages/browser/src/client/utils.ts +++ b/packages/browser/src/client/utils.ts @@ -16,6 +16,7 @@ interface BrowserRunnerState { config: ResolvedConfig type: 'tester' | 'orchestrator' wrapModule: (module: () => T) => T + iframeId?: string runTests?: (tests: string[]) => Promise createTesters?: (files: string[]) => Promise } diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index 9ea82b726ae4..297bac8bf614 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -106,6 +106,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { const decodedTestFile = decodeURIComponent(url.pathname.slice(testerPrefix.length)) // if decoded test file is "__vitest_all__" or not in the list of known files, run all tests const tests = decodedTestFile === '__vitest_all__' || !files.includes(decodedTestFile) ? '__vitest_browser_runner__.files' : JSON.stringify([decodedTestFile]) + const iframeId = decodedTestFile === '__vitest_all__' ? '"__vitest_all__"' : JSON.stringify(decodedTestFile) if (!testerScripts) testerScripts = await formatScripts(project.config.browser.testerScripts, server) @@ -119,6 +120,7 @@ export default (project: WorkspaceProject, base = '/'): Plugin[] => { // TODO: have only a single global variable to not pollute the global scope ``, }) diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index 3b55bcb36708..6eeee677358a 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -40,6 +40,7 @@ function generateContextFile(project: WorkspaceProject) { return ` const rpc = () => __vitest_worker__.rpc +const channel = new BroadcastChannel('vitest') export const server = { platform: ${JSON.stringify(process.platform)}, @@ -54,6 +55,22 @@ export const commands = server.commands export const page = { get config() { return __vitest_browser_runner__.config + }, + viewport(width, height) { + const id = __vitest_browser_runner__.iframeId + channel.postMessage({ type: 'viewport', width, height, id }) + return new Promise((resolve) => { + channel.addEventListener('message', function handler(e) { + if (e.data.type === 'viewport:done' && e.data.id === id) { + channel.removeEventListener('message', handler) + resolve() + } + if (e.data.type === 'viewport:fail' && e.data.id === id) { + channel.removeEventListener('message', handler) + reject(new Error(e.data.error)) + } + }) + }) } } ` diff --git a/packages/ui/client/auto-imports.d.ts b/packages/ui/client/auto-imports.d.ts index f2cbcae93687..594e5ba5b5b8 100644 --- a/packages/ui/client/auto-imports.d.ts +++ b/packages/ui/client/auto-imports.d.ts @@ -40,6 +40,7 @@ declare global { const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] const defineComponent: typeof import('vue')['defineComponent'] + const detailSizes: typeof import('./composables/navigation')['detailSizes'] const disableCoverage: typeof import('./composables/navigation')['disableCoverage'] const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] const effectScope: typeof import('vue')['effectScope'] @@ -102,6 +103,7 @@ declare global { const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] const reactivePick: typeof import('@vueuse/core')['reactivePick'] const readonly: typeof import('vue')['readonly'] + const recalculateDetailPanels: typeof import('./composables/navigation')['recalculateDetailPanels'] const ref: typeof import('vue')['ref'] const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] const refDebounced: typeof import('@vueuse/core')['refDebounced'] diff --git a/packages/ui/client/components/BrowserIframe.vue b/packages/ui/client/components/BrowserIframe.vue index aa4747033cb2..a509a9b70e0f 100644 --- a/packages/ui/client/components/BrowserIframe.vue +++ b/packages/ui/client/components/BrowserIframe.vue @@ -1,12 +1,35 @@ @@ -62,31 +85,9 @@ function changeViewport(name: string) { />
-
+
Select a test to run
- - diff --git a/packages/ui/client/composables/navigation.ts b/packages/ui/client/composables/navigation.ts index fb86261680c5..20190f084888 100644 --- a/packages/ui/client/composables/navigation.ts +++ b/packages/ui/client/composables/navigation.ts @@ -12,6 +12,19 @@ export const coverageEnabled = computed(() => { return coverageConfigured.value && coverage.value.reporter.map(([reporterName]) => reporterName).includes('html') }) +export const detailSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-detailSizes', [33, 67], { + initOnMounted: true, +}) + +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] +} // @ts-expect-error not typed global window.__vitest_ui_api__ = { @@ -23,6 +36,10 @@ window.__vitest_ui_api__ = { currentModule.value = findById(fileId) showDashboard(false) }, + resetDetailSizes() { + detailSizes.value = [33, 67] + }, + recalculateDetailPanels, } export const openedTreeItems = useLocalStorage('vitest-ui_task-tree-opened', []) // TODO diff --git a/packages/ui/client/pages/index.vue b/packages/ui/client/pages/index.vue index f2fb2c9f1b16..bfa4f38d5ce9 100644 --- a/packages/ui/client/pages/index.vue +++ b/packages/ui/client/pages/index.vue @@ -2,15 +2,12 @@ // @ts-expect-error missing types import { Pane, Splitpanes } from 'splitpanes' import { browserState } from '~/composables/client'; -import { coverageUrl, coverageVisible, initializeNavigation } from '../composables/navigation' +import { coverageUrl, coverageVisible, initializeNavigation, detailSizes } from '../composables/navigation' const dashboardVisible = initializeNavigation() const mainSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-mainSizes', [33, 67], { initOnMounted: true, }) -const detailSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-detailSizes', [33, 67], { - initOnMounted: true, -}) const onMainResized = useDebounceFn((event: { size: number }[]) => { event.forEach((e, i) => { @@ -28,9 +25,6 @@ function resizeMain() { const panelWidth = Math.min(width / 3, 300) mainSizes.value[0] = (100 * panelWidth) / width mainSizes.value[1] = 100 - mainSizes.value[0] - // initialize suite width with the same navigation panel width in pixels (adjust its % inside detail's split pane) - detailSizes.value[0] = (100 * panelWidth) / (width - panelWidth) - detailSizes.value[1] = 100 - detailSizes.value[0] } @@ -47,18 +41,16 @@ function resizeMain() { - - - - - - - - - - - - + + + + + + + + + + diff --git a/packages/vitest/src/node/cli/cli-config.ts b/packages/vitest/src/node/cli/cli-config.ts index 22e54dcea7d9..eddf392d2192 100644 --- a/packages/vitest/src/node/cli/cli-config.ts +++ b/packages/vitest/src/node/cli/cli-config.ts @@ -350,6 +350,7 @@ export const cliOptionsConfig: VitestCLIOptions = { indexScripts: null, testerScripts: null, commands: null, + viewport: null, }, }, pool: { diff --git a/packages/vitest/src/node/config.ts b/packages/vitest/src/node/config.ts index fd3dd0119c45..90a10c428b88 100644 --- a/packages/vitest/src/node/config.ts +++ b/packages/vitest/src/node/config.ts @@ -537,6 +537,10 @@ export function resolveConfig( resolved.browser.isolate ??= true resolved.browser.ui ??= !isCI + resolved.browser.viewport ??= {} as any + resolved.browser.viewport.width ??= 414 + resolved.browser.viewport.height ??= 896 + if (resolved.browser.enabled && stdProvider === 'stackblitz') resolved.browser.provider = 'none' diff --git a/packages/vitest/src/types/browser.ts b/packages/vitest/src/types/browser.ts index ae610c7fe387..9b5465d18c4f 100644 --- a/packages/vitest/src/types/browser.ts +++ b/packages/vitest/src/types/browser.ts @@ -85,6 +85,22 @@ export interface BrowserConfigOptions { */ ui?: boolean + /** + * Default viewport size + */ + viewport?: { + /** + * Width of the viewport + * @default 414 + */ + width: number + /** + * Height of the viewport + * @default 896 + */ + height: number + } + /** * Scripts injected into the tester iframe. */ @@ -148,4 +164,8 @@ export interface ResolvedBrowserOptions extends BrowserConfigOptions { isolate: boolean api: ApiConfig ui: boolean + viewport: { + width: number + height: number + } } diff --git a/test/browser/test/dom.test.ts b/test/browser/test/dom.test.ts index 2e5447aa1b53..034f3a32ae2a 100644 --- a/test/browser/test/dom.test.ts +++ b/test/browser/test/dom.test.ts @@ -1,8 +1,10 @@ import { expect, test } from 'vitest' +import { page } from '@vitest/browser/context' import { createNode } from '#src/createNode' import '../src/button.css' -test('renders div', () => { +test('renders div', async () => { + await page.viewport(1500, 600) document.body.style.background = '#f3f3f3' const wrapper = document.createElement('div') wrapper.className = 'wrapper'