[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: add html reporter based on Vitest UI (#2444)

* feat: vitest html report

* chore: copy ui to vitest dist

* feat: report copy ui dist

* feat: ui report version builder

* fix: copy file

* chore: remove

* feat: html metadata path

* feat: add declare

* feat: static file client

* feat: mock rpc

* fix: mock error

* chore: update meta

* chore: compress json

* chore: comment

* chore: update merge config

* chore: lock

* feat: remove all control command from ui

* chore: remove

* feat: report command

* feat: version tag

* chore: reset lock file

* feat: remove hooks

* chore: update

* fix: runningPromise

* chore: update debug mode and disable click on transform

* docs: report

* chore: remove the version mark

* feat: report

* fix: lint

* chore: copy ui from @vitest/ui

* chore: export report from ui

* chore: update

* fix: lint

* docs: ui html report

* feat: ensurePackageInstalled

* update

* feat: more info

* chore: improve documentation and tests

* chore: fix UI bundle size

* chore: ui tests

* perf: remove output report using the global variable to judge report mode

* chore: update

* fix: build

* fix: report

* fix: parse

* chore: fix html reporters test

* chore: don't store config in html reporter test

* chore: update ui docs

* feat: log

* chore: fix tests

* test: fix html reporter tests

* docs add vitest fo UI reporter

Co-authored-by: Vladimir <sleuths.slews0s@icloud.com>

authored by

yoho
Vladimir
and committed by
GitHub
(Dec 19, 2022, 1:23 PM +0300) b8f34eb8 881bd7a3

+721 -231
+31 -44
pnpm-lock.yaml
··· 236 236 react-dom: 18.0.0_react@18.0.0 237 237 devDependencies: 238 238 '@testing-library/react': 13.3.0_zpnidt7m3osuk7shl3s4oenomq 239 - '@types/node': 18.11.15 239 + '@types/node': 18.11.16 240 240 '@types/react': 18.0.26 241 241 '@vitejs/plugin-react': 3.0.0 242 242 jsdom: 20.0.3 ··· 704 704 cypress: ^11.2.0 705 705 d3-graph-controller: ^2.4.0 706 706 diff: ^5.1.0 707 + fast-glob: ^3.2.12 707 708 flatted: ^3.2.7 708 709 floating-vue: ^2.0.0-y.0 709 710 picocolors: ^1.0.0 ··· 718 719 vue: ^3.2.45 719 720 vue-router: ^4.1.6 720 721 dependencies: 722 + fast-glob: 3.2.12 723 + flatted: 3.2.7 721 724 sirv: 2.0.2 722 725 devDependencies: 723 726 '@faker-js/faker': 7.6.0 ··· 738 741 cypress: 11.2.0 739 742 d3-graph-controller: 2.4.0 740 743 diff: 5.1.0 741 - flatted: 3.2.7 742 744 floating-vue: 2.0.0-y.0_vue@3.2.45 743 745 picocolors: 1.0.0 744 746 rollup: 2.79.1 ··· 1086 1088 test/reporters: 1087 1089 specifiers: 1088 1090 execa: ^6.1.0 1091 + flatted: ^3.2.7 1089 1092 pkg-reporter: ./reportPkg/ 1090 1093 vitest: workspace:* 1091 1094 vitest-sonar-reporter: 0.3.3 1092 1095 devDependencies: 1093 1096 execa: 6.1.0 1097 + flatted: 3.2.7 1094 1098 pkg-reporter: link:reportPkg 1095 1099 vitest: link:../../packages/vitest 1096 1100 vitest-sonar-reporter: 0.3.3_vitest@packages+vitest ··· 4841 4845 dependencies: 4842 4846 '@types/istanbul-lib-coverage': 2.0.4 4843 4847 '@types/istanbul-reports': 3.0.1 4844 - '@types/node': 18.11.15 4848 + '@types/node': 18.11.16 4845 4849 '@types/yargs': 15.0.14 4846 4850 chalk: 4.1.2 4847 4851 dev: true ··· 4853 4857 '@jest/schemas': 29.0.0 4854 4858 '@types/istanbul-lib-coverage': 2.0.4 4855 4859 '@types/istanbul-reports': 3.0.1 4856 - '@types/node': 18.11.15 4860 + '@types/node': 18.11.16 4857 4861 '@types/yargs': 17.0.12 4858 4862 chalk: 4.1.2 4859 4863 dev: true ··· 5322 5326 dependencies: 5323 5327 '@nodelib/fs.stat': 2.0.5 5324 5328 run-parallel: 1.2.0 5325 - dev: true 5326 5329 5327 5330 /@nodelib/fs.stat/1.1.3: 5328 5331 resolution: {integrity: sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==} ··· 5332 5335 /@nodelib/fs.stat/2.0.5: 5333 5336 resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} 5334 5337 engines: {node: '>= 8'} 5335 - dev: true 5336 5338 5337 5339 /@nodelib/fs.walk/1.2.8: 5338 5340 resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} ··· 5340 5342 dependencies: 5341 5343 '@nodelib/fs.scandir': 2.1.5 5342 5344 fastq: 1.13.0 5343 - dev: true 5344 5345 5345 5346 /@npmcli/fs/1.1.1: 5346 5347 resolution: {integrity: sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==} ··· 5366 5367 engines: {node: '>=14'} 5367 5368 hasBin: true 5368 5369 dependencies: 5369 - '@types/node': 18.11.15 5370 + '@types/node': 18.11.16 5370 5371 playwright-core: 1.28.0 5371 5372 dev: true 5372 5373 ··· 7192 7193 /@types/cheerio/0.22.31: 7193 7194 resolution: {integrity: sha512-Kt7Cdjjdi2XWSfrZ53v4Of0wG3ZcmaegFXjMmz9tfNrZSkzzo36G0AL1YqSdcIA78Etjt6E609pt5h1xnQkPUw==} 7194 7195 dependencies: 7195 - '@types/node': 18.11.15 7196 + '@types/node': 18.11.16 7196 7197 dev: true 7197 7198 7198 7199 /@types/codemirror/5.60.5: ··· 7258 7259 /@types/fs-extra/9.0.13: 7259 7260 resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} 7260 7261 dependencies: 7261 - '@types/node': 18.11.15 7262 + '@types/node': 18.11.16 7262 7263 dev: true 7263 7264 7264 7265 /@types/glob/7.2.0: 7265 7266 resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} 7266 7267 dependencies: 7267 7268 '@types/minimatch': 5.1.1 7268 - '@types/node': 18.11.15 7269 + '@types/node': 18.11.16 7269 7270 dev: true 7270 7271 7271 7272 /@types/glob/8.0.0: 7272 7273 resolution: {integrity: sha512-l6NQsDDyQUVeoTynNpC9uRvCUint/gSUXQA2euwmTuWGvPY5LSDUu6tkCtJB2SvGQlJQzLaKqcGZP4//7EDveA==} 7273 7274 dependencies: 7274 7275 '@types/minimatch': 5.1.1 7275 - '@types/node': 18.11.15 7276 + '@types/node': 18.11.16 7276 7277 dev: true 7277 7278 7278 7279 /@types/graceful-fs/4.1.5: 7279 7280 resolution: {integrity: sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==} 7280 7281 dependencies: 7281 - '@types/node': 18.11.15 7282 + '@types/node': 18.11.16 7282 7283 dev: true 7283 7284 7284 7285 /@types/hast/2.3.4: ··· 7339 7340 /@types/jsdom/20.0.1: 7340 7341 resolution: {integrity: sha512-d0r18sZPmMQr1eG35u12FZfhIXNrnsPU/g5wvRKCUf/tOGilKKwYMYGqh33BNR6ba+2gkHw1EUiHoN3mn7E5IQ==} 7341 7342 dependencies: 7342 - '@types/node': 18.11.15 7343 + '@types/node': 18.11.16 7343 7344 '@types/tough-cookie': 4.0.2 7344 7345 parse5: 7.1.1 7345 7346 dev: true ··· 7383 7384 /@types/node-fetch/2.6.2: 7384 7385 resolution: {integrity: sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==} 7385 7386 dependencies: 7386 - '@types/node': 18.11.15 7387 + '@types/node': 18.11.16 7387 7388 form-data: 3.0.1 7388 7389 dev: true 7389 7390 ··· 7399 7400 resolution: {integrity: sha512-0KXV57tENYmmJMl+FekeW9V3O/rlcqGQQJ/hNh9r8pKIj304pskWuEd8fCyNT86g/TpO0gcOTiLzsHLEURFMIQ==} 7400 7401 dev: true 7401 7402 7402 - /@types/node/18.11.15: 7403 - resolution: {integrity: sha512-VkhBbVo2+2oozlkdHXLrb3zjsRkpdnaU2bXmX8Wgle3PUi569eLRaHGlgETQHR7lLL1w7GiG3h9SnePhxNDecw==} 7403 + /@types/node/18.11.16: 7404 + resolution: {integrity: sha512-6T7P5bDkRhqRxrQtwj7vru+bWTpelgtcETAZEUSdq0YISKz8WKdoBukQLYQQ6DFHvU9JRsbFq0JH5C51X2ZdnA==} 7404 7405 dev: true 7405 7406 7406 7407 /@types/node/18.7.13: ··· 7433 7434 /@types/prompts/2.4.2: 7434 7435 resolution: {integrity: sha512-TwNx7qsjvRIUv/BCx583tqF5IINEVjCNqg9ofKHRlSoUHE62WBHrem4B1HGXcIrG511v29d1kJ9a/t2Esz7MIg==} 7435 7436 dependencies: 7436 - '@types/node': 18.11.15 7437 + '@types/node': 18.11.16 7437 7438 kleur: 3.0.3 7438 7439 dev: true 7439 7440 ··· 7506 7507 /@types/resolve/1.17.1: 7507 7508 resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==} 7508 7509 dependencies: 7509 - '@types/node': 18.11.15 7510 + '@types/node': 18.11.16 7510 7511 dev: true 7511 7512 7512 7513 /@types/resolve/1.20.2: ··· 7523 7524 /@types/set-cookie-parser/2.4.2: 7524 7525 resolution: {integrity: sha512-fBZgytwhYAUkj/jC/FAV4RQ5EerRup1YQsXQCh8rZfiHkc4UahC192oH0smGwsXol3cL3A5oETuAHeQHmhXM4w==} 7525 7526 dependencies: 7526 - '@types/node': 18.11.15 7527 + '@types/node': 18.11.16 7527 7528 dev: true 7528 7529 7529 7530 /@types/sinonjs__fake-timers/8.1.1: ··· 7610 7611 /@types/webpack-sources/3.2.0: 7611 7612 resolution: {integrity: sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==} 7612 7613 dependencies: 7613 - '@types/node': 18.11.15 7614 + '@types/node': 18.11.16 7614 7615 '@types/source-list-map': 0.1.2 7615 7616 source-map: 0.7.4 7616 7617 dev: true ··· 7618 7619 /@types/webpack/4.41.32: 7619 7620 resolution: {integrity: sha512-cb+0ioil/7oz5//7tZUSwbrSAN/NWHrQylz5cW8G0dWTcF/g+/dSdMlKVZspBYuMAN1+WnwHrkxiRrLcwd0Heg==} 7620 7621 dependencies: 7621 - '@types/node': 18.11.15 7622 + '@types/node': 18.11.16 7622 7623 '@types/tapable': 1.0.8 7623 7624 '@types/uglify-js': 3.17.0 7624 7625 '@types/webpack-sources': 3.2.0 ··· 7629 7630 /@types/ws/8.5.3: 7630 7631 resolution: {integrity: sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==} 7631 7632 dependencies: 7632 - '@types/node': 18.11.15 7633 + '@types/node': 18.11.16 7633 7634 dev: true 7634 7635 7635 7636 /@types/yargs-parser/21.0.0: ··· 7652 7653 resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==} 7653 7654 requiresBuild: true 7654 7655 dependencies: 7655 - '@types/node': 18.11.15 7656 + '@types/node': 18.11.16 7656 7657 dev: true 7657 7658 optional: true 7658 7659 ··· 9758 9759 engines: {node: '>=8'} 9759 9760 dependencies: 9760 9761 fill-range: 7.0.1 9761 - dev: true 9762 9762 9763 9763 /broadcast-channel/3.7.0: 9764 9764 resolution: {integrity: sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==} ··· 12636 12636 glob-parent: 5.1.2 12637 12637 merge2: 1.4.1 12638 12638 micromatch: 4.0.5 12639 - dev: true 12640 12639 12641 12640 /fast-json-parse/1.0.3: 12642 12641 resolution: {integrity: sha512-FRWsaZRWEJ1ESVNbDWmsAlqDk96gPQezzLghafp5J4GUKjbCz3OkAHuZs5TuPEtkbVQERysLp9xv6c24fBm8Aw==} ··· 12699 12698 resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} 12700 12699 dependencies: 12701 12700 reusify: 1.0.4 12702 - dev: true 12703 12701 12704 12702 /fb-watchman/2.0.1: 12705 12703 resolution: {integrity: sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==} ··· 12780 12778 engines: {node: '>=8'} 12781 12779 dependencies: 12782 12780 to-regex-range: 5.0.1 12783 - dev: true 12784 12781 12785 12782 /finalhandler/1.2.0: 12786 12783 resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==} ··· 13252 13249 engines: {node: '>= 6'} 13253 13250 dependencies: 13254 13251 is-glob: 4.0.3 13255 - dev: true 13256 13252 13257 13253 /glob-parent/6.0.2: 13258 13254 resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} ··· 14192 14188 /is-extglob/2.1.1: 14193 14189 resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} 14194 14190 engines: {node: '>=0.10.0'} 14195 - dev: true 14196 14191 14197 14192 /is-finite/1.1.0: 14198 14193 resolution: {integrity: sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==} ··· 14225 14220 engines: {node: '>=0.10.0'} 14226 14221 dependencies: 14227 14222 is-extglob: 2.1.1 14228 - dev: true 14229 14223 14230 14224 /is-hexadecimal/1.0.4: 14231 14225 resolution: {integrity: sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==} ··· 14278 14272 /is-number/7.0.0: 14279 14273 resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 14280 14274 engines: {node: '>=0.12.0'} 14281 - dev: true 14282 14275 14283 14276 /is-obj/1.0.1: 14284 14277 resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} ··· 14584 14577 dependencies: 14585 14578 '@jest/types': 26.6.2 14586 14579 '@types/graceful-fs': 4.1.5 14587 - '@types/node': 18.11.15 14580 + '@types/node': 18.11.16 14588 14581 anymatch: 3.1.2 14589 14582 fb-watchman: 2.0.1 14590 14583 graceful-fs: 4.2.10 ··· 14652 14645 resolution: {integrity: sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==} 14653 14646 engines: {node: '>= 10.14.2'} 14654 14647 dependencies: 14655 - '@types/node': 18.11.15 14648 + '@types/node': 18.11.16 14656 14649 graceful-fs: 4.2.10 14657 14650 dev: true 14658 14651 ··· 14661 14654 engines: {node: '>= 10.14.2'} 14662 14655 dependencies: 14663 14656 '@jest/types': 26.6.2 14664 - '@types/node': 18.11.15 14657 + '@types/node': 18.11.16 14665 14658 chalk: 4.1.2 14666 14659 graceful-fs: 4.2.10 14667 14660 is-ci: 2.0.0 ··· 14673 14666 engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 14674 14667 dependencies: 14675 14668 '@jest/types': 29.0.1 14676 - '@types/node': 18.11.15 14669 + '@types/node': 18.11.16 14677 14670 chalk: 4.1.2 14678 14671 ci-info: 3.7.0 14679 14672 graceful-fs: 4.2.10 ··· 14684 14677 resolution: {integrity: sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==} 14685 14678 engines: {node: '>= 10.13.0'} 14686 14679 dependencies: 14687 - '@types/node': 18.11.15 14680 + '@types/node': 18.11.16 14688 14681 merge-stream: 2.0.0 14689 14682 supports-color: 7.2.0 14690 14683 dev: true ··· 14693 14686 resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} 14694 14687 engines: {node: '>= 10.13.0'} 14695 14688 dependencies: 14696 - '@types/node': 18.11.15 14689 + '@types/node': 18.11.16 14697 14690 merge-stream: 2.0.0 14698 14691 supports-color: 8.1.1 14699 14692 dev: true ··· 15536 15529 /merge2/1.4.1: 15537 15530 resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} 15538 15531 engines: {node: '>= 8'} 15539 - dev: true 15540 15532 15541 15533 /methods/1.1.2: 15542 15534 resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} ··· 15583 15575 dependencies: 15584 15576 braces: 3.0.2 15585 15577 picomatch: 2.3.1 15586 - dev: true 15587 15578 15588 15579 /microseconds/0.2.0: 15589 15580 resolution: {integrity: sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==} ··· 17364 17355 17365 17356 /queue-microtask/1.2.3: 17366 17357 resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} 17367 - dev: true 17368 17358 17369 17359 /quick-format-unescaped/4.0.4: 17370 17360 resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} ··· 18099 18089 /reusify/1.0.4: 18100 18090 resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} 18101 18091 engines: {iojs: '>=1.0.0', node: '>=0.10.0'} 18102 - dev: true 18103 18092 18104 18093 /rfdc/1.3.0: 18105 18094 resolution: {integrity: sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==} ··· 18245 18234 resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} 18246 18235 dependencies: 18247 18236 queue-microtask: 1.2.3 18248 - dev: true 18249 18237 18250 18238 /run-queue/1.0.3: 18251 18239 resolution: {integrity: sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==} ··· 19593 19581 engines: {node: '>=8.0'} 19594 19582 dependencies: 19595 19583 is-number: 7.0.0 19596 - dev: true 19597 19584 19598 19585 /to-regex/3.0.2: 19599 19586 resolution: {integrity: sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==}
+1
docs/config/index.md
··· 341 341 - `'dot'` - show each task as a single dot 342 342 - `'junit'` - JUnit XML reporter (you can configure `testsuites` tag name with `VITEST_JUNIT_SUITE_NAME` environmental variable) 343 343 - `'json'` - give a simple JSON summary 344 + - `'html'` - outputs HTML report based on [`@vitest/ui`](/guide/ui) 344 345 - path of a custom reporter (e.g. `'./path/to/reporter.ts'`, `'@scope/reporter'`) 345 346 346 347 ### outputTruncateLength
+26
docs/guide/ui.md
··· 21 21 22 22 <img alt="Vitest UI" img-light src="https://user-images.githubusercontent.com/11247099/171992267-5cae2fa0-b927-400a-8eb1-da776974cb61.png"> 23 23 <img alt="Vitest UI" img-dark src="https://user-images.githubusercontent.com/11247099/171992272-7c6057e2-80c3-4b17-a7b6-0ac28e5a5e0b.png"> 24 + 25 + Since Vitest 0.26.0, UI can also be used as a reporter. Use `'html'` reporter in your Vitest configuration to generate HTML output and preview results of your tests: 26 + 27 + ```ts 28 + // vitest.config.ts 29 + 30 + export default { 31 + test: { 32 + reporters: ['html'] 33 + } 34 + } 35 + ``` 36 + 37 + ::: warning 38 + If you still want to see how your tests are running in real time in the terminal, don't forget to add `default` reporter to `reporters` option: `['default', 'html']`. 39 + ::: 40 + 41 + ::: tip 42 + To preview your HTML report, you can use [vite preview](https://vitejs.dev/guide/cli.html#vite-preview) command: 43 + 44 + ```sh 45 + npx vite preview --base __vitest__ --outDir ./html 46 + ``` 47 + 48 + You can configure output with [`outputFile`](/config/#outputfile) config option. You need to specify `.html` path there. For example, `./html/index.html` is the default value. 49 + :::
+2 -1
packages/ui/index.html
··· 9 9 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 10 10 <link href="https://fonts.googleapis.com/css2?family=Readex+Pro:wght@300;400&display=swap" rel="stylesheet"> 11 11 <script> 12 - (function() { 12 + (function () { 13 13 const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches 14 14 const setting = localStorage.getItem('vueuse-color-scheme') || 'auto' 15 15 if (setting === 'dark' || (prefersDark && setting !== 'light')) 16 16 document.documentElement.classList.toggle('dark', true) 17 17 })() 18 18 </script> 19 + <!-- !LOAD_METADATA! --> 19 20 </head> 20 21 <body> 21 22 <div id="app"></div>
+6 -1
packages/ui/package.json
··· 15 15 "types": "./dist/index.d.ts", 16 16 "import": "./dist/index.js" 17 17 }, 18 + "./reporter": { 19 + "types": "./dist/reporter.d.ts", 20 + "import": "./dist/reporter.js" 21 + }, 18 22 "./*": "./*" 19 23 }, 20 24 "main": "./dist/index.js", ··· 35 39 "prepublishOnly": "pnpm build" 36 40 }, 37 41 "dependencies": { 42 + "fast-glob": "^3.2.12", 43 + "flatted": "^3.2.7", 38 44 "sirv": "^2.0.2" 39 45 }, 40 46 "devDependencies": { ··· 56 62 "cypress": "^11.2.0", 57 63 "d3-graph-controller": "^2.4.0", 58 64 "diff": "^5.1.0", 59 - "flatted": "^3.2.7", 60 65 "floating-vue": "^2.0.0-y.0", 61 66 "picocolors": "^1.0.0", 62 67 "rollup": "^2.79.1",
+9 -8
packages/ui/rollup.config.js
··· 7 7 import alias from '@rollup/plugin-alias' 8 8 import pkg from './package.json' 9 9 10 - const entry = [ 11 - './node/index.ts', 12 - ] 13 - 14 10 const external = [ 15 11 ...builtinModules, 16 12 ...Object.keys(pkg.dependencies), 17 13 ...Object.keys(pkg.peerDependencies || {}), 18 14 'worker_threads', 15 + 'vitest/node', 16 + 'vitest', 19 17 ] 20 18 21 19 export default () => [ 20 + 'index', 21 + 'reporter', 22 + ].map(entry => [ 22 23 { 23 - input: entry, 24 + input: `./node/${entry}.ts`, 24 25 output: { 25 26 dir: 'dist', 26 27 format: 'esm', ··· 44 45 onwarn, 45 46 }, 46 47 { 47 - input: entry, 48 + input: `./node/${entry}.ts`, 48 49 output: { 49 - file: 'dist/index.d.ts', 50 + file: `dist/${entry}.d.ts`, 50 51 format: 'esm', 51 52 }, 52 53 external, ··· 54 55 dts(), 55 56 ], 56 57 }, 57 - ] 58 + ]).flat() 58 59 59 60 function onwarn(message) { 60 61 if (['EMPTY_BUNDLE', 'CIRCULAR_DEPENDENCY'].includes(message.code))
+15
packages/ui/vite.config.ts
··· 8 8 import Pages from 'vite-plugin-pages' 9 9 import { presetAttributify, presetIcons, presetUno } from 'unocss' 10 10 11 + // for debug: 12 + // open a static file serve to share the report json 13 + // and ui using the link to load the report json data 14 + const debugLink = 'http://127.0.0.1:4173' 15 + 11 16 export const config: UserConfig = { 12 17 root: __dirname, 13 18 base: '/__vitest__/', ··· 17 22 '~/': `${resolve(__dirname, 'client')}/`, 18 23 '@vitest/ws-client': `${resolve(__dirname, '../ws-client/src/index.ts')}`, 19 24 }, 25 + }, 26 + define: { 27 + __REPORT__: false, 20 28 }, 21 29 plugins: [ 22 30 Vue(), ··· 53 61 '@vueuse/core', 54 62 ], 55 63 }), 64 + { 65 + name: 'debug-html-report', 66 + apply: 'serve', 67 + transformIndexHtml(html) { 68 + return html.replace('<!-- !LOAD_METADATA! -->', `<script>window.METADATA_PATH="${debugLink}/html.meta.json"</script>`) 69 + }, 70 + }, 56 71 ], 57 72 build: { 58 73 outDir: './dist/client',
-29
packages/vitest/LICENSE.md
··· 2037 2037 2038 2038 --------------------------------------- 2039 2039 2040 - ## sourcemap-codec 2041 - License: MIT 2042 - By: Rich Harris 2043 - Repository: https://github.com/Rich-Harris/sourcemap-codec 2044 - 2045 - > The MIT License 2046 - > 2047 - > Copyright (c) 2015 Rich Harris 2048 - > 2049 - > Permission is hereby granted, free of charge, to any person obtaining a copy 2050 - > of this software and associated documentation files (the "Software"), to deal 2051 - > in the Software without restriction, including without limitation the rights 2052 - > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 2053 - > copies of the Software, and to permit persons to whom the Software is 2054 - > furnished to do so, subject to the following conditions: 2055 - > 2056 - > The above copyright notice and this permission notice shall be included in 2057 - > all copies or substantial portions of the Software. 2058 - > 2059 - > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 2060 - > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 2061 - > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 2062 - > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 2063 - > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 2064 - > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 2065 - > THE SOFTWARE. 2066 - 2067 - --------------------------------------- 2068 - 2069 2040 ## string-width 2070 2041 License: MIT 2071 2042 By: Sindre Sorhus
+1
test/reporters/package.json
··· 6 6 }, 7 7 "devDependencies": { 8 8 "execa": "^6.1.0", 9 + "flatted": "^3.2.7", 9 10 "pkg-reporter": "./reportPkg/", 10 11 "vitest": "workspace:*", 11 12 "vitest-sonar-reporter": "0.3.3"
+6
packages/ui/client/shim.d.ts
··· 7 7 const component: DefineComponent<{}, {}, any> 8 8 export default component 9 9 } 10 + 11 + const __REPORT__: boolean 12 + 13 + declare interface Window { 14 + METADATA_PATH?: string 15 + }
+77
packages/ui/node/reporter.ts
··· 1 + import { existsSync, promises as fs } from 'node:fs' 2 + import { fileURLToPath } from 'node:url' 3 + import { basename, dirname, relative, resolve } from 'pathe' 4 + import c from 'picocolors' 5 + import fg from 'fast-glob' 6 + import { stringify } from 'flatted' 7 + // eslint-disable-next-line no-restricted-imports 8 + import type { File, ModuleGraphData, Reporter, ResolvedConfig, Vitest } from 'vitest' 9 + import { getModuleGraph } from '../../vitest/src/utils/graph' 10 + import { getOutputFile } from '../../vitest/src/utils/config-helpers' 11 + 12 + interface HTMLReportData { 13 + paths: string[] 14 + files: File[] 15 + config: ResolvedConfig 16 + moduleGraph: Record<string, ModuleGraphData> 17 + } 18 + 19 + const distDir = resolve(fileURLToPath(import.meta.url), '../../dist') 20 + 21 + export default class HTMLReporter implements Reporter { 22 + start = 0 23 + ctx!: Vitest 24 + reportUIPath!: string 25 + 26 + async onInit(ctx: Vitest) { 27 + this.ctx = ctx 28 + this.start = Date.now() 29 + } 30 + 31 + async onFinished() { 32 + const result: HTMLReportData = { 33 + paths: await this.ctx.state.getPaths(), 34 + files: this.ctx.state.getFiles(), 35 + config: this.ctx.config, 36 + moduleGraph: {}, 37 + } 38 + await Promise.all( 39 + result.files.map(async (file) => { 40 + result.moduleGraph[file.filepath] = await getModuleGraph(this.ctx, file.filepath) 41 + }), 42 + ) 43 + await this.writeReport(stringify(result)) 44 + } 45 + 46 + async writeReport(report: string) { 47 + const htmlFile = getOutputFile(this.ctx.config, 'html') || 'html/index.html' 48 + const htmlFileName = basename(htmlFile) 49 + const htmlDir = resolve(this.ctx.config.root, dirname(htmlFile)) 50 + 51 + const metaFile = resolve(htmlDir, 'html.meta.json') 52 + 53 + if (!existsSync(htmlDir)) 54 + await fs.mkdir(resolve(htmlDir, 'assets'), { recursive: true }) 55 + 56 + await fs.writeFile(metaFile, report, 'utf-8') 57 + const ui = resolve(distDir, 'client') 58 + // copy ui 59 + const files = fg.sync('**/*', { cwd: ui }) 60 + await Promise.all(files.map(async (f) => { 61 + if (f === 'index.html') { 62 + const html = await fs.readFile(resolve(ui, f), 'utf-8') 63 + const filePath = relative(htmlDir, metaFile) 64 + await fs.writeFile( 65 + resolve(htmlDir, htmlFileName), 66 + html.replace('<!-- !LOAD_METADATA! -->', `<script>window.METADATA_PATH="${filePath}"</script>`), 67 + ) 68 + } 69 + else { 70 + await fs.copyFile(resolve(ui, f), resolve(htmlDir, f)) 71 + } 72 + })) 73 + 74 + this.ctx.logger.log(`${c.bold(c.inverse(c.magenta(' HTML ')))} ${c.magenta('Report is generated')}`) 75 + this.ctx.logger.log(`${c.dim(' You can run ')}${c.bold(`npx vite preview --base __vitest__ --outDir ${relative(this.ctx.config.root, htmlDir)}`)}${c.dim(' to see the test results.')}`) 76 + } 77 + }
+1
test/reporters/fixtures/.gitignore
··· 1 + html
+2
test/reporters/fixtures/json-fail.test.ts
··· 3 3 // I am comment1 4 4 // I am comment2 5 5 test('should fail', () => { 6 + // eslint-disable-next-line no-console 7 + console.log('json-fail>should fail') 6 8 expect(2).toEqual(1) 7 9 })
+79
test/reporters/tests/html.test.ts
··· 1 + import fs from 'fs' 2 + import { resolve } from 'pathe' 3 + import { execa } from 'execa' 4 + import { describe, expect, it } from 'vitest' 5 + import { parse } from 'flatted' 6 + 7 + const skip = (process.platform === 'win32' || process.platform === 'darwin') && process.env.CI 8 + 9 + describe.skipIf(skip)('html reporter', async () => { 10 + const vitestRoot = resolve(__dirname, '../../..') 11 + const root = resolve(__dirname, '../fixtures') 12 + 13 + it('resolves to "passing" status for test file "all-passing-or-skipped"', async () => { 14 + const [expected, testFile, basePath] = ['passing', 'all-passing-or-skipped', 'html/all-passing-or-skipped'] 15 + await execa('npx', ['vitest', 'run', testFile, '--reporter=html', `--outputFile=${basePath}/index.html`], { 16 + cwd: root, 17 + env: { 18 + ...process.env, 19 + CI: 'true', 20 + NO_COLOR: 'true', 21 + }, 22 + stdio: 'inherit', 23 + }).catch(e => e) 24 + const metaJson = fs.readFileSync(resolve(root, `${basePath}/html.meta.json`), { encoding: 'utf-8' }) 25 + const indexHtml = fs.readFileSync(resolve(root, `${basePath}/index.html`), { encoding: 'utf-8' }) 26 + const resultJson = parse(metaJson.replace(new RegExp(vitestRoot, 'g'), '<rootDir>')) 27 + resultJson.config = {} // doesn't matter for a test 28 + const file = resultJson.files[0] 29 + file.id = 0 30 + file.collectDuration = 0 31 + file.setupDuration = 0 32 + file.result.duration = 0 33 + file.result.startTime = 0 34 + const task = file.tasks[0] 35 + task.id = 0 36 + task.result.duration = 0 37 + task.result.startTime = 0 38 + expect(task.result.error).not.toBeDefined() 39 + expect(task.result.logs).not.toBeDefined() 40 + expect(resultJson).toMatchSnapshot(`tests are ${expected}`) 41 + expect(indexHtml).toMatch('window.METADATA_PATH="html.meta.json"') 42 + }, 120000) 43 + 44 + it('resolves to "failing" status for test file "json-fail"', async () => { 45 + const [expected, testFile, basePath] = ['failing', 'json-fail', 'html/fail'] 46 + await execa('npx', ['vitest', 'run', testFile, '--reporter=html', `--outputFile=${basePath}/index.html`], { 47 + cwd: root, 48 + env: { 49 + ...process.env, 50 + CI: 'true', 51 + NO_COLOR: 'true', 52 + }, 53 + stdio: 'inherit', 54 + }).catch(e => e) 55 + const metaJson = fs.readFileSync(resolve(root, `${basePath}/html.meta.json`), { encoding: 'utf-8' }) 56 + const indexHtml = fs.readFileSync(resolve(root, `${basePath}/index.html`), { encoding: 'utf-8' }) 57 + const resultJson = parse(metaJson.replace(new RegExp(vitestRoot, 'g'), '<rootDir>')) 58 + resultJson.config = {} // doesn't matter for a test 59 + const file = resultJson.files[0] 60 + file.id = 0 61 + file.collectDuration = 0 62 + file.setupDuration = 0 63 + file.result.duration = 0 64 + file.result.startTime = 0 65 + const task = file.tasks[0] 66 + task.id = 0 67 + task.result.duration = 0 68 + task.result.startTime = 0 69 + expect(task.result.error).toBeDefined() 70 + task.result.error.stack = task.result.error.stack.split('\n')[0] 71 + task.result.error.stackStr = task.result.error.stackStr.split('\n')[0] 72 + expect(task.logs).toBeDefined() 73 + expect(task.logs).toHaveLength(1) 74 + task.logs[0].taskId = 0 75 + task.logs[0].time = 0 76 + expect(resultJson).toMatchSnapshot(`tests are ${expected}`) 77 + expect(indexHtml).toMatch('window.METADATA_PATH="html.meta.json"') 78 + }, 120000) 79 + })
+3 -1
packages/ui/client/components/FileDetails.vue
··· 1 1 <script setup lang="ts"> 2 - import { client, current, currentLogs } from '~/composables/client' 2 + import { client, current, currentLogs, isReport } from '~/composables/client' 3 3 import type { Params } from '~/composables/params' 4 4 import { viewMode } from '~/composables/params' 5 5 import type { ModuleGraph } from '~/composables/module-graph' ··· 52 52 </div> 53 53 <div class="flex text-lg"> 54 54 <IconButton 55 + v-if="!isReport" 55 56 v-tooltip.bottom="'Open in editor'" 56 57 title="Open in editor" 57 58 icon="i-carbon-launch" ··· 76 77 Module Graph 77 78 </button> 78 79 <button 80 + v-if="!isReport" 79 81 tab-button 80 82 :class="{ 'tab-button-active': viewMode === 'editor' }" 81 83 @click="changeViewMode('editor')"
+3 -2
packages/ui/client/components/Navigation.vue
··· 4 4 import { client, findById } from '../composables/client' 5 5 import type { Task } from '#types' 6 6 import { isDark, toggleDark } from '~/composables' 7 - import { files, runAll } from '~/composables/client' 7 + import { files, isReport, runAll } from '~/composables/client' 8 8 import { activeFileId } from '~/composables/params' 9 9 10 10 const failedSnapshot = computed(() => files.value && hasFailedSnapshot(files.value)) ··· 34 34 @click="showDashboard(true)" 35 35 /> 36 36 <IconButton 37 - v-if="failedSnapshot" 37 + v-if="(failedSnapshot && !isReport)" 38 38 v-tooltip.bottom="'Update all failed snapshot(s)'" 39 39 icon="i-carbon-result-old" 40 40 @click="updateSnapshot()" 41 41 /> 42 42 <IconButton 43 + v-if="!isReport" 43 44 v-tooltip.bottom="filteredTests ? (filteredTests.length === 0 ? 'No test to run (clear filter)' : 'Rerun filtered') : 'Rerun all'" 44 45 :disabled="filteredTests?.length === 0" 45 46 icon="i-carbon-play"
+3 -2
packages/ui/client/components/Suites.vue
··· 1 1 <script setup lang="ts"> 2 2 import { hasFailedSnapshot } from '@vitest/ws-client' 3 - import { client, current, runCurrent } from '~/composables/client' 3 + import { client, current, isReport, runCurrent } from '~/composables/client' 4 4 5 5 const name = computed(() => current.value?.name.split(/\//g).pop()) 6 6 ··· 16 16 <span font-bold text-sm flex-auto ws-nowrap overflow-hidden truncate>{{ name }}</span> 17 17 <div class="flex text-lg"> 18 18 <IconButton 19 - v-if="failedSnapshot" 19 + v-if="(failedSnapshot && !isReport)" 20 20 v-tooltip.bottom="`Update failed snapshot(s) of ${current.name}`" 21 21 icon="i-carbon-result-old" 22 22 @click="updateSnapshot()" 23 23 /> 24 24 <IconButton 25 + v-if="!isReport" 25 26 v-tooltip.bottom="'Rerun file'" 26 27 icon="i-carbon-play" 27 28 @click="runCurrent()"
-97
packages/ui/client/composables/client.ts
··· 1 - import { createClient, getTasks } from '@vitest/ws-client' 2 - import type { WebSocketStatus } from '@vueuse/core' 3 - import type { Ref } from 'vue' 4 - import { reactive } from 'vue' 5 - import type { RunState } from '../../types' 6 - import { activeFileId } from './params' 7 - import type { File, ResolvedConfig } from '#types' 8 - 9 - export const PORT = import.meta.hot ? '51204' : location.port 10 - export const HOST = [location.hostname, PORT].filter(Boolean).join(':') 11 - export const ENTRY_URL = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${HOST}/__vitest_api__` 12 - 13 - export const testRunState: Ref<RunState> = ref('idle') 14 - 15 - export const client = createClient(ENTRY_URL, { 16 - reactive: reactive as any, 17 - handlers: { 18 - onTaskUpdate() { 19 - testRunState.value = 'running' 20 - }, 21 - onFinished() { 22 - testRunState.value = 'idle' 23 - }, 24 - }, 25 - }) 26 - 27 - export const config = shallowRef<ResolvedConfig>({} as any) 28 - export const status = ref<WebSocketStatus>('CONNECTING') 29 - export const files = computed(() => client.state.getFiles()) 30 - export const current = computed(() => files.value.find(file => file.id === activeFileId.value)) 31 - export const currentLogs = computed(() => getTasks(current.value).map(i => i?.logs || []).flat() || []) 32 - 33 - export const findById = (id: string) => { 34 - return files.value.find(file => file.id === id) 35 - } 36 - 37 - export const isConnected = computed(() => status.value === 'OPEN') 38 - export const isConnecting = computed(() => status.value === 'CONNECTING') 39 - export const isDisconnected = computed(() => status.value === 'CLOSED') 40 - 41 - export function runAll(files = client.state.getFiles()) { 42 - return runFiles(files) 43 - } 44 - 45 - export function runFiles(files: File[]) { 46 - files.forEach((f) => { 47 - delete f.result 48 - getTasks(f).forEach(i => delete i.result) 49 - }) 50 - return client.rpc.rerun(files.map(i => i.filepath)) 51 - } 52 - 53 - export function runCurrent() { 54 - if (current.value) 55 - return runFiles([current.value]) 56 - } 57 - 58 - watch( 59 - () => client.ws, 60 - (ws) => { 61 - status.value = 'CONNECTING' 62 - 63 - ws.addEventListener('open', () => { 64 - status.value = 'OPEN' 65 - client.state.filesMap.clear() 66 - client.rpc.getFiles().then(files => client.state.collectFiles(files)) 67 - client.rpc.getConfig().then(_config => config.value = _config) 68 - }) 69 - 70 - ws.addEventListener('close', () => { 71 - setTimeout(() => { 72 - if (status.value === 'CONNECTING') 73 - status.value = 'CLOSED' 74 - }, 1000) 75 - }) 76 - }, 77 - { immediate: true }, 78 - ) 79 - 80 - // display the first file on init 81 - // if (!activeFileId.value) { 82 - // const stop = watch( 83 - // () => client.state.getFiles(), 84 - // (files) => { 85 - // if (activeFileId.value) { 86 - // stop() 87 - // return 88 - // } 89 - // 90 - // if (files.length && files[0].id) { 91 - // activeFileId.value = files[0].id 92 - // stop() 93 - // } 94 - // }, 95 - // { immediate: true }, 96 - // ) 97 - // }
+2 -34
packages/vitest/src/api/setup.ts
··· 4 4 import { parse, stringify } from 'flatted' 5 5 import type { WebSocket } from 'ws' 6 6 import { WebSocketServer } from 'ws' 7 - import type { ModuleNode } from 'vite' 8 7 import { API_PATH } from '../constants' 9 8 import type { Vitest } from '../node' 10 9 import type { File, ModuleGraphData, Reporter, TaskResultPack, UserConsoleLog } from '../types' 10 + import { getModuleGraph } from '../utils' 11 11 import type { TransformResultWithSource, WebSocketEvents, WebSocketHandlers } from './types' 12 12 13 13 export function setup(ctx: Vitest) { ··· 75 75 } 76 76 }, 77 77 async getModuleGraph(id: string): Promise<ModuleGraphData> { 78 - const graph: Record<string, string[]> = {} 79 - const externalized = new Set<string>() 80 - const inlined = new Set<string>() 81 - 82 - function clearId(id?: string | null) { 83 - return id?.replace(/\?v=\w+$/, '') || '' 84 - } 85 - async function get(mod?: ModuleNode, seen = new Map<ModuleNode, string>()) { 86 - if (!mod || !mod.id) 87 - return 88 - if (seen.has(mod)) 89 - return seen.get(mod) 90 - let id = clearId(mod.id) 91 - seen.set(mod, id) 92 - const rewrote = await ctx.vitenode.shouldExternalize(id) 93 - if (rewrote) { 94 - id = rewrote 95 - externalized.add(id) 96 - seen.set(mod, id) 97 - } 98 - else { 99 - inlined.add(id) 100 - } 101 - const mods = Array.from(mod.importedModules).filter(i => i.id && !i.id.includes('/vitest/dist/')) 102 - graph[id] = (await Promise.all(mods.map(m => get(m, seen)))).filter(Boolean) as string[] 103 - return id 104 - } 105 - await get(ctx.server.moduleGraph.getModuleById(id)) 106 - return { 107 - graph, 108 - externalized: Array.from(externalized), 109 - inlined: Array.from(inlined), 110 - } 78 + return getModuleGraph(ctx, id) 111 79 }, 112 80 updateSnapshot(file?: File) { 113 81 if (!file)
+4 -4
packages/vitest/src/node/core.ts
··· 339 339 if (hasFailed(files)) 340 340 process.exitCode = 1 341 341 342 - if (!this.config.browser) 343 - await this.report('onFinished', files, this.state.getUnhandledErrors()) 344 342 this.cache.results.updateResults(files) 345 343 await this.cache.results.writeToCache() 346 344 })() 347 - .finally(() => { 348 - this.runningPromise = undefined 345 + .finally(async () => { 349 346 this.state.finishCollectingPaths() 347 + if (!this.config.browser) 348 + await this.report('onFinished', this.state.getFiles(), this.state.getUnhandledErrors()) 349 + this.runningPromise = undefined 350 350 }) 351 351 352 352 return await this.runningPromise
+1 -1
packages/vitest/src/runtime/error.ts
··· 23 23 if (!val || typeof val === 'string') 24 24 return val 25 25 if (typeof val === 'function') 26 - return `Function<${val.name}>` 26 + return `Function<${val.name || 'anonymous'}>` 27 27 if (typeof val === 'symbol') 28 28 return val.toString() 29 29 if (typeof val !== 'object')
+1 -1
packages/vitest/src/types/config.ts
··· 158 158 * Custom reporter for output. Can contain one or more built-in report names, reporter instances, 159 159 * and/or paths to custom reporters 160 160 */ 161 - reporters?: Arrayable<BuiltinReporters | Reporter | Omit<string, BuiltinReporters>> 161 + reporters?: Arrayable<BuiltinReporters | 'html' | Reporter | Omit<string, BuiltinReporters>> 162 162 163 163 /** 164 164 * diff output length
+2 -3
packages/vitest/src/types/tasks.ts
··· 1 1 import type { ChainableFunction } from '../runtime/chain' 2 - import type { BenchFactory } from './benchmark' 3 - import type { Awaitable, ErrorWithDiff } from './general' 4 - import type { Benchmark, BenchmarkAPI, BenchmarkResult, UserConsoleLog } from '.' 2 + import type { BenchFactory, Benchmark, BenchmarkAPI, BenchmarkResult } from './benchmark' 3 + import type { Awaitable, ErrorWithDiff, UserConsoleLog } from './general' 5 4 6 5 export type RunMode = 'run' | 'skip' | 'only' | 'todo' 7 6 export type TaskState = RunMode | 'pass' | 'fail'
+1 -1
packages/vitest/src/utils/config-helpers.ts
··· 4 4 outputFile?: string | Partial<Record<string, string>> 5 5 } 6 6 7 - export const getOutputFile = (config: PotentialConfig | undefined, reporter: BuiltinReporters | BenchmarkBuiltinReporters) => { 7 + export const getOutputFile = (config: PotentialConfig | undefined, reporter: BuiltinReporters | BenchmarkBuiltinReporters | 'html') => { 8 8 if (!config?.outputFile) 9 9 return 10 10
+38
packages/vitest/src/utils/graph.ts
··· 1 + import type { ModuleNode } from 'vite' 2 + import type { ModuleGraphData, Vitest } from '../types' 3 + 4 + export async function getModuleGraph(ctx: Vitest, id: string): Promise<ModuleGraphData> { 5 + const graph: Record<string, string[]> = {} 6 + const externalized = new Set<string>() 7 + const inlined = new Set<string>() 8 + 9 + function clearId(id?: string | null) { 10 + return id?.replace(/\?v=\w+$/, '') || '' 11 + } 12 + async function get(mod?: ModuleNode, seen = new Map<ModuleNode, string>()) { 13 + if (!mod || !mod.id) 14 + return 15 + if (seen.has(mod)) 16 + return seen.get(mod) 17 + let id = clearId(mod.id) 18 + seen.set(mod, id) 19 + const rewrote = await ctx.vitenode.shouldExternalize(id) 20 + if (rewrote) { 21 + id = rewrote 22 + externalized.add(id) 23 + seen.set(mod, id) 24 + } 25 + else { 26 + inlined.add(id) 27 + } 28 + const mods = Array.from(mod.importedModules).filter(i => i.id && !i.id.includes('/vitest/dist/')) 29 + graph[id] = (await Promise.all(mods.map(m => get(m, seen)))).filter(Boolean) as string[] 30 + return id 31 + } 32 + await get(ctx.server.moduleGraph.getModuleById(id)) 33 + return { 34 + graph, 35 + externalized: Array.from(externalized), 36 + inlined: Array.from(inlined), 37 + } 38 + }
+1
packages/vitest/src/utils/index.ts
··· 10 10 import { getNames } from './tasks' 11 11 import { isBrowser, isNode } from './env' 12 12 13 + export * from './graph' 13 14 export * from './tasks' 14 15 export * from './base' 15 16 export * from './global'
+202
test/reporters/tests/__snapshots__/html.test.ts.snap
··· 1 + // Vitest Snapshot v1 2 + 3 + exports[`html reporter > resolves to "failing" status for test file "json-fail" > tests are failing 1`] = ` 4 + { 5 + "config": {}, 6 + "files": [ 7 + { 8 + "collectDuration": 0, 9 + "filepath": "<rootDir>/test/reporters/fixtures/json-fail.test.ts", 10 + "id": 0, 11 + "mode": "run", 12 + "name": "json-fail.test.ts", 13 + "result": { 14 + "duration": 0, 15 + "hooks": { 16 + "afterAll": "pass", 17 + "beforeAll": "pass", 18 + }, 19 + "startTime": 0, 20 + "state": "fail", 21 + }, 22 + "setupDuration": 0, 23 + "tasks": [ 24 + { 25 + "file": [Circular], 26 + "id": 0, 27 + "logs": [ 28 + { 29 + "content": "json-fail>should fail 30 + ", 31 + "size": 1, 32 + "taskId": 0, 33 + "time": 0, 34 + "type": "stdout", 35 + }, 36 + ], 37 + "mode": "run", 38 + "name": "should fail", 39 + "result": { 40 + "duration": 0, 41 + "error": { 42 + "actual": "2", 43 + "constructor": "Function<AssertionError>", 44 + "expected": "1", 45 + "message": "expected 2 to deeply equal 1", 46 + "name": "AssertionError", 47 + "nameStr": "AssertionError", 48 + "operator": "strictEqual", 49 + "showDiff": true, 50 + "stack": "AssertionError: expected 2 to deeply equal 1", 51 + "stackStr": "AssertionError: expected 2 to deeply equal 1", 52 + "toJSON": "Function<anonymous>", 53 + "toString": "Function<toString>", 54 + }, 55 + "hooks": { 56 + "afterEach": "pass", 57 + "beforeEach": "pass", 58 + }, 59 + "retryCount": 0, 60 + "startTime": 0, 61 + "state": "fail", 62 + }, 63 + "suite": { 64 + "file": [Circular], 65 + "id": "", 66 + "mode": "run", 67 + "name": "", 68 + "tasks": [ 69 + [Circular], 70 + ], 71 + "type": "suite", 72 + }, 73 + "type": "test", 74 + }, 75 + ], 76 + "type": "suite", 77 + }, 78 + ], 79 + "moduleGraph": { 80 + "<rootDir>/test/reporters/fixtures/json-fail.test.ts": { 81 + "externalized": [], 82 + "graph": { 83 + "<rootDir>/test/reporters/fixtures/json-fail.test.ts": [], 84 + }, 85 + "inlined": [ 86 + "<rootDir>/test/reporters/fixtures/json-fail.test.ts", 87 + ], 88 + }, 89 + }, 90 + "paths": [], 91 + } 92 + `; 93 + 94 + exports[`html reporter > resolves to "passing" status for test file "all-passing-or-skipped" > tests are passing 1`] = ` 95 + { 96 + "config": {}, 97 + "files": [ 98 + { 99 + "collectDuration": 0, 100 + "filepath": "<rootDir>/test/reporters/fixtures/all-passing-or-skipped.test.ts", 101 + "id": 0, 102 + "mode": "run", 103 + "name": "all-passing-or-skipped.test.ts", 104 + "result": { 105 + "duration": 0, 106 + "hooks": { 107 + "afterAll": "pass", 108 + "beforeAll": "pass", 109 + }, 110 + "startTime": 0, 111 + "state": "pass", 112 + }, 113 + "setupDuration": 0, 114 + "tasks": [ 115 + { 116 + "file": [Circular], 117 + "id": 0, 118 + "mode": "run", 119 + "name": "2 + 3 = 5", 120 + "result": { 121 + "duration": 0, 122 + "hooks": { 123 + "afterEach": "pass", 124 + "beforeEach": "pass", 125 + }, 126 + "retryCount": 0, 127 + "startTime": 0, 128 + "state": "pass", 129 + }, 130 + "suite": { 131 + "file": [Circular], 132 + "id": "", 133 + "mode": "run", 134 + "name": "", 135 + "tasks": [ 136 + [Circular], 137 + { 138 + "file": [Circular], 139 + "id": "1111755131_1", 140 + "mode": "skip", 141 + "name": "3 + 3 = 6", 142 + "suite": [Circular], 143 + "type": "test", 144 + }, 145 + ], 146 + "type": "suite", 147 + }, 148 + "type": "test", 149 + }, 150 + { 151 + "file": [Circular], 152 + "id": "1111755131_1", 153 + "mode": "skip", 154 + "name": "3 + 3 = 6", 155 + "suite": { 156 + "file": [Circular], 157 + "id": "", 158 + "mode": "run", 159 + "name": "", 160 + "tasks": [ 161 + { 162 + "file": [Circular], 163 + "id": 0, 164 + "mode": "run", 165 + "name": "2 + 3 = 5", 166 + "result": { 167 + "duration": 0, 168 + "hooks": { 169 + "afterEach": "pass", 170 + "beforeEach": "pass", 171 + }, 172 + "retryCount": 0, 173 + "startTime": 0, 174 + "state": "pass", 175 + }, 176 + "suite": [Circular], 177 + "type": "test", 178 + }, 179 + [Circular], 180 + ], 181 + "type": "suite", 182 + }, 183 + "type": "test", 184 + }, 185 + ], 186 + "type": "suite", 187 + }, 188 + ], 189 + "moduleGraph": { 190 + "<rootDir>/test/reporters/fixtures/all-passing-or-skipped.test.ts": { 191 + "externalized": [], 192 + "graph": { 193 + "<rootDir>/test/reporters/fixtures/all-passing-or-skipped.test.ts": [], 194 + }, 195 + "inlined": [ 196 + "<rootDir>/test/reporters/fixtures/all-passing-or-skipped.test.ts", 197 + ], 198 + }, 199 + }, 200 + "paths": [], 201 + } 202 + `;
+1 -1
test/reporters/tests/__snapshots__/json.test.ts.snap
··· 11 11 "fullName": " should fail", 12 12 "location": { 13 13 "column": 13, 14 - "line": 6, 14 + "line": 8, 15 15 }, 16 16 "status": "failed", 17 17 "title": "should fail",
+3
packages/ui/client/components/views/ViewModuleGraph.vue
··· 2 2 import type { ResizeContext } from 'd3-graph-controller' 3 3 import { GraphController, Markers, PositionInitializers, defineGraphConfig } from 'd3-graph-controller' 4 4 import type { Selection } from 'd3-selection' 5 + import { isReport } from '~/composables/client' 5 6 import type { ModuleGraph, ModuleGraphController, ModuleLink, ModuleNode, ModuleType } from '~/composables/module-graph' 6 7 7 8 const props = defineProps<{ ··· 87 88 } 88 89 89 90 function bindOnClick(selection: Selection<SVGCircleElement, ModuleNode, SVGGElement, undefined>) { 91 + if (isReport) 92 + return 90 93 // Only trigger on left-click and primary touch 91 94 const isValidClick = (event: PointerEvent) => event.button === 0 92 95
+109
packages/ui/client/composables/client/index.ts
··· 1 + import { createClient, getTasks } from '@vitest/ws-client' 2 + import type { WebSocketStatus } from '@vueuse/core' 3 + import type { Ref } from 'vue' 4 + import { reactive } from 'vue' 5 + import type { RunState } from '../../../types' 6 + import { activeFileId } from '../params' 7 + import { createStaticClient } from './static' 8 + import type { File, ResolvedConfig } from '#types' 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 = `${location.protocol === 'https:' ? 'wss:' : 'ws:'}//${HOST}/__vitest_api__` 13 + export const isReport = !!window.METADATA_PATH 14 + export const testRunState: Ref<RunState> = ref('idle') 15 + 16 + export const client = (function createVitestClient() { 17 + if (isReport) { 18 + return createStaticClient() 19 + } 20 + else { 21 + return createClient(ENTRY_URL, { 22 + reactive: reactive as any, 23 + handlers: { 24 + onTaskUpdate() { 25 + testRunState.value = 'running' 26 + }, 27 + onFinished() { 28 + testRunState.value = 'idle' 29 + }, 30 + }, 31 + }) 32 + } 33 + })() 34 + 35 + export const config = shallowRef<ResolvedConfig>({} as any) 36 + export const status = ref<WebSocketStatus>('CONNECTING') 37 + export const files = computed(() => client.state.getFiles()) 38 + export const current = computed(() => files.value.find(file => file.id === activeFileId.value)) 39 + export const currentLogs = computed(() => getTasks(current.value).map(i => i?.logs || []).flat() || []) 40 + 41 + export const findById = (id: string) => { 42 + return files.value.find(file => file.id === id) 43 + } 44 + 45 + export const isConnected = computed(() => status.value === 'OPEN') 46 + export const isConnecting = computed(() => status.value === 'CONNECTING') 47 + export const isDisconnected = computed(() => status.value === 'CLOSED') 48 + 49 + export function runAll(files = client.state.getFiles()) { 50 + return runFiles(files) 51 + } 52 + 53 + export function runFiles(files: File[]) { 54 + files.forEach((f) => { 55 + delete f.result 56 + getTasks(f).forEach(i => delete i.result) 57 + }) 58 + return client.rpc.rerun(files.map(i => i.filepath)) 59 + } 60 + 61 + export function runCurrent() { 62 + if (current.value) 63 + return runFiles([current.value]) 64 + } 65 + 66 + watch( 67 + () => client.ws, 68 + (ws) => { 69 + status.value = 'CONNECTING' 70 + 71 + ws.addEventListener('open', async () => { 72 + status.value = 'OPEN' 73 + client.state.filesMap.clear() 74 + const [files, _config] = await Promise.all([ 75 + client.rpc.getFiles(), 76 + client.rpc.getConfig(), 77 + ]) 78 + client.state.collectFiles(files) 79 + config.value = _config 80 + }) 81 + 82 + ws.addEventListener('close', () => { 83 + setTimeout(() => { 84 + if (status.value === 'CONNECTING') 85 + status.value = 'CLOSED' 86 + }, 1000) 87 + }) 88 + }, 89 + { immediate: true }, 90 + ) 91 + 92 + // display the first file on init 93 + // if (!activeFileId.value) { 94 + // const stop = watch( 95 + // () => client.state.getFiles(), 96 + // (files) => { 97 + // if (activeFileId.value) { 98 + // stop() 99 + // return 100 + // } 101 + // 102 + // if (files.length && files[0].id) { 103 + // activeFileId.value = files[0].id 104 + // stop() 105 + // } 106 + // }, 107 + // { immediate: true }, 108 + // ) 109 + // }
+84
packages/ui/client/composables/client/static.ts
··· 1 + import type { BirpcReturn } from 'birpc' 2 + import type { VitestClient } from '@vitest/ws-client' 3 + import type { WebSocketHandlers } from 'vitest/src/api/types' 4 + import { parse } from 'flatted' 5 + import type { File, ModuleGraphData, ResolvedConfig } from 'vitest/src/types' 6 + import { StateManager } from '../../../../vitest/src/node/state' 7 + 8 + interface HTMLReportMetadata { 9 + paths: string[] 10 + files: File[] 11 + config: ResolvedConfig 12 + moduleGraph: Record<string, ModuleGraphData> 13 + } 14 + 15 + const noop: any = () => {} 16 + const asyncNoop: any = () => Promise.resolve() 17 + 18 + export function createStaticClient(): VitestClient { 19 + const ctx = reactive({ 20 + state: new StateManager(), 21 + waitForConnection, 22 + reconnect, 23 + ws: new EventTarget(), 24 + }) as VitestClient 25 + 26 + ctx.state.filesMap = reactive(ctx.state.filesMap) 27 + ctx.state.idMap = reactive(ctx.state.idMap) 28 + 29 + let metadata!: HTMLReportMetadata 30 + 31 + const rpc = { 32 + getFiles: () => { 33 + return metadata.files 34 + }, 35 + getPaths: async () => { 36 + return metadata.paths 37 + }, 38 + getConfig: () => { 39 + return metadata.config 40 + }, 41 + getModuleGraph: async (id) => { 42 + return metadata.moduleGraph[id] 43 + }, 44 + getTransformResult: async (id) => { 45 + return { 46 + code: id, 47 + source: '', 48 + } 49 + }, 50 + readFile: async (id) => { 51 + return Promise.resolve(id) 52 + }, 53 + onWatcherStart: asyncNoop, 54 + onFinished: asyncNoop, 55 + onCollected: asyncNoop, 56 + onTaskUpdate: noop, 57 + writeFile: asyncNoop, 58 + rerun: asyncNoop, 59 + updateSnapshot: asyncNoop, 60 + } as WebSocketHandlers 61 + 62 + ctx.rpc = rpc as any as BirpcReturn<WebSocketHandlers> 63 + 64 + let openPromise: Promise<void> 65 + 66 + function reconnect() { 67 + registerMetadata() 68 + } 69 + 70 + async function registerMetadata() { 71 + const res = await fetch(window.METADATA_PATH!) 72 + metadata = parse(await res.text()) as HTMLReportMetadata 73 + const event = new Event('open') 74 + ctx.ws.dispatchEvent(event) 75 + } 76 + 77 + registerMetadata() 78 + 79 + function waitForConnection() { 80 + return openPromise 81 + } 82 + 83 + return ctx 84 + }
+7 -1
packages/vitest/src/node/reporters/utils.ts
··· 1 1 import type { ViteNodeRunner } from 'vite-node/client' 2 2 import type { Reporter } from '../../types' 3 + import { ensurePackageInstalled } from '../../utils' 3 4 import { BenchmarkReportsMap, ReportersMap } from './index' 4 5 import type { BenchmarkBuiltinReporters, BuiltinReporters } from './index' 5 6 ··· 21 22 function createReporters(reporterReferences: Array<string | Reporter | BuiltinReporters>, runner: ViteNodeRunner) { 22 23 const promisedReporters = reporterReferences.map(async (referenceOrInstance) => { 23 24 if (typeof referenceOrInstance === 'string') { 24 - if (referenceOrInstance in ReportersMap) { 25 + if (referenceOrInstance === 'html') { 26 + await ensurePackageInstalled('@vitest/ui', runner.root) 27 + const CustomReporter = await loadCustomReporterModule('@vitest/ui/reporter', runner) 28 + return new CustomReporter() 29 + } 30 + else if (referenceOrInstance in ReportersMap) { 25 31 const BuiltinReporter = ReportersMap[referenceOrInstance as BuiltinReporters] 26 32 return new BuiltinReporter() 27 33 }