diff --git a/packages/browser/context.d.ts b/packages/browser/context.d.ts index 44cac03274e2..efe50f936ceb 100644 --- a/packages/browser/context.d.ts +++ b/packages/browser/context.d.ts @@ -87,5 +87,5 @@ export const page: { /** * Change the size of iframe's viewport. */ - viewport: (width: number | string, height: number | string) => Promise + viewport: (width: number, height: number) => Promise } diff --git a/packages/browser/src/client/orchestrator.ts b/packages/browser/src/client/orchestrator.ts index 606b43c99af8..f67fbccf17e2 100644 --- a/packages/browser/src/client/orchestrator.ts +++ b/packages/browser/src/client/orchestrator.ts @@ -38,12 +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.zIndex = '1' + iframe.style.position = 'relative' iframe.setAttribute('allowfullscreen', 'true') iframe.setAttribute('allow', 'clipboard-write;') @@ -71,8 +69,8 @@ interface IframeErrorEvent { interface IframeViewportEvent { type: 'viewport' - width: number | string - height: number | string + width: number + height: number id: string } @@ -111,8 +109,6 @@ client.ws.addEventListener('open', async () => { 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}`) @@ -123,13 +119,7 @@ client.ws.addEventListener('open', async () => { }, 'Teardown Error') return } - iframe.style.width = widthStr - iframe.style.height = heightStr - const ui = getUiAPI() - if (ui) { - await new Promise(r => requestAnimationFrame(r)) - ui.recalculateDetailPanels() - } + await setIframeViewport(iframe, width, height) channel.postMessage({ type: 'viewport:done', id }) break } @@ -143,7 +133,7 @@ client.ws.addEventListener('open', async () => { // so we only select it when the run is done if (ui && filenames.length > 1) { const id = generateFileId(filenames[filenames.length - 1]) - ui.setCurrentById(id) + ui.setCurrentFileId(id) } await done() } @@ -189,37 +179,26 @@ async function createTesters(testFiles: string[]) { container.className = 'scrolls' container.textContent = '' } + const { width, height } = config.browser.viewport if (config.isolate === false) { - createIframe( + const iframe = createIframe( container, ID_ALL, ) - const ui = getUiAPI() - - if (ui) { - await new Promise(r => requestAnimationFrame(r)) - ui.recalculateDetailPanels() - } + await setIframeViewport(iframe, width, height) } else { // otherwise, we need to wait for each iframe to finish before creating the next one // this is the most stable way to run tests in the browser for (const file of testFiles) { - const ui = getUiAPI() - - createIframe( + const iframe = createIframe( container, file, ) - if (ui) { - const id = generateFileId(file) - ui.setCurrentById(id) - await new Promise(r => requestAnimationFrame(r)) - ui.recalculateDetailPanels() - } + await setIframeViewport(iframe, width, height) await new Promise((resolve) => { channel.addEventListener('message', function handler(e: MessageEvent) { @@ -240,3 +219,14 @@ function generateFileId(file: string) { const path = relative(config.root, file) return generateHash(`${path}${project}`) } + +async function setIframeViewport(iframe: HTMLIFrameElement, width: number, height: number) { + const ui = getUiAPI() + if (ui) { + await ui.setIframeViewport(width, height) + } + else { + iframe.style.width = `${width}px` + iframe.style.height = `${height}px` + } +} diff --git a/packages/browser/src/client/ui.ts b/packages/browser/src/client/ui.ts index 213528dff761..c6a31506063d 100644 --- a/packages/browser/src/client/ui.ts +++ b/packages/browser/src/client/ui.ts @@ -1,13 +1,6 @@ -import type { File } from '@vitest/runner' +import type { BrowserUI } from 'vitest' -interface UiAPI { - currentModule: File - setCurrentById: (fileId: string) => void - resetDetailSizes: () => void - recalculateDetailPanels: () => void -} - -export function getUiAPI(): UiAPI | undefined { +export function getUiAPI(): BrowserUI | undefined { // @ts-expect-error not typed global return window.__vitest_ui_api__ } diff --git a/packages/browser/src/node/plugins/pluginContext.ts b/packages/browser/src/node/plugins/pluginContext.ts index 6eeee677358a..b9dfc4490b54 100644 --- a/packages/browser/src/node/plugins/pluginContext.ts +++ b/packages/browser/src/node/plugins/pluginContext.ts @@ -59,7 +59,7 @@ export const page = { viewport(width, height) { const id = __vitest_browser_runner__.iframeId channel.postMessage({ type: 'viewport', width, height, id }) - return new Promise((resolve) => { + return new Promise((resolve, reject) => { channel.addEventListener('message', function handler(e) { if (e.data.type === 'viewport:done' && e.data.id === id) { channel.removeEventListener('message', handler) diff --git a/packages/ui/client/auto-imports.d.ts b/packages/ui/client/auto-imports.d.ts index 0f38cb8410b9..af3b710de18a 100644 --- a/packages/ui/client/auto-imports.d.ts +++ b/packages/ui/client/auto-imports.d.ts @@ -35,6 +35,7 @@ declare global { const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] const currentModule: typeof import('./composables/navigation')['currentModule'] const customRef: typeof import('vue')['customRef'] + const customViewport: typeof import('./composables/browser')['customViewport'] const dashboardVisible: typeof import('./composables/navigation')['dashboardVisible'] const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] @@ -53,6 +54,7 @@ declare global { const filesSuccess: typeof import('./composables/summary')['filesSuccess'] const filesTodo: typeof import('./composables/summary')['filesTodo'] const finished: typeof import('./composables/summary')['finished'] + const getCurrentBrowserIframe: typeof import('./composables/api')['getCurrentBrowserIframe'] const getCurrentInstance: typeof import('vue')['getCurrentInstance'] const getCurrentScope: typeof import('vue')['getCurrentScope'] const getModuleGraph: typeof import('./composables/module-graph')['getModuleGraph'] @@ -76,6 +78,7 @@ declare global { const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onBrowserPanelResizing: typeof import('./composables/browser')['onBrowserPanelResizing'] const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] const onDeactivated: typeof import('vue')['onDeactivated'] const onErrorCaptured: typeof import('vue')['onErrorCaptured'] @@ -115,6 +118,7 @@ declare global { const resolveComponent: typeof import('vue')['resolveComponent'] const resolveRef: typeof import('@vueuse/core')['resolveRef'] const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] + const setIframeViewport: typeof import('./composables/api')['setIframeViewport'] const shallowReactive: typeof import('vue')['shallowReactive'] const shallowReadonly: typeof import('vue')['shallowReadonly'] const shallowRef: typeof import('vue')['shallowRef'] @@ -315,6 +319,7 @@ declare global { const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] const viewMode: typeof import('./composables/params')['viewMode'] + const viewport: typeof import('./composables/browser')['viewport'] const watch: typeof import('vue')['watch'] const watchArray: typeof import('@vueuse/core')['watchArray'] const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] diff --git a/packages/ui/client/components/BrowserIframe.vue b/packages/ui/client/components/BrowserIframe.vue index bc0bc4fc71f4..fad8eba679b8 100644 --- a/packages/ui/client/components/BrowserIframe.vue +++ b/packages/ui/client/components/BrowserIframe.vue @@ -1,41 +1,34 @@ @@ -68,6 +61,13 @@ async function changeViewport(name: ViewportSize) { border="b-2 base" > +
-
+
Select a test to run
diff --git a/packages/ui/client/composables/api.ts b/packages/ui/client/composables/api.ts new file mode 100644 index 000000000000..c69f868c6831 --- /dev/null +++ b/packages/ui/client/composables/api.ts @@ -0,0 +1,46 @@ +import type { BrowserUI } from 'vitest' +import { findById } from './client' +import { customViewport, viewport } from './browser' +import { detailSizes } from '~/composables/navigation' + +const ui: BrowserUI = { + setCurrentFileId(fileId: string) { + activeFileId.value = fileId + currentModule.value = findById(fileId) + showDashboard(false) + }, + async setIframeViewport(width: number, height: number) { + // reset the button before setting a custom viewport + viewport.value = 'custom' + customViewport.value = [width, height] + await setIframeViewport(width, height) + }, +} + +// @ts-expect-error not typed global +window.__vitest_ui_api__ = ui + +function recalculateDetailPanels() { + const iframe = getCurrentBrowserIframe() + 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] +} + +export function getCurrentBrowserIframe() { + return document.querySelector('#tester-ui iframe[data-vitest]')! +} + +export async function setIframeViewport(width: number | string, height: number | string) { + const iframe = getCurrentBrowserIframe() + // change the viewport of the iframe + iframe.style.width = typeof width === 'string' ? width : `${width}px` + iframe.style.height = typeof height === 'string' ? height : `${height}px` + // wait until it renders the new size and resize the panel to make the iframe visible + // this will not make it fully visible if viewport is too wide, but it's better than nothing + await new Promise(r => requestAnimationFrame(r)) + recalculateDetailPanels() +} diff --git a/packages/ui/client/composables/browser.ts b/packages/ui/client/composables/browser.ts index 92557449dbf0..5cfb3f75f5df 100644 --- a/packages/ui/client/composables/browser.ts +++ b/packages/ui/client/composables/browser.ts @@ -1,45 +1,11 @@ -import type { Ref } from 'vue' -import { detailSizes } from '~/composables/navigation' +export type ViewportSize = 'small-mobile' | 'large-mobile' | 'tablet' | 'full' | 'custom' +export const viewport = ref('full') +export const customViewport = ref<[number, number]>() -type ResizingListener = (isResizing: boolean) => void +export function onBrowserPanelResizing(isResizing: boolean) { + const tester = document.querySelector('#tester-ui') + if (!tester) + return -const resizingListeners = new Set() - -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] -} - -export function useResizing(testerRef: Ref) { - function onResizing(isResizing: boolean) { - const tester = testerRef.value - if (!tester) - return - - tester.style.pointerEvents = isResizing ? 'none' : '' - } - - onMounted(() => { - resizingListeners.add(onResizing) - }) - - onUnmounted(() => { - resizingListeners.delete(onResizing) - }) - - return { recalculateDetailPanels } -} - -export function useNotifyResizing() { - function notifyResizing(isResizing: boolean) { - for (const listener of resizingListeners) - listener(isResizing) - } - - return { notifyResizing } + tester.style.pointerEvents = isResizing ? 'none' : '' } diff --git a/packages/ui/client/composables/navigation.ts b/packages/ui/client/composables/navigation.ts index bf9a3dda0727..26ec838b5036 100644 --- a/packages/ui/client/composables/navigation.ts +++ b/packages/ui/client/composables/navigation.ts @@ -16,21 +16,6 @@ export const detailSizes = useLocalStorage<[left: number, right: number]>('vites initOnMounted: true, }) -// @ts-expect-error not typed global -window.__vitest_ui_api__ = { - get currentModule() { - return currentModule.value - }, - setCurrentById(fileId: string) { - activeFileId.value = fileId - currentModule.value = findById(fileId) - showDashboard(false) - }, - resetDetailSizes() { - detailSizes.value = [33, 67] - }, - recalculateDetailPanels, -} export const openedTreeItems = useLocalStorage('vitest-ui_task-tree-opened', []) // TODO // For html report preview, "coverage.reportsDirectory" must be explicitly set as a subdirectory of html report. diff --git a/packages/ui/client/pages/index.vue b/packages/ui/client/pages/index.vue index 806e264fc986..8a4f5f52fdba 100644 --- a/packages/ui/client/pages/index.vue +++ b/packages/ui/client/pages/index.vue @@ -3,9 +3,8 @@ import { Pane, Splitpanes } from 'splitpanes' import { browserState } from '~/composables/client'; import { coverageUrl, coverageVisible, initializeNavigation, detailSizes } from '~/composables/navigation' -import { useNotifyResizing } from '~/composables/browser' +import { onBrowserPanelResizing } from '~/composables/browser' -const { notifyResizing } = useNotifyResizing() const dashboardVisible = initializeNavigation() const mainSizes = useLocalStorage<[left: number, right: number]>('vitest-ui_splitpanes-mainSizes', [33, 67], { @@ -21,7 +20,7 @@ const onModuleResized = useDebounceFn((event: { size: number }[]) => { event.forEach((e, i) => { detailSizes.value[i] = e.size }) - notifyResizing(false) + onBrowserPanelResizing(false) }, 0) function resizeMain() { @@ -45,8 +44,8 @@ function resizeMain() { - - + + diff --git a/packages/vitest/src/types/index.ts b/packages/vitest/src/types/index.ts index 3da526a29867..1cb10341959c 100644 --- a/packages/vitest/src/types/index.ts +++ b/packages/vitest/src/types/index.ts @@ -24,6 +24,7 @@ export type { Mocked, MockedClass, } from '../integrations/spy' +export type { BrowserUI } from './ui' export type { ExpectStatic, diff --git a/packages/vitest/src/types/ui.ts b/packages/vitest/src/types/ui.ts new file mode 100644 index 000000000000..6e794c074267 --- /dev/null +++ b/packages/vitest/src/types/ui.ts @@ -0,0 +1,4 @@ +export interface BrowserUI { + setCurrentFileId: (fileId: string) => void + setIframeViewport: (width: number, height: number) => Promise +}