[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: init @vitest/browser package (#1302)

Co-authored-by: Joaquín Sánchez Jiménez <joaquin.sanchez@fi2net.es>
Co-authored-by: userquin <userquin@gmail.com>
Co-authored-by: M. Bagher Abiat <zorofight94@gmail.com>

authored by

Anthony Fu
Joaquín Sánchez Jiménez
userquin
M. Bagher Abiat
and committed by
GitHub
(Jul 20, 2022, 3:37 AM +0800) 88033bca 7c2138f2

+796 -92
+3 -1
package.json
··· 22 22 "test": "vitest --api -r test/core", 23 23 "test:run": "vitest run -r test/core", 24 24 "test:all": "cross-env CI=true pnpm -r --stream run test --allowOnly", 25 - "test:ci": "cross-env CI=true pnpm -r --stream --filter !test-fails run test --allowOnly", 25 + "test:ci": "cross-env CI=true pnpm -r --stream --filter !test-fails --filter !test-browser run test --allowOnly", 26 26 "test:ci:single-thread": "cross-env CI=true pnpm -r --stream --filter !test-fails run test --allowOnly --no-threads", 27 27 "typecheck": "tsc --noEmit", 28 28 "ui:build": "vite build packages/ui", ··· 40 40 "@types/lodash": "^4.14.182", 41 41 "@types/node": "^18.0.4", 42 42 "@types/ws": "^8.5.3", 43 + "@vitest/browser": "workspace:*", 44 + "@vitest/ui": "workspace:*", 43 45 "bumpp": "^8.2.1", 44 46 "c8": "^7.11.3", 45 47 "cross-env": "^7.0.3",
+88 -5
pnpm-lock.yaml
··· 19 19 '@types/lodash': ^4.14.182 20 20 '@types/node': ^18.0.4 21 21 '@types/ws': ^8.5.3 22 + '@vitest/browser': workspace:* 23 + '@vitest/ui': workspace:* 22 24 bumpp: ^8.2.1 23 25 c8: ^7.11.3 24 26 cross-env: ^7.0.3 ··· 55 57 '@types/lodash': 4.14.182 56 58 '@types/node': 18.0.4 57 59 '@types/ws': 8.5.3 60 + '@vitest/browser': link:packages/browser 61 + '@vitest/ui': link:packages/ui 58 62 bumpp: 8.2.1 59 63 c8: 7.11.3 60 64 cross-env: 7.0.3 ··· 570 574 vitest: link:../../packages/vitest 571 575 vue-template-compiler: 2.6.14 572 576 577 + packages/browser: 578 + specifiers: 579 + '@types/ws': ^8.2.2 580 + '@vitest/ws-client': workspace:* 581 + local-pkg: ^0.4.1 582 + mlly: ^0.5.2 583 + modern-node-polyfills: ^0.0.7 584 + picocolors: ^1.0.0 585 + rollup: ^2.67.2 586 + rollup-plugin-node-polyfills: ^0.2.1 587 + sirv: ^2.0.2 588 + vitest: workspace:* 589 + dependencies: 590 + local-pkg: 0.4.2 591 + mlly: 0.5.4 592 + modern-node-polyfills: 0.0.7 593 + rollup-plugin-node-polyfills: 0.2.1 594 + sirv: 2.0.2 595 + devDependencies: 596 + '@types/ws': 8.5.3 597 + '@vitest/ws-client': link:../ws-client 598 + picocolors: 1.0.0 599 + rollup: 2.77.0 600 + vitest: link:../vitest 601 + 573 602 packages/ui: 574 603 specifiers: 575 604 '@faker-js/faker': ^7.3.0 ··· 783 812 specifiers: 784 813 vitest: workspace:* 785 814 devDependencies: 815 + vitest: link:../../packages/vitest 816 + 817 + test/browser: 818 + specifiers: 819 + '@vitest/browser': workspace:* 820 + vitest: workspace:* 821 + devDependencies: 822 + '@vitest/browser': link:../../packages/browser 786 823 vitest: link:../../packages/vitest 787 824 788 825 test/cache: ··· 3669 3706 type-detect: 4.0.8 3670 3707 dev: true 3671 3708 3709 + /@jspm/core/2.0.0-beta.24: 3710 + resolution: {integrity: sha512-a4Bo/80Z6CoJNor5ldgs6002utmmbttP4JYd/FJ0Ob2fVdf6O6ha5SORBCqrnDnBvMc1TlrHY7dCfat5+H0a6A==} 3711 + dev: false 3712 + 3672 3713 /@lit/reactive-element/1.3.1: 3673 3714 resolution: {integrity: sha512-nOJARIr3pReqK3hfFCSW2Zg/kFcFsSAlIE7z4a0C9D2dPrgD/YSn3ZP2ET/rxKB65SXyG7jJbkynBRm+tGlacw==} 3674 3715 dev: false ··· 4204 4245 rollup: 2.76.0 4205 4246 dev: false 4206 4247 4248 + /@rollup/plugin-inject/4.0.4_rollup@2.77.0: 4249 + resolution: {integrity: sha512-4pbcU4J/nS+zuHk+c+OL3WtmEQhqxlZ9uqfjQMQDOHOPld7PsCd8k5LWs8h5wjwJN7MgnAn768F2sDxEP4eNFQ==} 4250 + peerDependencies: 4251 + rollup: ^1.20.0 || ^2.0.0 4252 + dependencies: 4253 + '@rollup/pluginutils': 3.1.0_rollup@2.77.0 4254 + estree-walker: 2.0.2 4255 + magic-string: 0.25.9 4256 + rollup: 2.77.0 4257 + dev: false 4258 + 4207 4259 /@rollup/plugin-json/4.1.0_rollup@2.77.0: 4208 4260 resolution: {integrity: sha512-yfLbTdNS6amI/2OpmbiBoW12vngr5NW2jCJVZSBEz+H5KfUJZ2M7sDjk0U6GOOdCWFVScShte29o9NezJ53TPw==} 4209 4261 peerDependencies: ··· 4263 4315 estree-walker: 1.0.1 4264 4316 picomatch: 2.3.1 4265 4317 rollup: 2.77.0 4266 - dev: true 4267 4318 4268 4319 /@rollup/pluginutils/4.2.1: 4269 4320 resolution: {integrity: sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==} ··· 6244 6295 6245 6296 /@types/estree/0.0.39: 6246 6297 resolution: {integrity: sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw==} 6247 - dev: true 6248 6298 6249 6299 /@types/estree/0.0.51: 6250 6300 resolution: {integrity: sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==} ··· 7672 7722 resolution: {integrity: sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==} 7673 7723 engines: {node: '>=0.4.0'} 7674 7724 hasBin: true 7675 - dev: true 7676 7725 7677 7726 /address/1.1.2: 7678 7727 resolution: {integrity: sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA==} ··· 11645 11694 - supports-color 11646 11695 dev: true 11647 11696 11697 + /estree-walker/0.6.1: 11698 + resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 11699 + dev: false 11700 + 11648 11701 /estree-walker/1.0.1: 11649 11702 resolution: {integrity: sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg==} 11650 - dev: true 11651 11703 11652 11704 /estree-walker/2.0.2: 11653 11705 resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} ··· 15540 15592 pathe: 0.3.2 15541 15593 pkg-types: 0.3.3 15542 15594 15595 + /modern-node-polyfills/0.0.7: 15596 + resolution: {integrity: sha512-nQ1ckaD2n6rKvt3YW5kSqJTnA8MFQU+TTSn84PVyX7ySyy1ZXOxxbH95QIAT1WgqWosvSHO6tQK4LdM8J0E5AQ==} 15597 + dependencies: 15598 + '@jspm/core': 2.0.0-beta.24 15599 + '@rollup/plugin-inject': 4.0.4_rollup@2.77.0 15600 + acorn: 8.7.1 15601 + esbuild: 0.14.47 15602 + local-pkg: 0.4.2 15603 + rollup: 2.77.0 15604 + dev: false 15605 + 15543 15606 /moment/2.29.3: 15544 15607 resolution: {integrity: sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==} 15545 15608 dev: true ··· 17953 18016 - supports-color 17954 18017 dev: true 17955 18018 18019 + /rollup-plugin-inject/3.0.2: 18020 + resolution: {integrity: sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w==} 18021 + deprecated: This package has been deprecated and is no longer maintained. Please use @rollup/plugin-inject. 18022 + dependencies: 18023 + estree-walker: 0.6.1 18024 + magic-string: 0.25.9 18025 + rollup-pluginutils: 2.8.2 18026 + dev: false 18027 + 17956 18028 /rollup-plugin-license/2.8.1_rollup@2.77.0: 17957 18029 resolution: {integrity: sha512-VYd9pzaNL7NN6xQp93XiiCV2UoduXgSmTcz6rl9bHPdiifT6yH3Zw/omEr73Rq8TIyN4nqJACBbKIT/2eE66wg==} 17958 18030 engines: {node: '>=10.0.0'} ··· 17971 18043 spdx-satisfies: 5.0.1 17972 18044 dev: true 17973 18045 18046 + /rollup-plugin-node-polyfills/0.2.1: 18047 + resolution: {integrity: sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA==} 18048 + dependencies: 18049 + rollup-plugin-inject: 3.0.2 18050 + dev: false 18051 + 17974 18052 /rollup-plugin-terser/7.0.2_rollup@2.77.0: 17975 18053 resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==} 17976 18054 peerDependencies: ··· 17982 18060 serialize-javascript: 4.0.0 17983 18061 terser: 5.12.1 17984 18062 dev: true 18063 + 18064 + /rollup-pluginutils/2.8.2: 18065 + resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 18066 + dependencies: 18067 + estree-walker: 0.6.1 18068 + dev: false 17985 18069 17986 18070 /rollup/2.74.1: 17987 18071 resolution: {integrity: sha512-K2zW7kV8Voua5eGkbnBtWYfMIhYhT9Pel2uhBk2WO5eMee161nPze/XRfvEQPFYz7KgrCCnmh2Wy0AMFLGGmMA==} ··· 18012 18096 hasBin: true 18013 18097 optionalDependencies: 18014 18098 fsevents: 2.3.2 18015 - dev: true 18016 18099 18017 18100 /rst-selector-parser/2.2.3: 18018 18101 resolution: {integrity: sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=}
+2
tsconfig.json
··· 16 16 "paths": { 17 17 "@vitest/ws-client": ["./packages/ws-client/src/index.ts"], 18 18 "@vitest/ui": ["./packages/ui/node/index.ts"], 19 + "@vitest/browser": ["./packages/browser/node/index.ts"], 19 20 "#types": ["./packages/vitest/src/index.ts"], 20 21 "~/*": ["./packages/ui/client/*"], 21 22 "vitest": ["./packages/vitest/src/index.ts"], 22 23 "vitest/globals": ["./packages/vitest/globals.d.ts"], 23 24 "vitest/node": ["./packages/vitest/src/node/index.ts"], 24 25 "vitest/config": ["./packages/vitest/src/config.ts"], 26 + "vitest/browser": ["./packages/vitest/src/browser.ts"], 25 27 "vite-node": ["./packages/vite-node/src/index.ts"], 26 28 "vite-node/client": ["./packages/vite-node/src/client.ts"], 27 29 "vite-node/server": ["./packages/vite-node/src/server.ts"],
+1
docs/guide/cli.md
··· 67 67 | `--mode <name>` | Override Vite mode (default: `test`) | 68 68 | `--globals` | Inject APIs globally | 69 69 | `--dom` | Mock browser api with happy-dom | 70 + | `--browser` | Run tests in browser | 70 71 | `--environment <env>` | Runner environment (default: `node`) | 71 72 | `--passWithNoTests` | Pass when no tests found | 72 73 | `--allowOnly` | Allow tests and suites that are marked as `only` (default: false in CI, true otherwise) |
+33
packages/browser/README.md
··· 1 + # @vitest/browser 2 + 3 + Browser runner for Vitest. 4 + 5 + > ⚠️ This package is **not yet ready** and it's for preview only. While this package will be released along with other packages, it will not follow semver for breaking changes until we mark it as ready. **Do not use it in production**. 6 + 7 + ## Progress 8 + 9 + Current Status: **Working in progress** 10 + 11 + - [x] Init package and integration 12 + - [x] Stub node packages for Vitest runtime 13 + - [ ] Works in development mode 14 + - [ ] Better log in terminal 15 + - [ ] Fulfill tests (using Browser only APIs, Vue and React components) 16 + - [ ] Show progress and error on the browser page 17 + - [ ] Headless mode in CI 18 + - [ ] Docs 19 + 20 + Related PRs 21 + 22 + - [#1302](https://github.com/vitest-dev/vitest/pull/1302) 23 + 24 + ## Development Setup 25 + 26 + At project root: 27 + 28 + ```bash 29 + pnpm dev 30 + 31 + cd test/browser 32 + pnpm vitest --browser 33 + ```
+29
packages/browser/index.html
··· 1 + <!DOCTYPE html> 2 + <html lang="en"> 3 + <head> 4 + <meta charset="UTF-8" /> 5 + <link rel="icon" href="/favicon.svg" type="image/svg+xml"> 6 + <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 + <title>Vitest Browser Runner</title> 8 + <style> 9 + html { 10 + overflow: hidden; 11 + padding: 0; 12 + margin: 0; 13 + } 14 + body { 15 + padding: 0; 16 + margin: 0; 17 + } 18 + #vitest-ui { 19 + width: 100vw; 20 + height: 100vh; 21 + border: none; 22 + } 23 + </style> 24 + </head> 25 + <body> 26 + <iframe id="vitest-ui" src=""></iframe> 27 + <script type="module" src="./client/main.ts"></script> 28 + </body> 29 + </html>
+49
packages/browser/package.json
··· 1 + { 2 + "name": "@vitest/browser", 3 + "type": "module", 4 + "version": "0.4.1", 5 + "description": "Browser running for Vitest", 6 + "repository": { 7 + "type": "git", 8 + "url": "git+https://github.com/vitest-dev/vitest.git", 9 + "directory": "packages/browser" 10 + }, 11 + "sideEffects": false, 12 + "exports": { 13 + ".": { 14 + "types": "./dist/index.d.ts", 15 + "import": "./dist/index.js" 16 + }, 17 + "./*": "./*" 18 + }, 19 + "main": "./dist/index.js", 20 + "module": "./dist/index.js", 21 + "types": "./dist/index.d.ts", 22 + "files": [ 23 + "dist", 24 + "stubs" 25 + ], 26 + "scripts": { 27 + "build": "rimraf dist && pnpm build:node && pnpm build:client && pnpm copy", 28 + "build:client": "vite build", 29 + "build:node": "rollup -c", 30 + "dev:client": "vite build --watch", 31 + "dev:node": "rollup -c --watch --watch.include=node/**", 32 + "dev": "rimraf dist && run-p dev:node dev:client", 33 + "copy": "esmo scripts/copy-ui-to-browser.ts" 34 + }, 35 + "dependencies": { 36 + "local-pkg": "^0.4.1", 37 + "mlly": "^0.5.2", 38 + "modern-node-polyfills": "^0.0.7", 39 + "rollup-plugin-node-polyfills": "^0.2.1", 40 + "sirv": "^2.0.2" 41 + }, 42 + "devDependencies": { 43 + "@types/ws": "^8.2.2", 44 + "@vitest/ws-client": "workspace:*", 45 + "picocolors": "^1.0.0", 46 + "rollup": "^2.67.2", 47 + "vitest": "workspace:*" 48 + } 49 + }
+50
packages/browser/rollup.config.js
··· 1 + import esbuild from 'rollup-plugin-esbuild' 2 + import dts from 'rollup-plugin-dts' 3 + import resolve from '@rollup/plugin-node-resolve' 4 + import commonjs from '@rollup/plugin-commonjs' 5 + import json from '@rollup/plugin-json' 6 + import alias from '@rollup/plugin-alias' 7 + import pkg from './package.json' 8 + 9 + const external = [ 10 + ...Object.keys(pkg.dependencies), 11 + ...Object.keys(pkg.peerDependencies || {}), 12 + 'worker_threads', 13 + ] 14 + 15 + const plugins = [ 16 + alias({ 17 + entries: [{ find: /^node:(.+)$/, replacement: '$1' }], 18 + }), 19 + resolve({ 20 + preferBuiltins: true, 21 + }), 22 + json(), 23 + commonjs(), 24 + esbuild({ 25 + target: 'node14', 26 + }), 27 + ] 28 + 29 + export default () => [ 30 + { 31 + input: [ 32 + './node/index.ts', 33 + ], 34 + output: { 35 + dir: 'dist', 36 + format: 'esm', 37 + }, 38 + external, 39 + plugins, 40 + }, 41 + { 42 + input: './node/index.ts', 43 + output: { 44 + file: 'dist/index.d.ts', 45 + format: 'esm', 46 + }, 47 + external, 48 + plugins: [dts()], 49 + }, 50 + ]
+4
packages/browser/tsconfig.json
··· 1 + { 2 + "extends": "../../tsconfig.json", 3 + "exclude": ["dist", "node_modules"] 4 + }
+13
packages/browser/types.ts
··· 1 + export interface WSMessage { 2 + /** 3 + * Message type 4 + */ 5 + type: string 6 + 7 + /** 8 + * Message Data 9 + */ 10 + data: any 11 + } 12 + 13 + export type RunState = 'idle' | 'running'
+12
packages/browser/vite.config.ts
··· 1 + import { defineConfig } from 'vite' 2 + 3 + export default defineConfig({ 4 + server: { 5 + watch: { ignored: ['**/**'] }, 6 + }, 7 + build: { 8 + minify: false, 9 + outDir: './dist/client', 10 + emptyOutDir: false, 11 + }, 12 + })
+5 -5
packages/vitest/LICENSE.md
··· 869 869 > same "printed page" as the copyright notice for easier 870 870 > identification within third-party archives. 871 871 > 872 - > Copyright 2021 ehmicky <ehmicky@gmail.com> 872 + > Copyright 2019 ehmicky <ehmicky@gmail.com> 873 873 > 874 874 > Licensed under the Apache License, Version 2.0 (the "License"); 875 875 > you may not use this file except in compliance with the License. ··· 1229 1229 1230 1230 > MIT License 1231 1231 > 1232 - > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) 1232 + > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com) 1233 1233 > 1234 1234 > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 1235 1235 > ··· 1281 1281 1282 1282 > MIT License 1283 1283 > 1284 - > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) 1284 + > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com) 1285 1285 > 1286 1286 > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 1287 1287 > ··· 1366 1366 1367 1367 > MIT License 1368 1368 > 1369 - > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) 1369 + > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com) 1370 1370 > 1371 1371 > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 1372 1372 > ··· 1882 1882 1883 1883 > MIT License 1884 1884 > 1885 - > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) 1885 + > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com) 1886 1886 > 1887 1887 > Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 1888 1888 >
+8
packages/vitest/package.json
··· 35 35 "types": "./dist/node.d.ts", 36 36 "import": "./dist/node.mjs" 37 37 }, 38 + "./browser": { 39 + "types": "./dist/browser.d.ts", 40 + "import": "./dist/browser.mjs" 41 + }, 38 42 "./config": { 39 43 "types": "./config.d.ts", 40 44 "require": "./dist/config.cjs", ··· 63 67 }, 64 68 "peerDependencies": { 65 69 "@edge-runtime/vm": "*", 70 + "@vitest/browser": "*", 66 71 "@vitest/ui": "*", 67 72 "c8": "*", 68 73 "happy-dom": "*", ··· 70 75 }, 71 76 "peerDependenciesMeta": { 72 77 "@vitest/ui": { 78 + "optional": true 79 + }, 80 + "@vitest/browser": { 73 81 "optional": true 74 82 }, 75 83 "c8": {
+4
packages/vitest/rollup.config.js
··· 16 16 17 17 const entries = [ 18 18 'src/index.ts', 19 + 'src/browser.ts', 19 20 'src/node/cli.ts', 20 21 'src/node.ts', 21 22 'src/runtime/worker.ts', 22 23 'src/runtime/entry.ts', 24 + 'src/runtime/suite.ts', 23 25 'src/integrations/spy.ts', 24 26 ] 25 27 26 28 const dtsEntries = [ 27 29 'src/index.ts', 28 30 'src/node.ts', 31 + 'src/browser.ts', 29 32 'src/config.ts', 30 33 ] 31 34 ··· 36 39 'worker_threads', 37 40 'inspector', 38 41 'c8', 42 + '@vitest/browser', 39 43 ] 40 44 41 45 const plugins = [
+12
test/browser/package.json
··· 1 + { 2 + "name": "@vitest/test-browser", 3 + "private": true, 4 + "scripts": { 5 + "test": "vitest --browser --open", 6 + "coverage": "vitest run --coverage" 7 + }, 8 + "devDependencies": { 9 + "@vitest/browser": "workspace:*", 10 + "vitest": "workspace:*" 11 + } 12 + }
+5
test/browser/tsconfig.json
··· 1 + { 2 + "compilerOptions": { 3 + "esModuleInterop": true 4 + } 5 + }
+8
test/browser/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + browser: true, 6 + open: true, 7 + }, 8 + })
+4
examples/vitesse/src/auto-import.d.ts
··· 12 12 const getCurrentScope: typeof import('vue')['getCurrentScope'] 13 13 const h: typeof import('vue')['h'] 14 14 const inject: typeof import('vue')['inject'] 15 + const isProxy: typeof import('vue')['isProxy'] 16 + const isReactive: typeof import('vue')['isReactive'] 15 17 const isReadonly: typeof import('vue')['isReadonly'] 16 18 const isRef: typeof import('vue')['isRef'] 17 19 const markRaw: typeof import('vue')['markRaw'] ··· 48 50 const useSlots: typeof import('vue')['useSlots'] 49 51 const watch: typeof import('vue')['watch'] 50 52 const watchEffect: typeof import('vue')['watchEffect'] 53 + const watchPostEffect: typeof import('vue')['watchPostEffect'] 54 + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 51 55 }
+1 -1
examples/vitesse/src/components.d.ts
··· 1 1 // generated by unplugin-vue-components 2 2 // We suggest you to commit this file into source control 3 - // Read more: https://github.com/vuejs/vue-next/pull/3399 3 + // Read more: https://github.com/vuejs/core/pull/3399 4 4 import '@vue/runtime-core' 5 5 6 6 declare module '@vue/runtime-core' {
+88
packages/browser/client/main.ts
··· 1 + import type { VitestClient } from '@vitest/ws-client' 2 + import { createClient } from '@vitest/ws-client' 3 + // eslint-disable-next-line no-restricted-imports 4 + import type { ResolvedConfig } from 'vitest' 5 + 6 + // @ts-expect-error mocking some node apis 7 + globalThis.process = { env: {}, argv: [], stdout: { write: () => {} } } 8 + globalThis.global = globalThis 9 + 10 + export const PORT = import.meta.hot ? '51204' : location.port 11 + export const HOST = [location.hostname, PORT].filter(Boolean).join(':') 12 + export const ENTRY_URL = `${ 13 + location.protocol === 'https:' ? 'wss:' : 'ws:' 14 + }//${HOST}/__vitest_api__` 15 + 16 + let config: ResolvedConfig | undefined 17 + const browserHashMap = new Map<string, string>() 18 + 19 + export const client = createClient(ENTRY_URL, { 20 + handlers: { 21 + async onPathsCollected(paths) { 22 + if (!paths) 23 + return 24 + 25 + // const config = __vitest_worker__.config 26 + const now = `${new Date().getTime()}` 27 + paths.forEach((i) => { 28 + browserHashMap.set(i, now) 29 + }) 30 + 31 + await runTests(paths, config, client) 32 + }, 33 + }, 34 + }) 35 + 36 + const ws = client.ws 37 + 38 + async function loadConfig() { 39 + let retries = 5 40 + do { 41 + try { 42 + await new Promise(resolve => setTimeout(resolve, 150)) 43 + config = await client.rpc.getConfig() 44 + return 45 + } 46 + catch (_) { 47 + // just ignore 48 + } 49 + } 50 + while (--retries > 0) 51 + 52 + throw new Error('cannot load configuration after 5 retries') 53 + } 54 + 55 + ws.addEventListener('open', async () => { 56 + await loadConfig() 57 + 58 + // @ts-expect-error mocking vitest apis 59 + globalThis.__vitest_worker__ = { 60 + config, 61 + browserHashMap, 62 + rpc: client.rpc, 63 + } 64 + 65 + // @ts-expect-error mocking vitest apis 66 + globalThis.__vitest_mocker__ = {} 67 + const paths = await client.rpc.getPaths() 68 + 69 + const now = `${new Date().getTime()}` 70 + paths.forEach(i => browserHashMap.set(i, now)) 71 + 72 + const iFrame = document.getElementById('vitest-ui') as HTMLIFrameElement 73 + iFrame.setAttribute('src', '/__vitest__/') 74 + 75 + await runTests(paths, config, client) 76 + }) 77 + 78 + async function runTests(paths: string[], config: any, client: VitestClient) { 79 + const name = '/__vitest_index__' 80 + const { startTests, setupGlobalEnv } = (await import(name)) as unknown as typeof import('vitest/browser') 81 + 82 + await setupGlobalEnv(config as any) 83 + 84 + await startTests(paths, config as any) 85 + 86 + await client.rpc.onFinished() 87 + await client.rpc.onWatcherStart() 88 + }
+85
packages/browser/node/index.ts
··· 1 + import { fileURLToPath } from 'url' 2 + // eslint-disable-next-line no-restricted-imports 3 + import { resolve } from 'path' 4 + import { builtinModules } from 'module' 5 + import { polyfillPath } from 'modern-node-polyfills' 6 + import sirv from 'sirv' 7 + import type { Plugin } from 'vite' 8 + import { resolvePath } from 'mlly' 9 + 10 + const stubs = [ 11 + 'fs', 12 + 'local-pkg', 13 + 'module', 14 + 'noop', 15 + 'perf_hooks', 16 + ] 17 + 18 + const polyfills = [ 19 + 'util', 20 + 'tty', 21 + 'process', 22 + 'path', 23 + 'buffer', 24 + ] 25 + 26 + export default (base = '/'): Plugin[] => { 27 + const pkgRoot = resolve(fileURLToPath(import.meta.url), '../..') 28 + const distRoot = resolve(pkgRoot, 'dist') 29 + 30 + return [ 31 + { 32 + enforce: 'pre', 33 + name: 'vitest:browser', 34 + async resolveId(id, _, ctx) { 35 + if (ctx.ssr) 36 + return 37 + 38 + if (id === '/__vitest_index__') { 39 + const result = await resolvePath('vitest/browser') 40 + return result 41 + } 42 + 43 + if (stubs.includes(id)) 44 + return resolve(pkgRoot, 'stubs', id) 45 + 46 + if (polyfills.includes(id)) 47 + return polyfillPath(normalizeId(id)) 48 + 49 + return null 50 + }, 51 + async configureServer(server) { 52 + server.middlewares.use( 53 + base, 54 + sirv(resolve(distRoot, 'client'), { 55 + single: false, 56 + dev: true, 57 + }), 58 + ) 59 + }, 60 + }, 61 + { 62 + name: 'modern-node-polyfills', 63 + async resolveId(id, _, ctx) { 64 + if (ctx.ssr || !builtinModules.includes(id)) 65 + return 66 + 67 + id = normalizeId(id) 68 + return { id: await polyfillPath(id), moduleSideEffects: false } 69 + }, 70 + }, 71 + ] 72 + } 73 + 74 + function normalizeId(id: string, base?: string): string { 75 + if (base && id.startsWith(base)) 76 + id = `/${id.slice(base.length)}` 77 + 78 + return id 79 + .replace(/^\/@id\/__x00__/, '\0') // virtual modules start with `\0` 80 + .replace(/^\/@id\//, '') 81 + .replace(/^__vite-browser-external:/, '') 82 + .replace(/^node:/, '') 83 + .replace(/[?&]v=\w+/, '?') // remove ?v= query 84 + .replace(/\?$/, '') // remove end query mark 85 + }
+5
packages/browser/public/favicon.svg
··· 1 + <svg width="165" height="165" viewBox="0 0 165 165" fill="none" xmlns="http://www.w3.org/2000/svg"> 2 + <path d="M120.831 57.2543L84.693 109.505C84.3099 110.059 83.7558 110.474 83.1148 110.687C82.4738 110.9 81.7809 110.898 81.1412 110.684C80.5015 110.469 79.95 110.052 79.5702 109.497C79.1905 108.941 79.0032 108.277 79.037 107.606L80.4833 78.7582L57.1343 73.8064C56.6353 73.7007 56.1704 73.474 55.7807 73.1465C55.391 72.8191 55.0885 72.4009 54.9001 71.929C54.7117 71.4571 54.6432 70.9461 54.7006 70.4412C54.758 69.9364 54.9395 69.4532 55.2291 69.0345L91.3675 16.7837C91.7507 16.2294 92.3048 15.8145 92.9458 15.6018C93.5869 15.3891 94.2798 15.3902 94.9196 15.6051C95.5593 15.8199 96.1109 16.2367 96.4906 16.7923C96.8703 17.3478 97.0575 18.0117 97.0236 18.6833L95.5773 47.5314L118.926 52.4828C119.425 52.5885 119.89 52.8152 120.28 53.1426C120.67 53.4701 120.972 53.8883 121.16 54.3602C121.349 54.8321 121.417 55.3431 121.36 55.8479C121.303 56.3528 121.121 56.836 120.831 57.2547L120.831 57.2543Z" fill="#FCC72B"/> 3 + <path d="M82.9866 153.343C82.0254 153.344 81.0735 153.156 80.1855 152.788C79.2975 152.42 78.4909 151.88 77.8122 151.2L43.6658 117.056C42.2998 115.683 41.5341 113.824 41.5366 111.887C41.5392 109.95 42.3098 108.092 43.6796 106.723C45.0493 105.353 46.9064 104.582 48.8435 104.579C50.7807 104.577 52.6399 105.342 54.0134 106.708L82.9866 135.678L146.105 72.5626C147.481 71.2088 149.336 70.4536 151.266 70.4615C153.197 70.4693 155.046 71.2396 156.41 72.6045C157.775 73.9695 158.546 75.8184 158.554 77.7487C158.561 79.679 157.806 81.5342 156.452 82.9101L88.1597 151.2C87.4811 151.881 86.6747 152.42 85.7869 152.788C84.8992 153.156 83.9475 153.344 82.9866 153.343Z" fill="#729B1B"/> 4 + <path d="M82.9572 153.343C83.9184 153.344 84.8703 153.156 85.7583 152.788C86.6463 152.42 87.4528 151.88 88.1316 151.2L122.278 117.056C123.644 115.683 124.41 113.824 124.407 111.887C124.405 109.95 123.634 108.092 122.264 106.723C120.894 105.353 119.037 104.582 117.1 104.579C115.163 104.577 113.304 105.342 111.93 106.708L82.9572 135.678L19.8389 72.5626C18.4629 71.2088 16.6077 70.4536 14.6775 70.4615C12.7472 70.4693 10.8982 71.2396 9.53331 72.6045C8.16839 73.9695 7.39811 75.8184 7.39025 77.7487C7.38239 79.679 8.13759 81.5342 9.49135 82.9101L77.784 151.2C78.4627 151.881 79.2691 152.42 80.1568 152.788C81.0446 153.156 81.9963 153.344 82.9572 153.343Z" fill="#729B1B" fill-opacity="0.5"/> 5 + </svg>
+19
packages/browser/scripts/copy-ui-to-browser.ts
··· 1 + import { fileURLToPath } from 'url' 2 + import fs from 'fs' 3 + import { resolve } from 'pathe' 4 + import fg from 'fast-glob' 5 + 6 + const root = resolve(fileURLToPath(import.meta.url), '../../../../packages') 7 + 8 + const ui = resolve(root, 'ui/dist/client') 9 + const browser = resolve(root, 'browser/dist/client/__vitest__/') 10 + 11 + const files = fg.sync('**/*', { cwd: ui }) 12 + 13 + fs.mkdirSync(browser) 14 + fs.mkdirSync(resolve(browser, 'assets')) 15 + 16 + files.forEach((f) => { 17 + fs.copyFileSync(resolve(ui, f), resolve(browser, f)) 18 + }) 19 +
+7
packages/browser/stubs/fs.js
··· 1 + import noop from './noop' 2 + 3 + export default noop 4 + export { noop as promises } 5 + export function existsSync() { 6 + return false 7 + }
+2
packages/browser/stubs/local-pkg.js
··· 1 + export const isPackageExists = () => {} 2 + export const importModule = () => {}
+1
packages/browser/stubs/module.js
··· 1 + export const createRequire = () => {}
+5
packages/browser/stubs/noop.js
··· 1 + export default new Proxy({}, { 2 + get() { 3 + throw new Error('Module has been externalized for browser compatibility and cannot be accessed in client code.') 4 + }, 5 + })
+1
packages/browser/stubs/perf_hooks.js
··· 1 + export const performance = globalThis.performance
+3
packages/browser/stubs/tty.js
··· 1 + export const isatty = () => false 2 + 3 + export default { isatty }
+8
packages/ui/client/auto-imports.d.ts
··· 75 75 const refThrottled: typeof import('@vueuse/core')['refThrottled'] 76 76 const refWithControl: typeof import('@vueuse/core')['refWithControl'] 77 77 const resolveComponent: typeof import('vue')['resolveComponent'] 78 + const resolveRef: typeof import('@vueuse/core')['resolveRef'] 79 + const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 78 80 const shallowReactive: typeof import('vue')['shallowReactive'] 79 81 const shallowReadonly: typeof import('vue')['shallowReadonly'] 80 82 const shallowRef: typeof import('vue')['shallowRef'] ··· 141 143 const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 142 144 const useFavicon: typeof import('@vueuse/core')['useFavicon'] 143 145 const useFetch: typeof import('@vueuse/core')['useFetch'] 146 + const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 144 147 const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 145 148 const useFocus: typeof import('@vueuse/core')['useFocus'] 146 149 const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] ··· 171 174 const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 172 175 const useNetwork: typeof import('@vueuse/core')['useNetwork'] 173 176 const useNow: typeof import('@vueuse/core')['useNow'] 177 + const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 174 178 const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 175 179 const useOnline: typeof import('@vueuse/core')['useOnline'] 176 180 const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] ··· 196 200 const useSlots: typeof import('vue')['useSlots'] 197 201 const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 198 202 const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 203 + const useStepper: typeof import('@vueuse/core')['useStepper'] 199 204 const useStorage: typeof import('@vueuse/core')['useStorage'] 200 205 const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 201 206 const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 202 207 const useSwipe: typeof import('@vueuse/core')['useSwipe'] 203 208 const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 204 209 const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 210 + const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 205 211 const useThrottle: typeof import('@vueuse/core')['useThrottle'] 206 212 const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 207 213 const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] ··· 228 234 const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 229 235 const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 230 236 const watch: typeof import('vue')['watch'] 237 + const watchArray: typeof import('@vueuse/core')['watchArray'] 231 238 const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 232 239 const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 233 240 const watchEffect: typeof import('vue')['watchEffect'] ··· 237 244 const watchPostEffect: typeof import('vue')['watchPostEffect'] 238 245 const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 239 246 const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 247 + const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 240 248 const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 241 249 const whenever: typeof import('@vueuse/core')['whenever'] 242 250 }
+6
packages/vitest/src/browser.ts
··· 1 + export { suite, test, describe, it } from './runtime/suite' 2 + export * from './runtime/hooks' 3 + export * from './integrations/chai' 4 + export { startTests } from './runtime/run' 5 + export { setupGlobalEnv } from './runtime/setup' 6 + export * from './types'
+2 -2
packages/vitest/src/constants.ts
··· 1 - import { fileURLToPath } from 'url' 1 + import url from 'url' 2 2 import { resolve } from 'pathe' 3 3 4 - export const distDir = resolve(fileURLToPath(import.meta.url), '../../dist') 4 + export const distDir = resolve(url.fileURLToPath(import.meta.url), '../../dist') 5 5 6 6 // if changed, update also jsdocs and docs 7 7 export const defaultPort = 51204
-1
packages/vitest/src/index.ts
··· 9 9 10 10 export * from './types' 11 11 export * from './api/types' 12 -
+31 -23
packages/ws-client/src/index.ts
··· 1 - import type { BirpcReturn } from 'birpc' 1 + import type { BirpcOptions, BirpcReturn } from 'birpc' 2 2 import { createBirpc } from 'birpc' 3 3 import { parse, stringify } from 'flatted' 4 4 // eslint-disable-next-line no-restricted-imports ··· 47 47 ctx.state.idMap = reactive(ctx.state.idMap) 48 48 49 49 let onMessage: Function 50 + const functions: WebSocketEvents = { 51 + onPathsCollected(paths) { 52 + ctx.state.collectPaths(paths) 53 + handlers.onPathsCollected?.(paths) 54 + }, 55 + onCollected(files) { 56 + ctx.state.collectFiles(files) 57 + handlers.onCollected?.(files) 58 + }, 59 + onTaskUpdate(packs) { 60 + ctx.state.updateTasks(packs) 61 + handlers.onTaskUpdate?.(packs) 62 + }, 63 + onUserConsoleLog(log) { 64 + ctx.state.updateUserLog(log) 65 + }, 66 + onFinished(files) { 67 + handlers.onFinished?.(files) 68 + }, 69 + } 70 + 71 + const birpcHandlers: BirpcOptions<WebSocketHandlers> = { 72 + post: msg => ctx.ws.send(msg), 73 + on: fn => (onMessage = fn), 74 + serialize: stringify, 75 + deserialize: parse, 76 + } 77 + 50 78 ctx.rpc = createBirpc<WebSocketHandlers, WebSocketEvents>( 51 - { 52 - onCollected(files) { 53 - ctx.state.collectFiles(files) 54 - handlers.onCollected?.(files) 55 - }, 56 - onTaskUpdate(packs) { 57 - ctx.state.updateTasks(packs) 58 - handlers.onTaskUpdate?.(packs) 59 - }, 60 - onUserConsoleLog(log) { 61 - ctx.state.updateUserLog(log) 62 - }, 63 - onFinished(files) { 64 - handlers.onFinished?.(files) 65 - }, 66 - }, 67 - { 68 - post: msg => ctx.ws.send(msg), 69 - on: fn => onMessage = fn, 70 - serialize: stringify, 71 - deserialize: parse, 72 - }, 79 + functions, 80 + birpcHandlers, 73 81 ) 74 82 75 83 let openPromise: Promise<void>
+5
test/browser/test/another.test.ts
··· 1 + import { expect, it } from 'vitest' 2 + 3 + it('basic 3', async () => { 4 + expect(globalThis.window).toBeDefined() 5 + })
+9
test/browser/test/basic.test.ts
··· 1 + import { expect, it } from 'vitest' 2 + 3 + it('basic', async () => { 4 + expect(globalThis.performance).toBeDefined() 5 + }) 6 + 7 + it('basic 2', () => { 8 + expect(globalThis.window).toBeDefined() 9 + })
+17
packages/vitest/src/api/setup.ts
··· 33 33 function setupClient(ws: WebSocket) { 34 34 const rpc = createBirpc<WebSocketEvents, WebSocketHandlers>( 35 35 { 36 + async onWatcherStart() { 37 + await ctx.report('onWatcherStart') 38 + }, 39 + async onFinished() { 40 + await ctx.report('onFinished') 41 + }, 42 + async onCollected(files) { 43 + ctx.state.collectFiles(files) 44 + await ctx.report('onCollected', files) 45 + }, 46 + async onTaskUpdate(packs) { 47 + ctx.state.updateTasks(packs) 48 + await ctx.report('onTaskUpdate', packs) 49 + }, 36 50 getFiles() { 37 51 return ctx.state.getFiles() 52 + }, 53 + getPaths() { 54 + return ctx.state.getPaths() 38 55 }, 39 56 readFile(id) { 40 57 return fs.readFile(id, 'utf-8')
+7 -2
packages/vitest/src/api/types.ts
··· 1 1 import type { TransformResult } from 'vite' 2 - import type { File, ModuleGraphData, Reporter, ResolvedConfig } from '../types' 2 + import type { File, ModuleGraphData, Reporter, ResolvedConfig, TaskResultPack } from '../types' 3 3 4 4 export interface TransformResultWithSource extends TransformResult { 5 5 source?: string 6 6 } 7 7 8 8 export interface WebSocketHandlers { 9 + onWatcherStart: () => Promise<void> 10 + onFinished(files?: File[]): Promise<void> 11 + onCollected(files?: File[]): Promise<void> 12 + onTaskUpdate(packs: TaskResultPack[]): void 9 13 getFiles(): File[] 14 + getPaths(): string[] 10 15 getConfig(): ResolvedConfig 11 16 getModuleGraph(id: string): Promise<ModuleGraphData> 12 17 getTransformResult(id: string): Promise<TransformResultWithSource | undefined> ··· 16 21 updateSnapshot(file?: File): Promise<void> 17 22 } 18 23 19 - export interface WebSocketEvents extends Pick<Reporter, 'onCollected' | 'onFinished' | 'onTaskUpdate' | 'onUserConsoleLog'> { 24 + export interface WebSocketEvents extends Pick<Reporter, 'onCollected' | 'onFinished' | 'onTaskUpdate' | 'onUserConsoleLog' | 'onPathsCollected'> { 20 25 }
+3 -3
packages/vitest/src/integrations/coverage.ts
··· 1 1 import { existsSync, promises as fs } from 'fs' 2 2 import { createRequire } from 'module' 3 - import { pathToFileURL } from 'url' 3 + import _url from 'url' 4 4 import type { Profiler } from 'inspector' 5 5 import { resolve } from 'pathe' 6 6 import type { RawSourceMap } from 'vite-node' ··· 57 57 if (!map) 58 58 return 59 59 60 - const url = pathToFileURL(file).href 60 + const url = _url.pathToFileURL(file).href 61 61 62 62 let code: string | undefined 63 63 try { ··· 85 85 const offset = 224 86 86 87 87 report._getSourceMap = (coverage: Profiler.ScriptCoverage) => { 88 - const path = pathToFileURL(coverage.url).href 88 + const path = _url.pathToFileURL(coverage.url).href 89 89 const data = sourceMapMeta[path] 90 90 91 91 if (!data)
+2
packages/vitest/src/node/cli-api.ts
··· 20 20 21 21 if (options.run) 22 22 options.watch = false 23 + if (options.browser) // enabling threads in browser mode causes inconsistensies 24 + options.threads = false 23 25 24 26 // this shouldn't affect _application root_ that can be changed inside config 25 27 const root = resolve(options.root || process.cwd())
+1
packages/vitest/src/node/cli.ts
··· 30 30 .option('--mode <name>', 'override Vite mode (default: test)') 31 31 .option('--globals', 'inject apis globally') 32 32 .option('--dom', 'mock browser api with happy-dom') 33 + .option('--browser', 'run tests in browser') 33 34 .option('--environment <env>', 'runner environment (default: node)') 34 35 .option('--passWithNoTests', 'pass when no tests found') 35 36 .option('--allowOnly', 'Allow tests and suites that are marked as only (default: !process.env.CI)')
+1 -1
packages/vitest/src/node/config.ts
··· 32 32 ): ApiConfig | undefined { 33 33 let api: ApiConfig | undefined 34 34 35 - if (options.ui && !options.api) 35 + if ((options.ui || options.browser) && !options.api) 36 36 api = { port: defaultPort } 37 37 else if (options.api === true) 38 38 api = { port: defaultPort }
+8 -6
packages/vitest/src/node/core.ts
··· 15 15 import { createReporters } from './reporters/utils' 16 16 import { StateManager } from './state' 17 17 import { resolveConfig } from './config' 18 - import { VitestGit } from './git' 19 18 import { Logger } from './logger' 20 19 import { VitestCache } from './cache' 21 20 ··· 141 140 if (this.config.coverage.enabled) 142 141 await reportCoverage(this) 143 142 144 - if (this.config.watch) 143 + if (this.config.watch && !this.config.browser) 145 144 await this.report('onWatcherStart') 146 145 } 147 146 ··· 171 170 172 171 async filterTestsBySource(tests: string[]) { 173 172 if (this.config.changed && !this.config.related) { 173 + const { VitestGit } = await import('./git') 174 174 const vitestGit = new VitestGit(this.config.root) 175 175 const related = await vitestGit.findChangedFiles({ 176 176 changedSince: this.config.changed, ··· 237 237 if (hasFailed(files)) 238 238 process.exitCode = 1 239 239 240 - await this.report('onFinished', files, this.state.getUnhandledErrors()) 241 - 240 + if (!this.config.browser) 241 + await this.report('onFinished', files, this.state.getUnhandledErrors()) 242 242 this.cache.results.updateResults(files) 243 243 await this.cache.results.writeToCache() 244 244 })() ··· 252 252 async rerunFiles(files: string[] = this.state.getFilepaths(), trigger?: string) { 253 253 await this.report('onWatcherRerun', files, trigger) 254 254 await this.runFiles(files) 255 - await this.report('onWatcherStart') 255 + if (!this.config.browser) 256 + await this.report('onWatcherStart') 256 257 } 257 258 258 259 async changeNamePattern(pattern: string, files: string[] = this.state.getFilepaths(), trigger?: string) { ··· 329 330 if (this.config.coverage.enabled) 330 331 await reportCoverage(this) 331 332 332 - await this.report('onWatcherStart') 333 + if (!this.config.browser) 334 + await this.report('onWatcherStart') 333 335 }, WATCHER_DEBOUNCE) 334 336 } 335 337
+3 -1
packages/vitest/src/node/logger.ts
··· 95 95 96 96 this.log(`${c.inverse(c.bold(mode))} ${versionTest} ${c.gray(this.ctx.config.root)}`) 97 97 98 - if (this.ctx.config.ui) 98 + if (this.ctx.config.browser) 99 + this.log(c.dim(c.green(` Browser runner started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.server.config.server.port}`)}`))) 100 + else if (this.ctx.config.ui) 99 101 this.log(c.dim(c.green(` UI started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.server.config.server.port}`)}${this.ctx.config.uiBase}`))) 100 102 else if (this.ctx.config.api) 101 103 this.log(c.dim(c.green(` API started at http://${this.ctx.config.api?.host || 'localhost'}:${c.bold(`${this.ctx.config.api.port}`)}`)))
+6 -2
packages/vitest/src/node/pool.ts
··· 1 1 import { MessageChannel } from 'worker_threads' 2 - import { pathToFileURL } from 'url' 2 + import _url from 'url' 3 3 import { cpus } from 'os' 4 4 import { resolve } from 'pathe' 5 5 import type { Options as TinypoolOptions } from 'tinypool' ··· 18 18 close: () => Promise<void> 19 19 } 20 20 21 - const workerPath = pathToFileURL(resolve(distDir, './worker.mjs')).href 21 + const workerPath = _url.pathToFileURL(resolve(distDir, './worker.mjs')).href 22 22 23 23 export function createPool(ctx: Vitest): WorkerPool { 24 24 const threadsCount = ctx.config.watch ··· 147 147 }, 148 148 resolveId(id, importer) { 149 149 return ctx.vitenode.resolveId(id, importer) 150 + }, 151 + onPathsCollected(paths) { 152 + ctx.state.collectPaths(paths) 153 + ctx.report('onPathsCollected', paths) 150 154 }, 151 155 onCollected(files) { 152 156 ctx.state.collectFiles(files)
+11
packages/vitest/src/node/state.ts
··· 3 3 // Note this file is shared for both node and browser, be aware to avoid node specific logic 4 4 export class StateManager { 5 5 filesMap = new Map<string, File>() 6 + pathsSet: Set<string> = new Set() 6 7 idMap = new Map<string, Task>() 7 8 taskFileMap = new WeakMap<Task, File>() 8 9 errorsSet = new Set<unknown>() ··· 20 21 return Array.from(this.errorsSet.values()) 21 22 } 22 23 24 + getPaths() { 25 + return Array.from(this.pathsSet) 26 + } 27 + 23 28 getFiles(keys?: string[]): File[] { 24 29 if (keys) 25 30 return keys.map(key => this.filesMap.get(key)!).filter(Boolean) ··· 34 39 return this.getFiles() 35 40 .filter(i => i.result?.state === 'fail') 36 41 .map(i => i.filepath) 42 + } 43 + 44 + collectPaths(paths: string[] = []) { 45 + paths.forEach((path) => { 46 + this.pathsSet.add(path) 47 + }) 37 48 } 38 49 39 50 collectFiles(files: File[] = []) {
+20 -3
packages/vitest/src/runtime/collect.ts
··· 1 - import { relative } from 'pathe' 2 1 import type { File, ResolvedConfig, Suite, TaskBase } from '../types' 2 + import { getWorkerState, isBrowser, relativePath } from '../utils' 3 3 import { clearCollectorContext, defaultSuite } from './suite' 4 4 import { getHooks, setHooks } from './map' 5 5 import { processError } from './error' ··· 23 23 export async function collectTests(paths: string[], config: ResolvedConfig) { 24 24 const files: File[] = [] 25 25 26 + const browserHashMap = getWorkerState().browserHashMap! 27 + 28 + async function importFromBrowser(filepath: string) { 29 + const match = filepath.match(/^(\w:\/)/) 30 + const hash = browserHashMap.get(filepath) 31 + if (match) 32 + return await import(`/@fs/${filepath.slice(match[1].length)}?v=${hash}`) 33 + else 34 + return await import(`${filepath}?v=${hash}`) 35 + } 36 + 26 37 for (const filepath of paths) { 27 - const path = relative(config.root, filepath) 38 + const path = relativePath(config.root, filepath) 28 39 const file: File = { 29 40 id: hash(path), 30 41 name: path, ··· 37 48 clearCollectorContext() 38 49 try { 39 50 await runSetupFiles(config) 40 - await import(filepath) 51 + 52 + if (config.browser && isBrowser) 53 + await importFromBrowser(filepath) 54 + else 55 + await import(filepath) 41 56 42 57 const defaultTasks = await defaultSuite.collect(file) 43 58 ··· 64 79 state: 'fail', 65 80 error: processError(e), 66 81 } 82 + if (config.browser) 83 + console.error(e) 67 84 } 68 85 69 86 calculateHash(file)
+7
packages/vitest/src/runtime/entry.ts
··· 10 10 11 11 const workerState = getWorkerState() 12 12 13 + // TODO @web-runner: we need to figure out how to do this on the browser 14 + if (config.browser) { 15 + workerState.mockMap.clear() 16 + await startTests(files, config) 17 + return 18 + } 19 + 13 20 // if calling from a worker, there will always be one file 14 21 // if calling with no-threads, this will be the whole suite 15 22 const filesWithEnv = await Promise.all(files.map(async (file) => {
+4 -4
packages/vitest/src/runtime/error.ts
··· 1 - import { format } from 'util' 2 - import { util } from 'chai' 1 + import util from 'util' 2 + import { util as ChaiUtil } from 'chai' 3 3 import { stringify } from '../integrations/chai/jest-matcher-utils' 4 4 import { deepClone, getType } from '../utils' 5 5 ··· 18 18 if (typeof Element !== 'undefined' && val instanceof Element) 19 19 return val.tagName 20 20 if (typeof val.asymmetricMatch === 'function') 21 - return `${val.toString()} ${format(val.sample)}` 21 + return `${val.toString()} ${util.format(val.sample)}` 22 22 23 23 if (seen.has(val)) 24 24 return seen.get(val) ··· 112 112 return { replacedActual: actual, replacedExpected: expected } 113 113 actualReplaced.set(actual, true) 114 114 expectedReplaced.set(expected, true) 115 - util.getOwnEnumerableProperties(expected).forEach((key) => { 115 + ChaiUtil.getOwnEnumerableProperties(expected).forEach((key) => { 116 116 const expectedValue = expected[key] 117 117 const actualValue = actual[key] 118 118 if (isAsymmetricMatcher(expectedValue)) {
+42 -7
packages/vitest/src/runtime/run.ts
··· 1 1 import limit from 'p-limit' 2 2 import type { File, HookCleanupCallback, HookListener, ResolvedConfig, Suite, SuiteHooks, Task, TaskResult, TaskState, Test } from '../types' 3 3 import { vi } from '../integrations/vi' 4 - import { getSnapshotClient } from '../integrations/snapshot/chai' 5 - import { clearTimeout, getFullName, getWorkerState, hasFailed, hasTests, partitionSuiteChildren, setTimeout, shuffle } from '../utils' 4 + import { clearTimeout, getFullName, getWorkerState, hasFailed, hasTests, isBrowser, isNode, partitionSuiteChildren, setTimeout, shuffle } from '../utils' 6 5 import { takeCoverage } from '../integrations/coverage' 7 6 import { getState, setState } from '../integrations/chai/jest-expect' 8 7 import { GLOBAL_EXPECT } from '../integrations/chai/constants' ··· 79 78 80 79 export async function runTest(test: Test) { 81 80 if (test.mode !== 'run') { 81 + const { getSnapshotClient } = await import('../integrations/snapshot/chai') 82 82 getSnapshotClient().skipTestSnapshots(test) 83 83 return 84 84 } ··· 98 98 99 99 clearModuleMocks() 100 100 101 - await getSnapshotClient().setTest(test) 101 + if (isNode) { 102 + const { getSnapshotClient } = await import('../integrations/snapshot/chai') 103 + await getSnapshotClient().setTest(test) 104 + } 102 105 103 106 const workerState = getWorkerState() 104 107 ··· 160 163 } 161 164 } 162 165 163 - getSnapshotClient().clearTest() 166 + if (isBrowser && test.result.error) 167 + console.error(test.result.error.message, test.result.error.stackStr) 168 + 169 + if (isNode) { 170 + const { getSnapshotClient } = await import('../integrations/snapshot/chai') 171 + getSnapshotClient().clearTest() 172 + } 164 173 165 174 test.result.duration = now() - start 166 175 167 - if (workerState.config.logHeapUsage) 176 + if (workerState.config.logHeapUsage && isNode) 168 177 test.result.heap = process.memoryUsage().heapUsed 169 178 170 179 workerState.current = undefined ··· 239 248 } 240 249 suite.result.duration = now() - start 241 250 242 - if (workerState.config.logHeapUsage) 251 + if (workerState.config.logHeapUsage && isNode) 243 252 suite.result.heap = process.memoryUsage().heapUsed 244 253 245 254 if (suite.mode === 'run') { ··· 265 274 : runSuite(c) 266 275 } 267 276 277 + async function runSuites(suites: Suite[]) { 278 + for (const suite of suites) 279 + await runSuite(suite) 280 + } 281 + 268 282 export async function runFiles(files: File[], config: ResolvedConfig) { 269 283 for (const file of files) { 270 284 if (!file.tasks.length && !config.passWithNoTests) { ··· 280 294 } 281 295 } 282 296 283 - export async function startTests(paths: string[], config: ResolvedConfig) { 297 + async function startTestsBrowser(paths: string[], config: ResolvedConfig) { 298 + if (isNode) { 299 + rpc().onPathsCollected(paths) 300 + } 301 + else { 302 + const files = await collectTests(paths, config) 303 + await rpc().onCollected(files) 304 + await runSuites(files) 305 + await sendTasksUpdate() 306 + } 307 + } 308 + 309 + async function startTestsNode(paths: string[], config: ResolvedConfig) { 284 310 const files = await collectTests(paths, config) 285 311 286 312 rpc().onCollected(files) 313 + 314 + const { getSnapshotClient } = await import('../integrations/snapshot/chai') 287 315 getSnapshotClient().clear() 288 316 289 317 await runFiles(files, config) ··· 293 321 await getSnapshotClient().saveCurrent() 294 322 295 323 await sendTasksUpdate() 324 + } 325 + 326 + export async function startTests(paths: string[], config: ResolvedConfig) { 327 + if (config.browser) 328 + return startTestsBrowser(paths, config) 329 + else 330 + return startTestsNode(paths, config) 296 331 } 297 332 298 333 export function clearModuleMocks() {
+2 -2
packages/vitest/src/runtime/suite.ts
··· 1 - import { format } from 'util' 1 + import util from 'util' 2 2 import type { File, RunMode, Suite, SuiteAPI, SuiteCollector, SuiteFactory, SuiteHooks, Task, Test, TestAPI, TestFunction } from '../types' 3 3 import { getWorkerState, isObject, noop } from '../utils' 4 4 import { createChainable } from './chain' ··· 24 24 } 25 25 26 26 const count = template.split('%').length - 1 27 - let formatted = format(template, ...items.slice(0, count)) 27 + let formatted = util.format(template, ...items.slice(0, count)) 28 28 if (isObject(items[0])) { 29 29 formatted = formatted.replace(/\$([$\w_]+)/g, (_, key) => { 30 30 return items[0][key]
+6
packages/vitest/src/types/config.ts
··· 270 270 ui?: boolean 271 271 272 272 /** 273 + * Use in browser environment 274 + * @experimental 275 + */ 276 + browser?: boolean 277 + 278 + /** 273 279 * Open UI automatically. 274 280 * 275 281 * @default true
+1
packages/vitest/src/types/reporter.ts
··· 4 4 5 5 export interface Reporter { 6 6 onInit?(ctx: Vitest): void 7 + onPathsCollected?: (paths?: string[]) => Awaitable<void> 7 8 onCollected?: (files?: File[]) => Awaitable<void> 8 9 onFinished?: (files?: File[], errors?: unknown[]) => Awaitable<void> 9 10 onTaskUpdate?: (packs: TaskResultPack[]) => Awaitable<void>
+2
packages/vitest/src/types/worker.ts
··· 24 24 25 25 onFinished: (files: File[], errors?: unknown[]) => void 26 26 onWorkerExit: (code?: number) => void 27 + onPathsCollected: (paths: string[]) => void 27 28 onUserConsoleLog: (log: UserConsoleLog) => void 28 29 onUnhandledRejection: (err: unknown) => void 29 30 onCollected: (files: File[]) => void ··· 40 41 current?: Test 41 42 filepath?: string 42 43 moduleCache: ModuleCacheMap 44 + browserHashMap?: Map<string, string> 43 45 mockMap: MockMap 44 46 }
+9 -2
packages/vitest/src/utils/index.ts
··· 1 + // eslint-disable-next-line no-restricted-imports 2 + import { relative as relativeBrowser } from 'path' 1 3 import c from 'picocolors' 2 4 import { isPackageExists } from 'local-pkg' 3 - import { resolve } from 'pathe' 5 + import { relative as relativeNode } from 'pathe' 4 6 import type { Suite, Task } from '../types' 5 7 import { getWorkerState } from '../utils/global' 6 8 import { getNames } from './tasks' ··· 10 12 export * from './global' 11 13 export * from './timers' 12 14 13 - export const isNode = typeof process !== 'undefined' && typeof process.platform !== 'undefined' 15 + export const isNode = typeof process < 'u' && typeof process.stdout < 'u' && !process.versions?.deno && !globalThis.window 16 + // export const isNode = typeof process !== 'undefined' && typeof process.platform !== 'undefined' 14 17 export const isBrowser = typeof window !== 'undefined' 15 18 export const isWindows = isNode && process.platform === 'win32' 19 + 20 + export const relativePath = isBrowser ? relativeBrowser : relativeNode 16 21 17 22 /** 18 23 * Partition in tasks groups by consecutive concurrent ··· 129 134 } 130 135 return null 131 136 } 137 + 138 + const resolve = isNode ? relativeNode : relativeBrowser 132 139 133 140 export { resolve as resolvePath } 134 141
+3 -3
packages/vitest/src/integrations/chai/index.ts
··· 1 - import chai, { util } from 'chai' 1 + import * as chai from 'chai' 2 2 import './setup' 3 3 import type { Test } from '../../types' 4 4 import { getFullName } from '../../utils' ··· 57 57 }) 58 58 } 59 59 60 - util.addMethod(expect, 'assertions', assertions) 61 - util.addMethod(expect, 'hasAssertions', hasAssertions) 60 + chai.util.addMethod(expect, 'assertions', assertions) 61 + chai.util.addMethod(expect, 'hasAssertions', hasAssertions) 62 62 63 63 return expect 64 64 }
+1 -1
packages/vitest/src/integrations/chai/setup.ts
··· 1 - import chai from 'chai' 1 + import * as chai from 'chai' 2 2 import Subset from 'chai-subset' 3 3 import { SnapshotPlugin } from '../snapshot/chai' 4 4 import { JestExtend } from './jest-extend'
+2 -2
packages/vitest/src/integrations/env/jsdom.ts
··· 1 - import type { Environment, JSDOMOptions } from '../../types' 1 + import type { Environment } from '../../types' 2 2 import { populateGlobal } from './utils' 3 3 4 4 export default <Environment>({ ··· 22 22 console = false, 23 23 cookieJar = false, 24 24 ...restOptions 25 - } = jsdom as JSDOMOptions 25 + } = jsdom as any 26 26 const dom = new JSDOM( 27 27 html, 28 28 {
+25 -9
packages/vitest/src/node/plugins/index.ts
··· 17 17 return (await import('@vitest/ui')).default(options.uiBase) 18 18 } 19 19 20 + async function BrowserPlugin() { 21 + await ensurePackageInstalled('@vitest/browser', ctx.config?.root || options.root || process.cwd()) 22 + return (await import('@vitest/browser')).default('/') 23 + } 24 + 20 25 return [ 21 26 <VitePlugin>{ 22 27 name: 'vitest', ··· 66 71 67 72 (options as ResolvedConfig).defines = defines 68 73 69 - const open = preOptions.ui && preOptions.open 70 - ? preOptions.uiBase ?? '/__vitest__/' 71 - : undefined 74 + let open: string | boolean | undefined 75 + 76 + if (preOptions.ui && preOptions.open) 77 + open = preOptions.uiBase ?? '/__vitest__/' 78 + else if (preOptions.browser) 79 + open = '/' 72 80 73 81 const config: ViteConfig = { 74 82 resolve: { ··· 86 94 hmr: false, 87 95 preTransformRequests: false, 88 96 }, 97 + } 98 + 99 + if (!options.browser) { 89 100 // disable deps optimization 90 - cacheDir: undefined, 91 - optimizeDeps: { 92 - // experimental in Vite >2.9.2, entries remains to help with older versions 93 - disabled: true, 94 - entries: [], 95 - }, 101 + Object.assign(config, { 102 + cacheDir: undefined, 103 + optimizeDeps: { 104 + // experimental in Vite >2.9.2, entries remains to help with older versions 105 + disabled: true, 106 + entries: [], 107 + }, 108 + }) 96 109 } 97 110 98 111 return config ··· 149 162 EnvReplacerPlugin(), 150 163 MocksPlugin(), 151 164 GlobalSetupPlugin(ctx), 165 + ...(options.browser 166 + ? await BrowserPlugin() 167 + : []), 152 168 CSSEnablerPlugin(ctx), 153 169 options.ui 154 170 ? await UIPlugin()
+2 -3
packages/vitest/src/node/reporters/base.ts
··· 1 1 import { performance } from 'perf_hooks' 2 - import { relative } from 'pathe' 3 2 import c from 'picocolors' 4 3 import type { ErrorWithDiff, File, Reporter, Task, TaskResultPack, UserConsoleLog } from '../../types' 5 - import { clearInterval, getFullName, getSuites, getTests, hasFailed, hasFailedSnapshot, isNode, setInterval } from '../../utils' 4 + import { clearInterval, getFullName, getSuites, getTests, hasFailed, hasFailedSnapshot, isNode, relativePath, setInterval } from '../../utils' 6 5 import type { Vitest } from '../../node' 7 6 import { F_RIGHT } from '../../utils/figures' 8 7 import { divider, getStateString, getStateSymbol, pointer, renderSnapshotSummary } from './renderers/utils' ··· 42 41 } 43 42 44 43 relative(path: string) { 45 - return relative(this.ctx.config.root, path) 44 + return relativePath(this.ctx.config.root, path) 46 45 } 47 46 48 47 async onFinished(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) {
+3 -3
packages/vitest/src/integrations/snapshot/port/utils.ts
··· 5 5 * LICENSE file in the root directory of this source tree. 6 6 */ 7 7 8 - import fs, { promises as fsp } from 'fs' 8 + import fs from 'fs' 9 9 import { dirname, join } from 'pathe' 10 10 import naturalCompare from 'natural-compare' 11 11 import type { OptionsReceived as PrettyFormatOptions } from 'pretty-format' ··· 152 152 ) 153 153 154 154 const content = `${writeSnapshotVersion()}\n\n${snapshots.join('\n\n')}\n` 155 - const skipWriting = fs.existsSync(snapshotPath) && (await fsp.readFile(snapshotPath, 'utf8')) === content 155 + const skipWriting = fs.existsSync(snapshotPath) && (await fs?.promises.readFile(snapshotPath, 'utf8')) === content 156 156 157 157 if (skipWriting) 158 158 return 159 159 160 160 ensureDirectoryExists(snapshotPath) 161 - await fsp.writeFile( 161 + await fs?.promises.writeFile( 162 162 snapshotPath, 163 163 content, 164 164 'utf-8',