[READ-ONLY] Mirror of https://github.com/vitest-dev/vitest. Next generation testing framework powered by Vite. vitest.dev
test testing-tools vite
12

Configure Feed

Select the types of activity you want to include in your feed.

feat(browser): make iframe scalable, improve documentation (#6257)

authored by

Vladimir and committed by
GitHub
(Aug 5, 2024, 12:51 PM +0200) 74ca11a4 b77e14fc

+834 -410
+1
README.md
··· 37 37 - [Native code coverage](https://vitest.dev/guide/features.html#coverage) via [`v8`](https://v8.dev/blog/javascript-code-coverage) or [`istanbul`](https://istanbul.js.org/). 38 38 - [Tinyspy](https://github.com/tinylibs/tinyspy) built-in for mocking, stubbing, and spies. 39 39 - [JSDOM](https://github.com/jsdom/jsdom) and [happy-dom](https://github.com/capricorn86/happy-dom) for DOM and browser API mocking 40 + - [Browser Mode](https://vitest.dev/guide/browser/) for running component tests in the browser 40 41 - Components testing ([Vue](./examples/vue), [React](./examples/react), [Svelte](./examples/svelte), [Lit](./examples/lit), [Vitesse](./examples/vitesse), [Marko](https://github.com/marko-js/examples/tree/master/examples/library-ts)) 41 42 - Workers multi-threading via [Tinypool](https://github.com/tinylibs/tinypool) (a lightweight fork of [Piscina](https://github.com/piscinajs/piscina)) 42 43 - Benchmarking support with [Tinybench](https://github.com/tinylibs/tinybench)
+15 -1
pnpm-lock.yaml
··· 171 171 vitepress: 172 172 specifier: ^1.3.1 173 173 version: 1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4) 174 + vitepress-plugin-tabs: 175 + specifier: ^0.5.0 176 + version: 0.5.0(vitepress@1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4))(vue@3.4.35(typescript@5.5.4)) 174 177 workbox-window: 175 178 specifier: ^7.1.0 176 179 version: 7.1.0 ··· 8871 8874 vite: 8872 8875 optional: true 8873 8876 8877 + vitepress-plugin-tabs@0.5.0: 8878 + resolution: {integrity: sha512-SIhFWwGsUkTByfc2b279ray/E0Jt8vDTsM1LiHxmCOBAEMmvzIBZSuYYT1DpdDTiS3SuJieBheJkYnwCq/yD9A==} 8879 + peerDependencies: 8880 + vitepress: ^1.0.0-rc.27 8881 + vue: ^3.3.8 8882 + 8874 8883 vitepress@1.3.1: 8875 8884 resolution: {integrity: sha512-soZDpg2rRVJNIM/IYMNDPPr+zTHDA5RbLDHAxacRu+Q9iZ2GwSR0QSUlLs+aEZTkG0SOX1dc8RmUYwyuxK8dfQ==} 8876 8885 hasBin: true ··· 13758 13767 '@one-ini/wasm': 0.1.1 13759 13768 commander: 10.0.1 13760 13769 minimatch: 9.0.1 13761 - semver: 7.6.3 13770 + semver: 7.6.2 13762 13771 13763 13772 ee-first@1.1.1: {} 13764 13773 ··· 18059 18068 vitefu@0.2.5(vite@5.3.3(@types/node@20.14.14)(terser@5.22.0)): 18060 18069 optionalDependencies: 18061 18070 vite: 5.3.3(@types/node@20.14.14)(terser@5.22.0) 18071 + 18072 + vitepress-plugin-tabs@0.5.0(vitepress@1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4))(vue@3.4.35(typescript@5.5.4)): 18073 + dependencies: 18074 + vitepress: 1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4) 18075 + vue: 3.4.35(typescript@5.5.4) 18062 18076 18063 18077 vitepress@1.3.1(@algolia/client-search@4.20.0)(@types/node@20.14.14)(@types/react@18.2.79)(postcss@8.4.40)(react-dom@18.0.0(react@18.2.0))(react@18.2.0)(search-insights@2.9.0)(terser@5.22.0)(typescript@5.5.4): 18064 18078 dependencies:
+1
docs/package.json
··· 32 32 "vite": "^5.2.8", 33 33 "vite-plugin-pwa": "^0.20.1", 34 34 "vitepress": "^1.3.1", 35 + "vitepress-plugin-tabs": "^0.5.0", 35 36 "workbox-window": "^7.1.0" 36 37 } 37 38 }
+49 -59
docs/.vitepress/config.ts
··· 1 1 import { defineConfig } from 'vitepress' 2 2 import { withPwa } from '@vite-pwa/vitepress' 3 3 import { transformerTwoslash } from '@shikijs/vitepress-twoslash' 4 + import { tabsMarkdownPlugin } from 'vitepress-plugin-tabs' 4 5 import { version } from '../../package.json' 5 6 import { 6 7 contributing, ··· 56 57 ], 57 58 lastUpdated: true, 58 59 markdown: { 60 + config(md) { 61 + md.use(tabsMarkdownPlugin) 62 + }, 59 63 theme: { 60 64 light: 'github-light', 61 65 dark: 'github-dark', 62 66 }, 63 67 codeTransformers: mode === 'development' 64 68 ? [] 65 - : [transformerTwoslash({ 66 - processHoverInfo: (info) => { 67 - if (info.includes(process.cwd())) { 68 - return info.replace(new RegExp(process.cwd(), 'g'), '') 69 - } 70 - return info 71 - }, 72 - })], 69 + : [ 70 + transformerTwoslash({ 71 + processHoverInfo: (info) => { 72 + if (info.includes(process.cwd())) { 73 + return info.replace(new RegExp(process.cwd(), 'g'), '') 74 + } 75 + return info 76 + }, 77 + }), 78 + ], 73 79 }, 74 80 themeConfig: { 75 81 logo: '/logo.svg', ··· 105 111 }, 106 112 107 113 nav: [ 108 - { text: 'Guide', link: '/guide/', activeMatch: '^/guide/' }, 114 + { text: 'Guide', link: '/guide/', activeMatch: '^/guide/(?!browser)' }, 109 115 { text: 'API', link: '/api/', activeMatch: '^/api/' }, 110 116 { text: 'Config', link: '/config/', activeMatch: '^/config/' }, 111 - { text: 'Advanced', link: '/advanced/api', activeMatch: '^/advanced/' }, 117 + { text: 'Browser Mode', link: '/guide/browser', activeMatch: '^/guide/browser/' }, 112 118 { 113 119 text: 'Resources', 114 120 items: [ 115 121 { 116 - text: 'Team', 117 - link: '/team', 122 + text: 'Advanced', 123 + link: '/advanced/api', 124 + activeMatch: '^/advanced/', 118 125 }, 119 126 { 120 - items: [ 121 - { 122 - text: 'Mastodon', 123 - link: mastodon, 124 - }, 125 - { 126 - text: 'X (formerly Twitter)', 127 - link: twitter, 128 - }, 129 - { 130 - text: 'Discord Chat', 131 - link: discord, 132 - }, 133 - ], 127 + text: 'Team', 128 + link: '/team', 134 129 }, 135 130 ], 136 131 }, ··· 174 169 ], 175 170 176 171 sidebar: { 177 - // TODO: bring sidebar of apis and config back 172 + '/guide/browser': [ 173 + { 174 + text: 'Why Browser Mode?', 175 + link: '/guide/browser/why', 176 + docFooterText: 'Why Browser Mode? | Browser Mode', 177 + }, 178 + { 179 + text: 'Context API', 180 + link: '/guide/browser/context', 181 + docFooterText: 'Context API | Browser Mode', 182 + }, 183 + { 184 + text: 'Interactivity API', 185 + link: '/guide/browser/interactivity-api', 186 + docFooterText: 'Interactivity API | Browser Mode', 187 + }, 188 + { 189 + text: 'Assertion API', 190 + link: '/guide/browser/assertion-api', 191 + docFooterText: 'Assertion API | Browser Mode', 192 + }, 193 + { 194 + text: 'Commands API', 195 + link: '/guide/browser/commands', 196 + docFooterText: 'Commands | Browser Mode', 197 + }, 198 + ], 199 + // TODO: bring sidebar of apis and config back 178 200 '/advanced': [ 179 201 { 180 202 items: [ ··· 251 273 { 252 274 text: 'Vitest UI', 253 275 link: '/guide/ui', 254 - }, 255 - { 256 - text: 'Browser Mode', 257 - link: '/guide/browser/', 258 - collapsed: false, 259 - items: [ 260 - { 261 - text: 'Context', 262 - link: '/guide/browser/context', 263 - docFooterText: 'Context | Browser Mode', 264 - }, 265 - { 266 - text: 'Interactivity API', 267 - link: '/guide/browser/interactivity-api', 268 - docFooterText: 'Interactivity API | Browser Mode', 269 - }, 270 - { 271 - text: 'Assertion API', 272 - link: '/guide/browser/assertion-api', 273 - docFooterText: 'Assertion API | Browser Mode', 274 - }, 275 - { 276 - text: 'Commands', 277 - link: '/guide/browser/commands', 278 - docFooterText: 'Commands | Browser Mode', 279 - }, 280 - { 281 - text: 'Examples', 282 - link: '/guide/browser/examples', 283 - docFooterText: 'Examples | Browser Mode', 284 - }, 285 - ], 286 276 }, 287 277 { 288 278 text: 'In-Source Testing',
+2 -2
docs/guide/coverage.md
··· 230 230 <img alt="html coverage activation in Vitest UI" img-light src="/vitest-ui-show-coverage-light.png"> 231 231 <img alt="html coverage activation in Vitest UI" img-dark src="/vitest-ui-show-coverage-dark.png"> 232 232 233 - <img alt="html coverage in Vitest UI" img-light src="/vitest-ui-coverage-light.png"> 234 - <img alt="html coverage in Vitest UI" img-dark src="/vitest-ui-coverage-dark.png"> 233 + <img alt="html coverage in Vitest UI" img-light src="/ui-coverage-1-light.png"> 234 + <img alt="html coverage in Vitest UI" img-dark src="/ui-coverage-1-dark.png">
+6
docs/guide/environment.md
··· 19 19 Since Vitest 2.0.4 the `require` of CSS and assets inside the external dependencies are resolved automatically. 20 20 ::: 21 21 22 + ::: warning 23 + "Environments" exist only when running tests in Node.js. 24 + 25 + `browser` is not considered an environment in Vitest. If you wish to run part of your tests using [Browser Mode](/guide/browser/), you can create a [workspace project](/guide/browser/#workspace-config). 26 + ::: 27 + 22 28 ## Environments for Specific Files 23 29 24 30 When setting `environment` option in your config, it will apply to all the test files in your project. To have more fine-grained control, you can use control comments to specify environment for specific files. Control comments are comments that start with `@vitest-environment` and are followed by the environment name:
+2 -2
docs/guide/ui.md
··· 18 18 19 19 Then you can visit the Vitest UI at <a href="http://localhost:51204/__vitest__/">`http://localhost:51204/__vitest__/`</a> 20 20 21 - <img alt="Vitest UI" img-light src="https://user-images.githubusercontent.com/11247099/171992267-5cae2fa0-b927-400a-8eb1-da776974cb61.png"> 22 - <img alt="Vitest UI" img-dark src="https://user-images.githubusercontent.com/11247099/171992272-7c6057e2-80c3-4b17-a7b6-0ac28e5a5e0b.png"> 21 + <img alt="Vitest UI" img-light src="/ui-1-light.png"> 22 + <img alt="Vitest UI" img-dark src="/ui-1-dark.png"> 23 23 24 24 UI can also be used as a reporter. Use `'html'` reporter in your Vitest configuration to generate HTML output and preview the results of your tests: 25 25
+23 -2
docs/guide/workspace.md
··· 141 141 142 142 If you need to run tests only inside a single project, use the `--project` CLI option: 143 143 144 - ```bash 144 + ::: code-group 145 + ```bash [npm] 145 146 npm run test --project e2e 146 147 ``` 148 + ```bash [yarn] 149 + yarn test --project e2e 150 + ``` 151 + ```bash [pnpm] 152 + pnpm run test --project e2e 153 + ``` 154 + ```bash [bun] 155 + bun test --project e2e 156 + ``` 157 + ::: 147 158 148 159 ::: tip 149 160 CLI option `--project` can be used multiple times to filter out several projects: 150 161 151 - ```bash 162 + ::: code-group 163 + ```bash [npm] 152 164 npm run test --project e2e --project unit 165 + ``` 166 + ```bash [yarn] 167 + yarn test --project e2e --project unit 168 + ``` 169 + ```bash [pnpm] 170 + pnpm run test --project e2e --project unit 171 + ``` 172 + ```bash [bun] 173 + bun test --project e2e --project unit 153 174 ``` 154 175 ::: 155 176
docs/public/ui-1-dark.png

This is a binary file and will not be displayed.

docs/public/ui-1-light.png

This is a binary file and will not be displayed.

docs/public/ui-browser-1-dark.png

This is a binary file and will not be displayed.

docs/public/ui-browser-1-light.png

This is a binary file and will not be displayed.

docs/public/ui-coverage-1-dark.png

This is a binary file and will not be displayed.

docs/public/ui-coverage-1-light.png

This is a binary file and will not be displayed.

docs/public/vitest-ui-coverage-dark.png

This is a binary file and will not be displayed.

docs/public/vitest-ui-coverage-light.png

This is a binary file and will not be displayed.

+2 -1
packages/ui/index.html
··· 2 2 <html lang="en"> 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 - <link rel="icon" href="/favicon.svg" type="image/svg+xml" /> 5 + <link rel="icon" href="/favicon.ico" sizes="48x48"> 6 + <link rel="icon" href="/favicon.svg" sizes="any" type="image/svg+xml"> 6 7 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 8 <title>Vitest</title> 8 9 <link rel="preconnect" href="https://fonts.googleapis.com" />
+1
packages/ui/vite.config.ts
··· 53 53 transformers: [ 54 54 transformerDirectives(), 55 55 ], 56 + safelist: 'absolute origin-top mt-[8px]'.split(' '), 56 57 }), 57 58 Components({ 58 59 dirs: ['client/components'],
-1
packages/vitest/package.json
··· 17 17 }, 18 18 "keywords": [ 19 19 "vite", 20 - "vite-node", 21 20 "vitest", 22 21 "test", 23 22 "jest"
+1
docs/.vitepress/components/FeaturesList.vue
··· 22 22 <ListItem><a target="_blank" href="https://www.chaijs.com/" rel="noopener noreferrer">Chai</a> built-in for assertions + <a target="_blank" href="https://jestjs.io/docs/expect" rel="noopener noreferrer">Jest expect</a> compatible APIs</ListItem> 23 23 <ListItem><a target="_blank" href="https://github.com/tinylibs/tinyspy" rel="noopener noreferrer">Tinyspy</a> built-in for mocking</ListItem> 24 24 <ListItem><a target="_blank" href="https://github.com/capricorn86/happy-dom" rel="noopener noreferrer">happy-dom</a> or <a target="_blank" href="https://github.com/jsdom/jsdom" rel="noopener noreferrer">jsdom</a> for DOM mocking</ListItem> 25 + <ListItem><a href="https://vitest.dev/guide/browser/" rel="noopener noreferrer">Browser Mode</a> for running component tests in the browser</ListItem> 25 26 <ListItem>Code coverage via <a target="_blank" href="https://v8.dev/blog/javascript-code-coverage" rel="noopener noreferrer">v8</a> or <a target="_blank" href="https://istanbul.js.org/" rel="noopener noreferrer">istanbul</a></ListItem> 26 27 <ListItem>Rust-like <a href="/guide/in-source">in-source testing</a></ListItem> 27 28 <ListItem>Type Testing via <a target="_blank" href="https://github.com/mmkal/expect-type" rel="noopener noreferrer">expect-type</a></ListItem>
+2
docs/.vitepress/theme/index.ts
··· 6 6 import '../style/vars.css' 7 7 import 'uno.css' 8 8 import TwoslashFloatingVue from '@shikijs/vitepress-twoslash/client' 9 + import { enhanceAppWithTabs } from 'vitepress-plugin-tabs/client' 9 10 import HomePage from '../components/HomePage.vue' 10 11 import Version from '../components/Version.vue' 11 12 import '@shikijs/vitepress-twoslash/style.css' ··· 24 25 enhanceApp({ app }) { 25 26 app.component('Version', Version) 26 27 app.use(TwoslashFloatingVue) 28 + enhanceAppWithTabs(app) 27 29 }, 28 30 } satisfies Theme
+1 -1
docs/guide/browser/assertion-api.md
··· 4 4 5 5 # Assertion API 6 6 7 - Vitest bundles [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. For detailed documentation, you can read the `jest-dom` readme: 7 + Vitest bundles the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. For detailed documentation, you can read the `jest-dom` readme: 8 8 9 9 - [`toBeDisabled`](https://github.com/testing-library/jest-dom#toBeDisabled) 10 10 - [`toBeEnabled`](https://github.com/testing-library/jest-dom#toBeEnabled)
+6 -4
docs/guide/browser/context.md
··· 1 1 --- 2 - title: Context | Browser Mode 2 + title: Context API | Browser Mode 3 3 --- 4 4 5 - # Context 5 + # Context API 6 6 7 7 Vitest exposes a context module via `@vitest/browser/context` entry point. As of 2.0, it exposes a small set of utilities that might be useful to you in tests. 8 8 ··· 42 42 ## `commands` 43 43 44 44 ::: tip 45 - Commands API is explained in detail at [Commands](/guide/browser/commands). 45 + This API is explained in detail at [Commands API](/guide/browser/commands). 46 46 ::: 47 47 48 48 ```ts ··· 59 59 60 60 ::: warning 61 61 While it exposes some utilities from Playwright's `page`, it is not the same object. Since the browser context is evaluated in the browser, your tests don't have access to Playwright's `page` because it runs on the server. 62 + 63 + Use [Commands API](/guide/browser/commands) if you need to have access to Playwright's `page` object. 62 64 ::: 63 65 64 66 ```ts ··· 93 95 94 96 ## `server` 95 97 96 - The `server` export represents the Node.js environment where the Vitest server is running. It is mostly useful for debugging. 98 + The `server` export represents the Node.js environment where the Vitest server is running. It is mostly useful for debugging or limiting your tests based on the environment. 97 99 98 100 ```ts 99 101 export const server: {
-158
docs/guide/browser/examples.md
··· 1 - --- 2 - title: Examples | Browser Mode 3 - --- 4 - 5 - # Examples 6 - 7 - Browser Mode is framework agnostic so it doesn't provide any method to render your components. However, you should be able to use your framework's test utils packages. 8 - 9 - We recommend using `testing-library` packages depending on your framework: 10 - 11 - - [`@testing-library/dom`](https://testing-library.com/docs/dom-testing-library/intro) if you don't use a framework 12 - - [`@testing-library/vue`](https://testing-library.com/docs/vue-testing-library/intro) to render [vue](https://vuejs.org) components 13 - - [`@testing-library/svelte`](https://testing-library.com/docs/svelte-testing-library/intro) to render [svelte](https://svelte.dev) components 14 - - [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro) to render [react](https://react.dev) components 15 - - [`@testing-library/preact`](https://testing-library.com/docs/preact-testing-library/intro) to render [preact](https://preactjs.com) components 16 - - [`solid-testing-library`](https://testing-library.com/docs/solid-testing-library/intro) to render [solid](https://www.solidjs.com) components 17 - - [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) to render [marko](https://markojs.com) components 18 - 19 - ::: warning 20 - `testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](#interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood. 21 - ::: 22 - 23 - ::: code-group 24 - ```ts [vue] 25 - // based on @testing-library/vue example 26 - // https://testing-library.com/docs/vue-testing-library/examples 27 - 28 - import { userEvent } from '@vitest/browser/context' 29 - import { render, screen } from '@testing-library/vue' 30 - import Component from './Component.vue' 31 - 32 - test('properly handles v-model', async () => { 33 - render(Component) 34 - 35 - // Asserts initial state. 36 - expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument() 37 - 38 - // Get the input DOM node by querying the associated label. 39 - const usernameInput = await screen.findByLabelText(/username/i) 40 - 41 - // Type the name into the input. This already validates that the input 42 - // is filled correctly, no need to check the value manually. 43 - await userEvent.fill(usernameInput, 'Bob') 44 - 45 - expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument() 46 - }) 47 - ``` 48 - ```ts [svelte] 49 - // based on @testing-library/svelte 50 - // https://testing-library.com/docs/svelte-testing-library/example 51 - 52 - import { render, screen } from '@testing-library/svelte' 53 - import { userEvent } from '@vitest/browser/context' 54 - import { expect, test } from 'vitest' 55 - 56 - import Greeter from './greeter.svelte' 57 - 58 - test('greeting appears on click', async () => { 59 - const user = userEvent.setup() 60 - render(Greeter, { name: 'World' }) 61 - 62 - const button = screen.getByRole('button') 63 - await user.click(button) 64 - const greeting = await screen.findByText(/hello world/iu) 65 - 66 - expect(greeting).toBeInTheDocument() 67 - }) 68 - ``` 69 - ```tsx [react] 70 - // based on @testing-library/react example 71 - // https://testing-library.com/docs/react-testing-library/example-intro 72 - 73 - import { userEvent } from '@vitest/browser/context' 74 - import { render, screen } from '@testing-library/react' 75 - import Fetch from './fetch' 76 - 77 - test('loads and displays greeting', async () => { 78 - // Render a React element into the DOM 79 - render(<Fetch url="/greeting" />) 80 - 81 - await userEvent.click(screen.getByText('Load Greeting')) 82 - // wait before throwing an error if it cannot find an element 83 - const heading = await screen.findByRole('heading') 84 - 85 - // assert that the alert message is correct 86 - expect(heading).toHaveTextContent('hello there') 87 - expect(screen.getByRole('button')).toBeDisabled() 88 - }) 89 - ``` 90 - ```tsx [preact] 91 - // based on @testing-library/preact example 92 - // https://testing-library.com/docs/preact-testing-library/example 93 - 94 - import { h } from 'preact' 95 - import { userEvent } from '@vitest/browser/context' 96 - import { render } from '@testing-library/preact' 97 - 98 - import HiddenMessage from '../hidden-message' 99 - 100 - test('shows the children when the checkbox is checked', async () => { 101 - const testMessage = 'Test Message' 102 - 103 - const { queryByText, getByLabelText, getByText } = render( 104 - <HiddenMessage>{testMessage}</HiddenMessage>, 105 - ) 106 - 107 - // query* functions will return the element or null if it cannot be found. 108 - // get* functions will return the element or throw an error if it cannot be found. 109 - expect(queryByText(testMessage)).not.toBeInTheDocument() 110 - 111 - // The queries can accept a regex to make your selectors more 112 - // resilient to content tweaks and changes. 113 - await userEvent.click(getByLabelText(/show/i)) 114 - 115 - expect(getByText(testMessage)).toBeInTheDocument() 116 - }) 117 - ``` 118 - ```tsx [solid] 119 - // baed on @testing-library/solid API 120 - // https://testing-library.com/docs/solid-testing-library/api 121 - 122 - import { render } from '@testing-library/solid' 123 - 124 - it('uses params', async () => { 125 - const App = () => ( 126 - <> 127 - <Route 128 - path="/ids/:id" 129 - component={() => ( 130 - <p> 131 - Id: 132 - {useParams()?.id} 133 - </p> 134 - )} 135 - /> 136 - <Route path="/" component={() => <p>Start</p>} /> 137 - </> 138 - ) 139 - const { findByText } = render(() => <App />, { location: 'ids/1234' }) 140 - expect(await findByText('Id: 1234')).toBeInTheDocument() 141 - }) 142 - ``` 143 - ```ts [marko] 144 - // baed on @testing-library/marko API 145 - // https://testing-library.com/docs/marko-testing-library/api 146 - 147 - import { render, screen } from '@marko/testing-library' 148 - import Greeting from './greeting.marko' 149 - 150 - test('renders a message', async () => { 151 - const { container } = await render(Greeting, { name: 'Marko' }) 152 - expect(screen.getByText(/Marko/)).toBeInTheDocument() 153 - expect(container.firstChild).toMatchInlineSnapshot(` 154 - <h1>Hello, Marko!</h1> 155 - `) 156 - }) 157 - ``` 158 - :::
+234 -33
docs/guide/browser/index.md
··· 7 7 8 8 This page provides information about the experimental browser mode feature in the Vitest API, which allows you to run your tests in the browser natively, providing access to browser globals like window and document. This feature is currently under development, and APIs may change in the future. 9 9 10 + <img alt="Vitest UI" img-light src="/ui-browser-1-light.png"> 11 + <img alt="Vitest UI" img-dark src="/ui-browser-1-dark.png"> 12 + 10 13 ## Installation 11 14 12 15 For easier setup, you can use `vitest init browser` command to install required dependencies and create browser configuration. ··· 49 52 However, to run tests in CI you need to install either [`playwright`](https://npmjs.com/package/playwright) or [`webdriverio`](https://www.npmjs.com/package/webdriverio). We also recommend switching to either one of them for testing locally instead of using the default `preview` provider since it relies on simulating events instead of using Chrome DevTools Protocol. 50 53 51 54 If you don't already use one of these tools, we recommend starting with Playwright because it supports parallel execution, which makes your tests run faster. Additionally, the Chrome DevTools Protocol that Playwright uses is generally faster than WebDriver. 52 - ::: 53 55 54 - ### Using Playwright 55 - 56 + ::: tabs key:provider 57 + == Playwright 56 58 [Playwright](https://npmjs.com/package/playwright) is a framework for Web Testing and Automation. 57 59 58 60 ::: code-group ··· 68 70 ```bash [bun] 69 71 bun add -D vitest @vitest/browser playwright 70 72 ``` 71 - ::: 72 - 73 - ### Using Webdriverio 73 + == WebdriverIO 74 74 75 75 [WebdriverIO](https://www.npmjs.com/package/webdriverio) allows you to run tests locally using the WebDriver protocol. 76 76 ··· 176 176 177 177 If you need to run some tests using Node-based runner, you can define a [workspace](/guide/workspace) file with separate configurations for different testing strategies: 178 178 179 + {#workspace-config} 180 + 179 181 ```ts 180 182 // vitest.workspace.ts 181 183 import { defineWorkspace } from 'vitest/config' ··· 211 213 ]) 212 214 ``` 213 215 216 + ### Provider Configuration 217 + 218 + :::tabs key:provider 219 + == Playwright 220 + You can configure how Vitest [launches the browser](https://playwright.dev/docs/api/class-browsertype#browser-type-launch) and creates the [page context](https://playwright.dev/docs/api/class-browsercontext) via [`providerOptions`](/config/#browser-provideroptions) field: 221 + 222 + ```ts 223 + export default defineConfig({ 224 + test: { 225 + browser: { 226 + providerOptions: { 227 + launch: { 228 + devtools: true, 229 + }, 230 + context: { 231 + geolocation: { 232 + latitude: 45, 233 + longitude: -30, 234 + }, 235 + reducedMotion: 'reduce', 236 + }, 237 + }, 238 + }, 239 + }, 240 + }) 241 + ``` 242 + 243 + To have type hints, add `@vitest/browser/providers/playwright` to `compilerOptions.types` in your `tsconfig.json` file. 244 + == WebdriverIO 245 + You can configure what [options](https://webdriver.io/docs/configuration#webdriverio) Vitest should use when starting a browser via [`providerOptions`](/config/#browser-provideroptions) field: 246 + 247 + ```ts 248 + export default defineConfig({ 249 + test: { 250 + browser: { 251 + browser: 'chrome', 252 + providerOptions: { 253 + region: 'eu', 254 + capabilities: { 255 + browserVersion: '27.0', 256 + platformName: 'Windows 10', 257 + }, 258 + }, 259 + }, 260 + }, 261 + }) 262 + ``` 263 + 264 + To have type hints, add `@vitest/browser/providers/webdriverio` to `compilerOptions.types` in your `tsconfig.json` file. 265 + ::: 266 + 214 267 ## Browser Option Types 215 268 216 269 The browser option in Vitest depends on the provider. Vitest will fail, if you pass `--browser` and don't specify its name in the config file. Available options: ··· 236 289 - Safari >=15.4 237 290 - Edge >=88 238 291 239 - ## Motivation 240 - 241 - We developed the Vitest browser mode feature to help improve testing workflows and achieve more accurate and reliable test results. This experimental addition to our testing API allows developers to run tests in a native browser environment. In this section, we'll explore the motivations behind this feature and its benefits for testing. 242 - 243 - ### Different Ways of Testing 244 - 245 - There are different ways to test JavaScript code. Some testing frameworks simulate browser environments in Node.js, while others run tests in real browsers. In this context, [jsdom](https://www.npmjs.com/package/jsdom) is an example of a spec implementation that simulates a browser environment by being used with a test runner like Jest or Vitest, while other testing tools such as [WebdriverIO](https://webdriver.io/) or [Cypress](https://www.cypress.io/) allow developers to test their applications in a real browser or in case of [Playwright](https://playwright.dev/) provide you a browser engine. 246 - 247 - ### The Simulation Caveat 248 - 249 - Testing JavaScript programs in simulated environments such as jsdom or happy-dom has simplified the test setup and provided an easy-to-use API, making them suitable for many projects and increasing confidence in test results. However, it is crucial to keep in mind that these tools only simulate a browser environment and not an actual browser, which may result in some discrepancies between the simulated environment and the real environment. Therefore, false positives or negatives in test results may occur. 250 - 251 - To achieve the highest level of confidence in our tests, it's crucial to test in a real browser environment. This is why we developed the browser mode feature in Vitest, allowing developers to run tests natively in a browser and gain more accurate and reliable test results. With browser-level testing, developers can be more confident that their application will work as intended in a real-world scenario. 252 - 253 - ## Drawbacks 254 - 255 - When using Vitest browser, it is important to consider the following drawbacks: 256 - 257 - ### Early Development 258 - 259 - The browser mode feature of Vitest is still in its early stages of development. As such, it may not yet be fully optimized, and there may be some bugs or issues that have not yet been ironed out. It is recommended that users augment their Vitest browser experience with a standalone browser-side test runner like WebdriverIO, Cypress or Playwright. 260 - 261 - ### Longer Initialization 262 - 263 - Vitest browser requires spinning up the provider and the browser during the initialization process, which can take some time. This can result in longer initialization times compared to other testing patterns. 264 - 265 - ## Cross-Browser Testing 292 + ## Running Tests 266 293 267 294 When you specify a browser name in the browser option, Vitest will try to run the specified browser using `preview` by default, and then run the tests there. If you don't want to use `preview`, you can configure the custom browser provider by using `browser.provider` option. 268 295 ··· 278 305 npx vitest --browser.name=chrome --browser.headless 279 306 ``` 280 307 308 + By default, Vitest will automatically open the browser UI for development. Your tests will run inside an iframe in the center. You can configure the viewport by selecting the preferred dimensions, calling `page.viewport` inside the test, or setting default values in [the config](/config/#browser-viewport). 309 + 281 310 ## Headless 282 311 283 312 Headless mode is another option available in the browser mode. In headless mode, the browser runs in the background without a user interface, which makes it useful for running automated tests. The headless option in Vitest can be set to a boolean value to enable or disable headless mode. 313 + 314 + When using headless mode, Vitest won't open the UI automatically. If you want to continue using the UI but have tests run headlessly, you can install the [`@vitest/ui`](/guide/ui) package and pass the --ui flag when running Vitest. 284 315 285 316 Here's an example configuration enabling headless mode: 286 317 ··· 306 337 307 338 ::: warning 308 339 Headless mode is not available by default. You need to use either [`playwright`](https://npmjs.com/package/playwright) or [`webdriverio`](https://www.npmjs.com/package/webdriverio) providers to enable this feature. 340 + ::: 341 + 342 + ## Examples 343 + 344 + Browser Mode is framework agnostic so it doesn't provide any method to render your components. However, you should be able to use your framework's test utils packages. 345 + 346 + We recommend using `testing-library` packages depending on your framework: 347 + 348 + - [`@testing-library/dom`](https://testing-library.com/docs/dom-testing-library/intro) if you don't use a framework 349 + - [`@testing-library/vue`](https://testing-library.com/docs/vue-testing-library/intro) to render [vue](https://vuejs.org) components 350 + - [`@testing-library/svelte`](https://testing-library.com/docs/svelte-testing-library/intro) to render [svelte](https://svelte.dev) components 351 + - [`@testing-library/react`](https://testing-library.com/docs/react-testing-library/intro) to render [react](https://react.dev) components 352 + - [`@testing-library/preact`](https://testing-library.com/docs/preact-testing-library/intro) to render [preact](https://preactjs.com) components 353 + - [`solid-testing-library`](https://testing-library.com/docs/solid-testing-library/intro) to render [solid](https://www.solidjs.com) components 354 + - [`@marko/testing-library`](https://testing-library.com/docs/marko-testing-library/intro) to render [marko](https://markojs.com) components 355 + 356 + Besides rendering components and querying elements using `@testing-library/your-framework`, you will also need to make assertions. Vitest bundles the [`@testing-library/jest-dom`](https://github.com/testing-library/jest-dom) library to provide a wide range of DOM assertions out of the box. Read more at the [Assertions API](/guide/browser/assertion-api). 357 + 358 + ```ts 359 + import { expect } from 'vitest' 360 + // element is rendered correctly 361 + await expect.element(screen.getByText('Hello World')).toBeInTheDocument() 362 + ``` 363 + 364 + Vitest exposes a [Context API](/guide/browser/context) with a small set of utilities that might be useful to you in tests. For example, if you need to make an interaction, like clicking an element or typing text into an input, you can use `userEvent` from `@vitest/browser/context`. Read more at the [Interactivity API](/guide/browser/interactivity-api). 365 + 366 + ```ts 367 + import { userEvent } from '@vitest/browser/context' 368 + await userEvent.type(screen.getByLabelText(/username/i), 'Alice') 369 + ``` 370 + 371 + ::: warning 372 + `testing-library` provides a package `@testing-library/user-event`. We do not recommend using it directly because it simulates events instead of actually triggering them - instead, use [`userEvent`](#interactivity-api) imported from `@vitest/browser/context` that uses Chrome DevTools Protocol or Webdriver (depending on the provider) under the hood. 373 + ::: 374 + 375 + ::: code-group 376 + ```ts [vue] 377 + // based on @testing-library/vue example 378 + // https://testing-library.com/docs/vue-testing-library/examples 379 + 380 + import { userEvent } from '@vitest/browser/context' 381 + import { render, screen } from '@testing-library/vue' 382 + import Component from './Component.vue' 383 + 384 + test('properly handles v-model', async () => { 385 + render(Component) 386 + 387 + // Asserts initial state. 388 + expect(screen.getByText('Hi, my name is Alice')).toBeInTheDocument() 389 + 390 + // Get the input DOM node by querying the associated label. 391 + const usernameInput = await screen.findByLabelText(/username/i) 392 + 393 + // Type the name into the input. This already validates that the input 394 + // is filled correctly, no need to check the value manually. 395 + await userEvent.fill(usernameInput, 'Bob') 396 + 397 + expect(screen.getByText('Hi, my name is Bob')).toBeInTheDocument() 398 + }) 399 + ``` 400 + ```ts [svelte] 401 + // based on @testing-library/svelte 402 + // https://testing-library.com/docs/svelte-testing-library/example 403 + 404 + import { render, screen } from '@testing-library/svelte' 405 + import { userEvent } from '@vitest/browser/context' 406 + import { expect, test } from 'vitest' 407 + 408 + import Greeter from './greeter.svelte' 409 + 410 + test('greeting appears on click', async () => { 411 + const user = userEvent.setup() 412 + render(Greeter, { name: 'World' }) 413 + 414 + const button = screen.getByRole('button') 415 + await user.click(button) 416 + const greeting = await screen.findByText(/hello world/iu) 417 + 418 + expect(greeting).toBeInTheDocument() 419 + }) 420 + ``` 421 + ```tsx [react] 422 + // based on @testing-library/react example 423 + // https://testing-library.com/docs/react-testing-library/example-intro 424 + 425 + import { userEvent } from '@vitest/browser/context' 426 + import { render, screen } from '@testing-library/react' 427 + import Fetch from './fetch' 428 + 429 + test('loads and displays greeting', async () => { 430 + // Render a React element into the DOM 431 + render(<Fetch url="/greeting" />) 432 + 433 + await userEvent.click(screen.getByText('Load Greeting')) 434 + // wait before throwing an error if it cannot find an element 435 + const heading = await screen.findByRole('heading') 436 + 437 + // assert that the alert message is correct 438 + expect(heading).toHaveTextContent('hello there') 439 + expect(screen.getByRole('button')).toBeDisabled() 440 + }) 441 + ``` 442 + ```tsx [preact] 443 + // based on @testing-library/preact example 444 + // https://testing-library.com/docs/preact-testing-library/example 445 + 446 + import { h } from 'preact' 447 + import { userEvent } from '@vitest/browser/context' 448 + import { render } from '@testing-library/preact' 449 + 450 + import HiddenMessage from '../hidden-message' 451 + 452 + test('shows the children when the checkbox is checked', async () => { 453 + const testMessage = 'Test Message' 454 + 455 + const { queryByText, getByLabelText, getByText } = render( 456 + <HiddenMessage>{testMessage}</HiddenMessage>, 457 + ) 458 + 459 + // query* functions will return the element or null if it cannot be found. 460 + // get* functions will return the element or throw an error if it cannot be found. 461 + expect(queryByText(testMessage)).not.toBeInTheDocument() 462 + 463 + // The queries can accept a regex to make your selectors more 464 + // resilient to content tweaks and changes. 465 + await userEvent.click(getByLabelText(/show/i)) 466 + 467 + expect(getByText(testMessage)).toBeInTheDocument() 468 + }) 469 + ``` 470 + ```tsx [solid] 471 + // baed on @testing-library/solid API 472 + // https://testing-library.com/docs/solid-testing-library/api 473 + 474 + import { render } from '@testing-library/solid' 475 + 476 + it('uses params', async () => { 477 + const App = () => ( 478 + <> 479 + <Route 480 + path="/ids/:id" 481 + component={() => ( 482 + <p> 483 + Id: 484 + {useParams()?.id} 485 + </p> 486 + )} 487 + /> 488 + <Route path="/" component={() => <p>Start</p>} /> 489 + </> 490 + ) 491 + const { findByText } = render(() => <App />, { location: 'ids/1234' }) 492 + expect(await findByText('Id: 1234')).toBeInTheDocument() 493 + }) 494 + ``` 495 + ```ts [marko] 496 + // baed on @testing-library/marko API 497 + // https://testing-library.com/docs/marko-testing-library/api 498 + 499 + import { render, screen } from '@marko/testing-library' 500 + import Greeting from './greeting.marko' 501 + 502 + test('renders a message', async () => { 503 + const { container } = await render(Greeting, { name: 'Marko' }) 504 + expect(screen.getByText(/Marko/)).toBeInTheDocument() 505 + expect(container.firstChild).toMatchInlineSnapshot(` 506 + <h1>Hello, Marko!</h1> 507 + `) 508 + }) 509 + ``` 309 510 ::: 310 511 311 512 ## Limitations
+18 -8
docs/guide/browser/interactivity-api.md
··· 4 4 5 5 # Interactivity API 6 6 7 - Vitest implements a subset of [`@testing-library/user-event`](https://testing-library.com/docs/user-event) APIs using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [webdriver](https://www.w3.org/TR/webdriver/) APIs instead of faking events which makes the browser behaviour more reliable and consistent with how users interact with a page. 7 + Vitest implements a subset of [`@testing-library/user-event`](https://testing-library.com/docs/user-event) APIs using [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/) or [webdriver](https://www.w3.org/TR/webdriver/) instead of faking events which makes the browser behaviour more reliable and consistent with how users interact with a page. 8 8 9 - Almost every `userEvent` method inherits its provider options. To see all available options in your IDE, add `webdriver` or `playwright` types to your `tsconfig.json` file: 9 + ```ts 10 + import { userEvent } from '@vitest/browser/context' 11 + 12 + await userEvent.click(document.querySelector('.button')) 13 + ``` 14 + 15 + Almost every `userEvent` method inherits its provider options. To see all available options in your IDE, add `webdriver` or `playwright` types (depending on your provider) to your `tsconfig.json` file: 10 16 11 17 ::: code-group 12 18 ```json [playwright] ··· 27 33 } 28 34 } 29 35 ``` 36 + ::: 37 + 38 + ::: warning 39 + This page uses `@testing-library/dom` in examples to query elements. If you are using a framework like Vue, React or any other, use `@testing-library/{framework-name}` instead. Simple examples are available on the [Browser Mode page](/guide/browser/#examples). 30 40 ::: 31 41 32 42 ## userEvent.setup ··· 136 146 137 147 - **Type:** `(element: Element, text: string) => Promise<void>` 138 148 139 - Fill an `input/textarea/conteneditable` element with text. This will remove any existing text in the input before typing the new value. 149 + Set a value to the `input/textarea/conteneditable` field. This will remove any existing text in the input before setting the new value. 140 150 141 151 ```ts 142 152 import { userEvent } from '@vitest/browser/context' ··· 151 161 }) 152 162 ``` 153 163 164 + This methods focuses the element, fills it and triggers an `input` event after filling. You can use an empty string to clear the field. 165 + 154 166 ::: tip 155 167 This API is faster than using [`userEvent.type`](#userevent-type) or [`userEvent.keyboard`](#userevent-keyboard), but it **doesn't support** [user-event `keyboard` syntax](https://testing-library.com/docs/user-event/keyboard) (e.g., `{Shift}{selectall}`). 156 168 157 - We recommend using this API over [`userEvent.type`](#userevent-type) in situations when you don't need to enter special characters. 169 + We recommend using this API over [`userEvent.type`](#userevent-type) in situations when you don't need to enter special characters or have granular control over keypress events. 158 170 ::: 159 171 160 172 References: ··· 173 185 174 186 ```ts 175 187 import { userEvent } from '@vitest/browser/context' 176 - import { screen } from '@testing-library/dom' 177 188 178 189 test('trigger keystrokes', async () => { 179 190 await userEvent.keyboard('foo') // translates to: f, o, o ··· 360 371 This works the same as [`userEvent.hover`](#userevent-hover), but moves the cursor to the `document.body` element instead. 361 372 362 373 ::: warning 363 - By default, the cursor position is in the center (in `webdriverio` provider) or in "some" visible place (in `playwright` provider) of the body element, so if the currently hovered element is already in the same position, this method will have no effect. 374 + By default, the cursor position is in "some" visible place (in `playwright` provider) or in the center (in `webdriverio` provider) of the body element, so if the currently hovered element is already in the same position, this method will have no effect. 364 375 ::: 365 376 366 377 ```ts ··· 389 400 ```ts 390 401 import { userEvent } from '@vitest/browser/context' 391 402 import { screen } from '@testing-library/dom' 392 - import '@testing-library/jest-dom' // adds support for "toHaveTextContent" 393 403 394 404 test('drag and drop works', async () => { 395 405 const source = screen.getByRole('img', { name: /logo/ }) ··· 397 407 398 408 await userEvent.dragAndDrop(source, target) 399 409 400 - expect(target).toHaveTextContent('Logo is processed') 410 + await expect.element(target).toHaveTextContent('Logo is processed') 401 411 }) 402 412 ``` 403 413
+32
docs/guide/browser/why.md
··· 1 + --- 2 + title: Why Browser Mode? | Browser Mode 3 + outline: deep 4 + --- 5 + 6 + # Why Browser Mode? 7 + 8 + ## Motivation 9 + 10 + We developed the Vitest browser mode feature to help improve testing workflows and achieve more accurate and reliable test results. This experimental addition to our testing API allows developers to run tests in a native browser environment. In this section, we'll explore the motivations behind this feature and its benefits for testing. 11 + 12 + ### Different Ways of Testing 13 + 14 + There are different ways to test JavaScript code. Some testing frameworks simulate browser environments in Node.js, while others run tests in real browsers. In this context, [jsdom](https://www.npmjs.com/package/jsdom) is an example of a spec implementation that simulates a browser environment by being used with a test runner like Jest or Vitest, while other testing tools such as [WebdriverIO](https://webdriver.io/) or [Cypress](https://www.cypress.io/) allow developers to test their applications in a real browser or in case of [Playwright](https://playwright.dev/) provide you a browser engine. 15 + 16 + ### The Simulation Caveat 17 + 18 + Testing JavaScript programs in simulated environments such as jsdom or happy-dom has simplified the test setup and provided an easy-to-use API, making them suitable for many projects and increasing confidence in test results. However, it is crucial to keep in mind that these tools only simulate a browser environment and not an actual browser, which may result in some discrepancies between the simulated environment and the real environment. Therefore, false positives or negatives in test results may occur. 19 + 20 + To achieve the highest level of confidence in our tests, it's crucial to test in a real browser environment. This is why we developed the browser mode feature in Vitest, allowing developers to run tests natively in a browser and gain more accurate and reliable test results. With browser-level testing, developers can be more confident that their application will work as intended in a real-world scenario. 21 + 22 + ## Drawbacks 23 + 24 + When using Vitest browser, it is important to consider the following drawbacks: 25 + 26 + ### Early Development 27 + 28 + The browser mode feature of Vitest is still in its early stages of development. As such, it may not yet be fully optimized, and there may be some bugs or issues that have not yet been ironed out. It is recommended that users augment their Vitest browser experience with a standalone browser-side test runner like WebdriverIO, Cypress or Playwright. 29 + 30 + ### Longer Initialization 31 + 32 + Vitest browser requires spinning up the provider and the browser during the initialization process, which can take some time. This can result in longer initialization times compared to other testing patterns.
+10 -1
packages/browser/providers/webdriverio.d.ts
··· 1 - import type { RemoteOptions } from 'webdriverio' 1 + import type { RemoteOptions, ClickOptions, DragAndDropOptions } from 'webdriverio' 2 2 import '../matchers.js' 3 3 4 4 declare module 'vitest/node' { 5 5 interface BrowserProviderOptions extends RemoteOptions {} 6 + 7 + export interface UserEventClickOptions extends ClickOptions {} 8 + 9 + export interface UserEventDragOptions extends DragAndDropOptions { 10 + sourceX?: number 11 + sourceY?: number 12 + targetX?: number 13 + targetY?: number 14 + } 6 15 7 16 export interface BrowserCommandContext { 8 17 browser: WebdriverIO.Browser
+6
packages/ui/client/auto-imports.d.ts
··· 52 52 const getCurrentScope: typeof import('vue')['getCurrentScope'] 53 53 const getModuleGraph: typeof import('./composables/module-graph')['getModuleGraph'] 54 54 const h: typeof import('vue')['h'] 55 + const hideRightPanel: typeof import('./composables/navigation')['hideRightPanel'] 55 56 const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 56 57 const initializeNavigation: typeof import('./composables/navigation')['initializeNavigation'] 57 58 const inject: typeof import('vue')['inject'] ··· 63 64 const isReadonly: typeof import('vue')['isReadonly'] 64 65 const isRef: typeof import('vue')['isRef'] 65 66 const lineNumber: typeof import('./composables/params')['lineNumber'] 67 + const mainSizes: typeof import('./composables/navigation')['mainSizes'] 66 68 const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 67 69 const markRaw: typeof import('vue')['markRaw'] 68 70 const navigateTo: typeof import('./composables/navigation')['navigateTo'] ··· 89 91 const onUpdated: typeof import('vue')['onUpdated'] 90 92 const openInEditor: typeof import('./composables/error')['openInEditor'] 91 93 const openedTreeItems: typeof import('./composables/navigation')['openedTreeItems'] 94 + const panels: typeof import('./composables/navigation')['panels'] 92 95 const params: typeof import('./composables/params')['params'] 93 96 const parseError: typeof import('./composables/error')['parseError'] 94 97 const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] ··· 121 124 const showCoverage: typeof import('./composables/navigation')['showCoverage'] 122 125 const showDashboard: typeof import('./composables/navigation')['showDashboard'] 123 126 const showLine: typeof import('./composables/codemirror')['showLine'] 127 + const showNavigationPanel: typeof import('./composables/navigation')['showNavigationPanel'] 128 + const showRightPanel: typeof import('./composables/navigation')['showRightPanel'] 124 129 const showSource: typeof import('./composables/codemirror')['showSource'] 125 130 const syncRef: typeof import('@vueuse/core')['syncRef'] 126 131 const syncRefs: typeof import('@vueuse/core')['syncRefs'] ··· 139 144 const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 140 145 const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 141 146 const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 147 + const ui: typeof import('./composables/api')['ui'] 142 148 const unifiedDiff: typeof import("./composables/diff")["unifiedDiff"] 143 149 const unref: typeof import('vue')['unref'] 144 150 const unrefElement: typeof import('@vueuse/core')['unrefElement']
packages/ui/public/favicon.ico

This is a binary file and will not be displayed.

+6
test/browser/test/dom.test.ts
··· 9 9 document.body.replaceChildren() 10 10 }) 11 11 12 + test('viewport works correctly', async () => { 13 + await page.viewport(800, 600) 14 + expect(window.innerWidth).toBe(800) 15 + expect(window.innerHeight).toBe(600) 16 + }) 17 + 12 18 test('renders div', async () => { 13 19 const wrapper = createWrapper() 14 20 const div = createNode()
+47 -1
test/browser/test/userEvent.test.ts
··· 91 91 expect(onClick).toHaveBeenCalled() 92 92 expect(dblClick).not.toHaveBeenCalled() 93 93 }) 94 + 95 + test.runIf(server.provider === 'playwright')('clicks with x/y coords', async () => { 96 + const canvas = document.createElement('canvas') 97 + canvas.width = 400 98 + canvas.height = 300 99 + canvas.style.backgroundColor = 'pink' 100 + 101 + const spy = vi.fn() 102 + 103 + // draw a blue square in the middle of the rectangle 104 + const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d') 105 + // If the user clicks on the blue square, it should turn red, and vice versa 106 + const onClick = (event: PointerEvent) => { 107 + const x = event.offsetX 108 + const y = event.offsetY 109 + if (x > 150 && x < 250 && y > 100 && y < 200) { 110 + if (!ctx) { 111 + throw new Error('Canvas is not defined') 112 + } 113 + if (ctx.fillStyle === '#ff0000') { 114 + ctx.fillStyle = 'blue' 115 + } 116 + else { 117 + ctx.fillStyle = 'red' 118 + } 119 + 120 + ctx.fillRect(150, 100, 100, 100) 121 + } 122 + 123 + spy({ x, y }) 124 + } 125 + 126 + canvas.addEventListener('click', onClick) 127 + document.body.appendChild(canvas) 128 + 129 + await userEvent.click(document.body, { 130 + position: { 131 + x: 200, 132 + y: 150, 133 + }, 134 + }) 135 + 136 + expect(spy).toHaveBeenCalledWith({ 137 + x: 200, 138 + y: 150, 139 + }) 140 + }) 94 141 }) 95 142 96 143 describe('userEvent.dblClick', () => { ··· 272 319 }) 273 320 274 321 shadowRoot.appendChild(target) 275 - expect.poll(() => document.body.contains(target)).toBeTruthy() 276 322 277 323 await userEvent.hover(target) 278 324
+4 -4
packages/browser/src/client/orchestrator.ts
··· 43 43 const container = await getContainer(config) 44 44 45 45 if (config.browser.ui) { 46 - container.className = 'scrolls' 46 + container.className = 'absolute origin-top mt-[8px]' 47 + container.parentElement!.setAttribute('data-ready', 'true') 47 48 container.textContent = '' 48 49 } 49 50 const { width, height } = config.browser.viewport ··· 99 100 ) 100 101 iframe.setAttribute('data-vitest', 'true') 101 102 102 - iframe.style.display = 'block' 103 103 iframe.style.border = 'none' 104 - iframe.style.zIndex = '1' 105 - iframe.style.position = 'relative' 104 + iframe.style.width = '100%' 105 + iframe.style.height = '100%' 106 106 iframe.setAttribute('allowfullscreen', 'true') 107 107 iframe.setAttribute('allow', 'clipboard-write;') 108 108 iframe.setAttribute('name', 'vitest-iframe')
+103 -39
packages/ui/client/components/BrowserIframe.vue
··· 1 1 <script setup lang="ts"> 2 - import { customViewport, viewport } from '~/composables/browser' 2 + import { viewport } from '~/composables/browser' 3 3 import type { ViewportSize } from '~/composables/browser' 4 - import { getCurrentBrowserIframe, setIframeViewport } from '~/composables/api' 4 + import { 5 + hideRightPanel, 6 + panels, 7 + showNavigationPanel, 8 + showRightPanel, 9 + } from '~/composables/navigation' 5 10 6 - const sizes: Record<ViewportSize, [width: string, height: string] | null> = { 7 - 'small-mobile': ['320px', '568px'], 8 - 'large-mobile': ['414px', '896px'], 9 - 'tablet': ['834px', '1112px'], 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, 11 + const sizes: Record<ViewportSize, [width: number, height: number]> = { 12 + 'small-mobile': [320, 568], 13 + 'large-mobile': [414, 896], 14 + 'tablet': [834, 1112], 15 + } 16 + 17 + function isViewport(name: ViewportSize) { 18 + const preset = sizes[name] 19 + return viewport.value[0] === preset[0] && viewport.value[1] === preset[1] 14 20 } 15 21 16 22 async function changeViewport(name: ViewportSize) { 17 - if (viewport.value === name) { 18 - viewport.value = customViewport.value ? 'custom' : 'full' 19 - } 20 - else { 21 - viewport.value = name 22 - } 23 - 24 - const iframe = getCurrentBrowserIframe() 25 - if (!iframe) { 26 - console.warn('Iframe not found') 27 - return 28 - } 29 - 30 - const [width, height] 31 - = (sizes[viewport.value] || customViewport.value || sizes.full)! 32 - 33 - await setIframeViewport(width, height) 23 + viewport.value = sizes[name] 34 24 } 25 + 26 + const { width: windowWidth, height: windowHeight } = useWindowSize() 27 + 28 + const PADDING_SIDES = 20 29 + const PADDING_TOP = 100 30 + 31 + const containerSize = computed(() => { 32 + const parentContainerWidth = windowWidth.value * (panels.details.size / 100) 33 + const parentOffsetWidth = parentContainerWidth * (panels.details.browser / 100) 34 + const containerWidth = parentOffsetWidth - PADDING_SIDES 35 + const containerHeight = windowHeight.value - PADDING_TOP 36 + return { 37 + width: containerWidth, 38 + height: containerHeight, 39 + } 40 + }) 41 + 42 + const scale = computed(() => { 43 + const [iframeWidth, iframeHeight] = viewport.value 44 + const { width: containerWidth, height: containerHeight } = containerSize.value 45 + const widthScale = containerWidth > iframeWidth ? 1 : containerWidth / iframeWidth 46 + const heightScale = containerHeight > iframeHeight ? 1 : containerHeight / iframeHeight 47 + return Math.min(1, widthScale, heightScale) 48 + }) 49 + 50 + const marginLeft = computed(() => { 51 + const containerWidth = containerSize.value.width 52 + const iframeWidth = viewport.value[0] 53 + const offset = Math.trunc((containerWidth + PADDING_SIDES - iframeWidth) / 2) 54 + return `${offset}px` 55 + }) 35 56 </script> 36 57 37 58 <template> 38 59 <div h="full" flex="~ col"> 39 60 <div p="3" h-10 flex="~ gap-2" items-center bg-header border="b base"> 61 + <IconButton 62 + v-show="panels.navigation <= 2" 63 + v-tooltip.bottom="'Show Navigation Panel'" 64 + title="Show Navigation Panel" 65 + rotate-180 66 + icon="i-carbon:side-panel-close" 67 + @click="showNavigationPanel()" 68 + /> 40 69 <div class="i-carbon-content-delivery-network" /> 41 70 <span pl-1 font-bold text-sm flex-auto ws-nowrap overflow-hidden truncate>Browser UI</span> 71 + <IconButton 72 + v-show="panels.details.main > 0" 73 + v-tooltip.bottom="'Hide Right Panel'" 74 + title="Hide Right Panel" 75 + icon="i-carbon:side-panel-close" 76 + rotate-180 77 + @click="hideRightPanel()" 78 + /> 79 + <IconButton 80 + v-show="panels.details.main === 0" 81 + v-tooltip.bottom="'Show Right Panel'" 82 + title="Show Right Panel" 83 + icon="i-carbon:side-panel-close" 84 + @click="showRightPanel()" 85 + /> 42 86 </div> 43 87 <div p="l3 y2 r2" flex="~ gap-2" items-center bg-header border="b-2 base"> 44 88 <!-- TODO: these are only for preview (thank you Storybook!), we need to support more different and custom sizes (as a dropdown) --> 45 89 <IconButton 46 - v-tooltip.bottom="'Flexible'" 47 - title="Flexible" 48 - icon="i-carbon:fit-to-screen" 49 - :active="viewport === 'full'" 50 - @click="changeViewport('full')" 51 - /> 52 - <IconButton 53 90 v-tooltip.bottom="'Small mobile'" 54 91 title="Small mobile" 55 92 icon="i-carbon:mobile" 56 - :active="viewport === 'small-mobile'" 93 + :active="isViewport('small-mobile')" 57 94 @click="changeViewport('small-mobile')" 58 95 /> 59 96 <IconButton 60 97 v-tooltip.bottom="'Large mobile'" 61 98 title="Large mobile" 62 99 icon="i-carbon:mobile-add" 63 - :active="viewport === 'large-mobile'" 100 + :active="isViewport('large-mobile')" 64 101 @click="changeViewport('large-mobile')" 65 102 /> 66 103 <IconButton 67 104 v-tooltip.bottom="'Tablet'" 68 105 title="Tablet" 69 106 icon="i-carbon:tablet" 70 - :active="viewport === 'tablet'" 107 + :active="isViewport('tablet')" 71 108 @click="changeViewport('tablet')" 72 109 /> 110 + <span class="pointer-events-none" text-sm> 111 + {{ viewport[0] }}x{{ viewport[1] }}px 112 + <span v-if="scale < 1">({{ (scale * 100).toFixed(0) }}%)</span> 113 + </span> 73 114 </div> 74 - <div flex-auto class="scrolls"> 115 + <div id="tester-container" relative> 75 116 <div 76 117 id="tester-ui" 77 118 class="flex h-full justify-center items-center font-light op70" 78 - style="overflow: auto; width: 100%; height: 100%" 119 + :data-scale="scale" 120 + :style="{ 121 + '--viewport-width': `${viewport[0]}px`, 122 + '--viewport-height': `${viewport[1]}px`, 123 + '--tester-transform': `scale(${scale})`, 124 + '--tester-margin-left': marginLeft, 125 + }" 79 126 > 80 127 Select a test to run 81 128 </div> 82 129 </div> 83 130 </div> 84 131 </template> 132 + 133 + <style scoped> 134 + #tester-container:not([data-ready]) { 135 + width: 100%; 136 + height: 100%; 137 + display: flex; 138 + align-items: center; 139 + justify-content: center; 140 + } 141 + 142 + [data-ready] #tester-ui { 143 + width: var(--viewport-width); 144 + height: var(--viewport-height); 145 + transform: var(--tester-transform); 146 + margin-left: var(--tester-margin-left); 147 + } 148 + </style>
+4 -39
packages/ui/client/composables/api.ts
··· 1 1 import type { BrowserUI } from 'vitest' 2 2 import { findById } from './client' 3 - import { customViewport, viewport } from './browser' 4 - import { detailSizes } from '~/composables/navigation' 3 + import { viewport } from './browser' 5 4 6 - const ui: BrowserUI = { 5 + export const ui: BrowserUI = { 7 6 setCurrentFileId(fileId: string) { 8 7 activeFileId.value = fileId 9 8 currentModule.value = findById(fileId) ··· 11 10 }, 12 11 async setIframeViewport(width: number, height: number) { 13 12 // reset the button before setting a custom viewport 14 - viewport.value = 'custom' 15 - customViewport.value = [width, height] 16 - await setIframeViewport(width, height) 13 + viewport.value = [width, height] 14 + await new Promise(r => requestAnimationFrame(r)) 17 15 }, 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>( 35 - '#tester-ui iframe[data-vitest]', 36 - )! 37 - } 38 - 39 - export async function setIframeViewport( 40 - width: number | string, 41 - height: number | string, 42 - ) { 43 - const iframe = getCurrentBrowserIframe() 44 - // change the viewport of the iframe 45 - iframe.style.width = typeof width === 'string' ? width : `${width}px` 46 - iframe.style.height = typeof height === 'string' ? height : `${height}px` 47 - // wait until it renders the new size and resize the panel to make the iframe visible 48 - // this will not make it fully visible if viewport is too wide, but it's better than nothing 49 - await new Promise(r => requestAnimationFrame(r)) 50 - recalculateDetailPanels() 51 16 }
+1 -13
packages/ui/client/composables/browser.ts
··· 2 2 | 'small-mobile' 3 3 | 'large-mobile' 4 4 | 'tablet' 5 - | 'full' 6 - | 'custom' 7 - export const viewport = ref<ViewportSize>('full') 8 - export const customViewport = ref<[number, number]>() 9 - 10 - export function onBrowserPanelResizing(isResizing: boolean) { 11 - const tester = document.querySelector<HTMLDivElement>('#tester-ui') 12 - if (!tester) { 13 - return 14 - } 15 - 16 - tester.style.pointerEvents = isResizing ? 'none' : '' 17 - } 5 + export const viewport = ref<[number, number]>([414, 896])
+36
packages/ui/client/composables/navigation.ts
··· 15 15 && !!coverage.value.htmlReporter 16 16 ) 17 17 }) 18 + export const mainSizes = useLocalStorage<[left: number, right: number]>( 19 + 'vitest-ui_splitpanes-mainSizes', 20 + [33, 67], 21 + { 22 + initOnMounted: true, 23 + }, 24 + ) 18 25 export const detailSizes = useLocalStorage<[left: number, right: number]>( 19 26 'vitest-ui_splitpanes-detailSizes', 20 27 [33, 67], ··· 22 29 initOnMounted: true, 23 30 }, 24 31 ) 32 + 33 + // live sizes of panels in percentage 34 + export const panels = reactive({ 35 + navigation: mainSizes.value[0], 36 + details: { 37 + size: mainSizes.value[1], 38 + // these sizes are relative to the details panel 39 + browser: detailSizes.value[0], 40 + main: detailSizes.value[1], 41 + }, 42 + }) 25 43 26 44 // TODO 27 45 // For html report preview, "coverage.reportsDirectory" must be explicitly set as a subdirectory of html report. ··· 100 118 dashboardVisible.value = false 101 119 currentModule.value = undefined 102 120 activeFileId.value = '' 121 + } 122 + 123 + export function hideRightPanel() { 124 + panels.details.browser = 100 125 + panels.details.main = 0 126 + detailSizes.value = [100, 0] 127 + } 128 + 129 + export function showRightPanel() { 130 + panels.details.browser = 33 131 + panels.details.main = 67 132 + detailSizes.value = [33, 67] 133 + } 134 + 135 + export function showNavigationPanel() { 136 + panels.navigation = 33 137 + panels.details.size = 67 138 + mainSizes.value = [33, 67] 103 139 }
+47 -12
packages/ui/client/pages/index.vue
··· 7 7 coverageVisible, 8 8 detailSizes, 9 9 initializeNavigation, 10 + mainSizes, 11 + panels, 10 12 } from '~/composables/navigation' 11 - import { onBrowserPanelResizing } from '~/composables/browser' 12 13 13 14 const dashboardVisible = initializeNavigation() 14 15 15 - const mainSizes = useLocalStorage<[left: number, right: number]>( 16 - 'vitest-ui_splitpanes-mainSizes', 17 - [33, 67], 18 - { 19 - initOnMounted: true, 20 - }, 21 - ) 16 + const onBrowserPanelResizing = useDebounceFn((event: { size: number }[]) => { 17 + // don't trigger events in the iframe while resizing 18 + preventBrowserEvents() 19 + recordDetailsResize(event) 20 + }, 0) 22 21 23 22 const onMainResized = useDebounceFn((event: { size: number }[]) => { 24 23 event.forEach((e, i) => { 25 24 mainSizes.value[i] = e.size 26 25 }) 26 + recordMainResize(event) 27 27 }, 0) 28 + 28 29 const onModuleResized = useDebounceFn((event: { size: number }[]) => { 29 30 event.forEach((e, i) => { 30 31 detailSizes.value[i] = e.size 31 32 }) 32 - onBrowserPanelResizing(false) 33 + recordDetailsResize(event) 34 + allowBrowserEvents() 35 + }, 0) 36 + 37 + const resizingMain = useDebounceFn((event: { size: number }[]) => { 38 + recordMainResize(event) 39 + preventBrowserEvents() 33 40 }, 0) 34 41 35 42 function resizeMain() { ··· 37 44 const panelWidth = Math.min(width / 3, 300) 38 45 mainSizes.value[0] = (100 * panelWidth) / width 39 46 mainSizes.value[1] = 100 - mainSizes.value[0] 47 + recordMainResize([ 48 + { size: mainSizes.value[0] }, 49 + { size: mainSizes.value[1] }, 50 + ]) 51 + } 52 + 53 + function recordMainResize(event: { size: number }[]) { 54 + panels.navigation = event[0].size 55 + panels.details.size = event[1].size 56 + } 57 + 58 + function recordDetailsResize(event: { size: number }[]) { 59 + panels.details.browser = event[0].size 60 + panels.details.main = event[1].size 61 + } 62 + 63 + function preventBrowserEvents() { 64 + const tester = document.querySelector<HTMLDivElement>('#tester-ui') 65 + if (tester) { 66 + tester.style.pointerEvents = 'none' 67 + } 68 + } 69 + 70 + function allowBrowserEvents() { 71 + const tester = document.querySelector<HTMLDivElement>('#tester-ui') 72 + if (tester) { 73 + tester.style.pointerEvents = '' 74 + } 40 75 } 41 76 </script> 42 77 ··· 46 81 <Splitpanes 47 82 class="pt-4px" 48 83 @resized="onMainResized" 49 - @resize="onBrowserPanelResizing(true)" 84 + @resize="resizingMain" 50 85 @ready="resizeMain" 51 86 > 52 87 <Pane :size="mainSizes[0]"> ··· 66 101 v-else 67 102 id="details-splitpanes" 68 103 key="browser-detail" 69 - @resize="onBrowserPanelResizing(true)" 104 + @resize="onBrowserPanelResizing" 70 105 @resized="onModuleResized" 71 106 > 72 107 <Pane :size="detailSizes[0]" min-size="10"> 73 108 <BrowserIframe v-once /> 74 109 </Pane> 75 - <Pane :size="detailSizes[1]" min-size="5"> 110 + <Pane :size="detailSizes[1]"> 76 111 <Dashboard v-if="dashboardVisible" key="summary" /> 77 112 <Coverage 78 113 v-else-if="coverageVisible"
-2
packages/vitest/src/node/pool.ts
··· 158 158 const filesByPool: Record<LocalPool, WorkspaceSpec[]> = { 159 159 forks: [], 160 160 threads: [], 161 - // browser: [], 162 161 vmThreads: [], 163 162 vmForks: [], 164 163 typescript: [], 165 164 } 166 165 167 166 const factories: Record<LocalPool, () => ProcessPool> = { 168 - // browser: () => createBrowserPool(ctx), 169 167 vmThreads: () => createVmThreadsPool(ctx, options), 170 168 threads: () => createThreadsPool(ctx, options), 171 169 forks: () => createForksPool(ctx, options),
+138 -6
packages/browser/src/client/tester/context.ts
··· 1 1 import type { Task, WorkerGlobalState } from 'vitest' 2 2 import type { BrowserRPC } from '@vitest/browser/client' 3 - import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventHoverOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context' 3 + import type { BrowserPage, UserEvent, UserEventClickOptions, UserEventDragAndDropOptions, UserEventHoverOptions, UserEventTabOptions, UserEventTypeOptions } from '../../../context' 4 4 import type { BrowserRunnerState } from '../utils' 5 5 6 6 // this file should not import anything directly, only types ··· 125 125 }, 126 126 click(element: Element, options: UserEventClickOptions = {}) { 127 127 const css = convertElementToCssSelector(element) 128 - return triggerCommand('__vitest_click', css, options) 128 + return triggerCommand('__vitest_click', css, processClickOptions(options)) 129 129 }, 130 130 dblClick(element: Element, options: UserEventClickOptions = {}) { 131 131 const css = convertElementToCssSelector(element) 132 - return triggerCommand('__vitest_dblClick', css, options) 132 + return triggerCommand('__vitest_dblClick', css, processClickOptions(options)) 133 133 }, 134 134 tripleClick(element: Element, options: UserEventClickOptions = {}) { 135 135 const css = convertElementToCssSelector(element) 136 - return triggerCommand('__vitest_tripleClick', css, options) 136 + return triggerCommand('__vitest_tripleClick', css, processClickOptions(options)) 137 137 }, 138 138 selectOptions(element, value) { 139 139 const values = provider === 'webdriverio' ··· 169 169 }, 170 170 hover(element: Element, options: UserEventHoverOptions = {}) { 171 171 const css = convertElementToCssSelector(element) 172 - return triggerCommand('__vitest_hover', css, options) 172 + return triggerCommand('__vitest_hover', css, processHoverOptions(options)) 173 173 }, 174 174 unhover(element: Element, options: UserEventHoverOptions = {}) { 175 175 const css = convertElementToCssSelector(element.ownerDocument.body) ··· 184 184 dragAndDrop(source: Element, target: Element, options = {}) { 185 185 const sourceCss = convertElementToCssSelector(source) 186 186 const targetCss = convertElementToCssSelector(target) 187 - return triggerCommand('__vitest_dragAndDrop', sourceCss, targetCss, options) 187 + return triggerCommand( 188 + '__vitest_dragAndDrop', 189 + sourceCss, 190 + targetCss, 191 + processDragAndDropOptions(options), 192 + ) 188 193 }, 189 194 } 190 195 } ··· 297 302 298 303 function getTaskFullName(task: Task): string { 299 304 return task.suite ? `${getTaskFullName(task.suite)} ${task.name}` : task.name 305 + } 306 + 307 + function processClickOptions(options_?: UserEventClickOptions) { 308 + // only ui scales the iframe, so we need to adjust the position 309 + if (!options_ || !state().config.browser.ui) { 310 + return options_ 311 + } 312 + if (provider === 'playwright') { 313 + const options = options_ as NonNullable< 314 + Parameters<import('playwright').Page['click']>[1] 315 + > 316 + if (options.position) { 317 + options.position = processPlaywrightPosition(options.position) 318 + } 319 + } 320 + if (provider === 'webdriverio') { 321 + const options = options_ as import('webdriverio').ClickOptions 322 + if (options.x != null || options.y != null) { 323 + const cache = {} 324 + if (options.x != null) { 325 + options.x = scaleCoordinate(options.x, cache) 326 + } 327 + if (options.y != null) { 328 + options.y = scaleCoordinate(options.y, cache) 329 + } 330 + } 331 + } 332 + return options_ 333 + } 334 + 335 + function processHoverOptions(options_?: UserEventHoverOptions) { 336 + // only ui scales the iframe, so we need to adjust the position 337 + if (!options_ || !state().config.browser.ui) { 338 + return options_ 339 + } 340 + 341 + if (provider === 'playwright') { 342 + const options = options_ as NonNullable< 343 + Parameters<import('playwright').Page['hover']>[1] 344 + > 345 + if (options.position) { 346 + options.position = processPlaywrightPosition(options.position) 347 + } 348 + } 349 + if (provider === 'webdriverio') { 350 + const options = options_ as import('webdriverio').MoveToOptions 351 + const cache = {} 352 + if (options.xOffset != null) { 353 + options.xOffset = scaleCoordinate(options.xOffset, cache) 354 + } 355 + if (options.yOffset != null) { 356 + options.yOffset = scaleCoordinate(options.yOffset, cache) 357 + } 358 + } 359 + return options_ 360 + } 361 + 362 + function processDragAndDropOptions(options_?: UserEventDragAndDropOptions) { 363 + // only ui scales the iframe, so we need to adjust the position 364 + if (!options_ || !state().config.browser.ui) { 365 + return options_ 366 + } 367 + if (provider === 'playwright') { 368 + const options = options_ as NonNullable< 369 + Parameters<import('playwright').Page['dragAndDrop']>[2] 370 + > 371 + if (options.sourcePosition) { 372 + options.sourcePosition = processPlaywrightPosition(options.sourcePosition) 373 + } 374 + if (options.targetPosition) { 375 + options.targetPosition = processPlaywrightPosition(options.targetPosition) 376 + } 377 + } 378 + if (provider === 'webdriverio') { 379 + const cache = {} 380 + const options = options_ as import('webdriverio').DragAndDropOptions & { 381 + targetX?: number 382 + targetY?: number 383 + sourceX?: number 384 + sourceY?: number 385 + } 386 + if (options.sourceX != null) { 387 + options.sourceX = scaleCoordinate(options.sourceX, cache) 388 + } 389 + if (options.sourceY != null) { 390 + options.sourceY = scaleCoordinate(options.sourceY, cache) 391 + } 392 + if (options.targetX != null) { 393 + options.targetX = scaleCoordinate(options.targetX, cache) 394 + } 395 + if (options.targetY != null) { 396 + options.targetY = scaleCoordinate(options.targetY, cache) 397 + } 398 + } 399 + return options_ 400 + } 401 + 402 + function scaleCoordinate(coordinate: number, cache: any) { 403 + return Math.round(coordinate * getCachedScale(cache)) 404 + } 405 + 406 + function getCachedScale(cache: { scale: number | undefined }) { 407 + return cache.scale ??= getIframeScale() 408 + } 409 + 410 + function processPlaywrightPosition(position: { x: number; y: number }) { 411 + const scale = getIframeScale() 412 + if (position.x != null) { 413 + position.x *= scale 414 + } 415 + if (position.y != null) { 416 + position.y *= scale 417 + } 418 + return position 419 + } 420 + 421 + function getIframeScale() { 422 + const testerUi = window.parent.document.querySelector('#tester-ui') as HTMLElement | null 423 + if (!testerUi) { 424 + throw new Error(`Cannot find Tester element. This is a bug in Vitest. Please, open a new issue with reproduction.`) 425 + } 426 + const scaleAttribute = testerUi.getAttribute('data-scale') 427 + const scale = Number(scaleAttribute) 428 + if (Number.isNaN(scale)) { 429 + throw new TypeError(`Cannot parse scale value from Tester element (${scaleAttribute}). This is a bug in Vitest. Please, open a new issue with reproduction.`) 430 + } 431 + return scale 300 432 }
+1 -9
packages/browser/src/client/tester/tester.html
··· 21 21 {__VITEST_ERROR_CATCHER__} 22 22 {__VITEST_SCRIPTS__} 23 23 </head> 24 - <body 25 - data-vitest-body 26 - style=" 27 - width: 100%; 28 - height: 100%; 29 - transform: scale(1); 30 - transform-origin: left top; 31 - " 32 - > 24 + <body data-vitest-body> 33 25 <script type="module" src="./tester.ts"></script> 34 26 {__VITEST_APPEND__} 35 27 </body>
+6 -5
packages/browser/src/node/commands/dragAndDrop.ts
··· 7 7 context, 8 8 source, 9 9 target, 10 - options, 10 + options_, 11 11 ) => { 12 12 if (context.provider instanceof PlaywrightBrowserProvider) { 13 13 const frame = await context.frame() ··· 16 16 `css=${target}`, 17 17 { 18 18 timeout: 1000, 19 - ...options, 19 + ...options_, 20 20 }, 21 21 ) 22 22 } 23 23 else if (context.provider instanceof WebdriverBrowserProvider) { 24 24 const $source = context.browser.$(source) 25 25 const $target = context.browser.$(target) 26 - const duration = (options as any)?.duration ?? 10 26 + const options = (options_ || {}) as any 27 + const duration = options.duration ?? 10 27 28 28 29 // https://github.com/webdriverio/webdriverio/issues/8022#issuecomment-1700919670 29 30 await context.browser 30 31 .action('pointer') 31 - .move({ duration: 0, origin: $source, x: 0, y: 0 }) 32 + .move({ duration: 0, origin: $source, x: options.sourceX ?? 0, y: options.sourceY ?? 0 }) 32 33 .down({ button: 0 }) 33 34 .move({ duration: 0, origin: 'pointer', x: 0, y: 0 }) 34 35 .pause(duration) 35 - .move({ duration: 0, origin: $target, x: 0, y: 0 }) 36 + .move({ duration: 0, origin: $target, x: options.targetX ?? 0, y: options.targetY ?? 0 }) 36 37 .move({ duration: 0, origin: 'pointer', x: 1, y: 0 }) 37 38 .move({ duration: 0, origin: 'pointer', x: -1, y: 0 }) 38 39 .up({ button: 0 })
+16 -4
packages/ui/client/components/explorer/ExplorerItem.vue
··· 23 23 type, 24 24 disableTaskLocation, 25 25 onItemClick, 26 + projectNameColor, 26 27 } = defineProps<{ 27 28 taskId: string 28 29 name: string ··· 135 136 showSource(t) 136 137 } 137 138 } 139 + 140 + const projectNameTextColor = computed(() => { 141 + switch (projectNameColor) { 142 + case 'blue': 143 + case 'green': 144 + case 'magenta': 145 + return 'white' 146 + default: 147 + return 'black' 148 + } 149 + }) 138 150 </script> 139 151 140 152 <template> ··· 162 174 </div> 163 175 <StatusIcon :state="state" :mode="task.mode" :failed-snapshot="failedSnapshot" w-4 /> 164 176 <div v-if="type === 'suite' && typecheck" class="i-logos:typescript-icon" flex-shrink-0 mr-2 /> 165 - <div flex items-end gap-2 :text="state === 'fail' ? 'red-500' : ''" overflow-hidden> 177 + <div flex items-end gap-2 overflow-hidden> 166 178 <span text-sm truncate font-light> 167 - <span v-if="type === 'file' && projectName" :style="{ color: projectNameColor }"> 168 - [{{ projectName }}] 179 + <span v-if="type === 'file' && projectName" class="rounded-full p-1 mr-1 text-xs" :style="{ backgroundColor: projectNameColor, color: projectNameTextColor }"> 180 + {{ projectName }} 169 181 </span> 170 - <span v-html="highlighted" /> 182 + <span :text="state === 'fail' ? 'red-500' : ''" v-html="highlighted" /> 171 183 </span> 172 184 <span v-if="typeof duration === 'number'" text="xs" op20 style="white-space: nowrap"> 173 185 {{ duration > 0 ? duration : '< 1' }}ms
+11
packages/ui/client/composables/client/index.ts
··· 7 7 import { ENTRY_URL, isReport } from '../../constants' 8 8 import { parseError } from '../error' 9 9 import { activeFileId } from '../params' 10 + import { ui } from '../../composables/api' 10 11 import { createStaticClient } from './static' 11 12 import { testRunState, unhandledErrors } from './state' 12 13 import { explorerTree } from '~/composables/explorer' ··· 106 107 } 107 108 } 108 109 110 + // for testing during dev 111 + // export const browserState: BrowserRunnerState = { 112 + // files: [], 113 + // config: {}, 114 + // type: 'orchestrator', 115 + // wrapModule: () => {}, 116 + // } 109 117 // @ts-expect-error not typed global 110 118 export const browserState = window.__vitest_browser_runner__ as 111 119 | BrowserRunnerState 112 120 | undefined 121 + 122 + // @ts-expect-error not typed global 123 + window.__vitest_ui_api__ = ui 113 124 114 125 watch( 115 126 () => client.ws,
+2 -3
packages/vitest/src/create/browser/examples.ts
··· 145 145 js: ` 146 146 export default function HelloWorld({ name }) { 147 147 const parent = document.createElement('div') 148 - document.body.appendChild(parent) 149 148 150 149 const h1 = document.createElement('h1') 151 150 h1.textContent = 'Hello ' + name + '!' ··· 157 156 ts: ` 158 157 export default function HelloWorld({ name }: { name: string }): HTMLDivElement { 159 158 const parent = document.createElement('div') 160 - document.body.appendChild(parent) 161 159 162 160 const h1 = document.createElement('h1') 163 161 h1.textContent = 'Hello ' + name + '!' ··· 169 167 test: ` 170 168 import { expect, test } from 'vitest' 171 169 import { getByText } from '@testing-library/dom' 172 - import HelloWorld from './HelloWorld' 170 + import HelloWorld from './HelloWorld.js' 173 171 174 172 test('renders name', () => { 175 173 const parent = HelloWorld({ name: 'Vitest' }) 174 + document.body.appendChild(parent) 176 175 177 176 const element = getByText(parent, 'Hello Vitest!') 178 177 expect(element).toBeInTheDocument()