[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 typechecking functionality (#2107)

Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>

authored by

Vladimir
Anthony Fu
and committed by
GitHub
(Nov 7, 2022, 11:49 PM +0800) 7c7f33d5 941ba372

+2155 -175
+1
docs/.vitepress/components/FeaturesList.vue
··· 23 23 <ListItem><a target="_blank" href="https://github.com/capricorn86/happy-dom" rel="noopener noreferrer">happy-dom</a> or <a target="_blank" href="https://github.com/jsdom/jsdom" rel="noopener noreferrer">jsdom</a> for DOM mocking</ListItem> 24 24 <ListItem>Code coverage via <a target="_blank" href="https://github.com/bcoe/c8" rel="noopener noreferrer">c8</a> or <a target="_blank" href="https://istanbul.js.org/" rel="noopener noreferrer">istanbul</a></ListItem> 25 25 <ListItem>Rust-like <a href="/guide/in-source">in-source testing</a></ListItem> 26 + <ListItem>Type Testing via <a target="_blank" href="https://github.com/mmkal/expect-type" rel="noopener noreferrer">expect-type</a></ListItem> 26 27 </ul> 27 28 </template> 28 29
+4
docs/.vitepress/config.ts
··· 116 116 link: '/guide/features', 117 117 }, 118 118 { 119 + text: 'Testing Types', 120 + link: '/guide/testing-types', 121 + }, 122 + { 119 123 text: 'CLI', 120 124 link: '/guide/cli', 121 125 },
+568 -3
docs/api/index.md
··· 72 72 }) 73 73 ``` 74 74 75 + ::: warning 76 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 77 + ::: 78 + 75 79 ### test.runIf 76 80 77 81 - **Type:** `(condition: any) => Test` ··· 88 92 // this test only runs in development 89 93 }) 90 94 ``` 95 + 96 + ::: warning 97 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 98 + ::: 91 99 92 100 ### test.only 93 101 ··· 152 160 }) 153 161 ``` 154 162 163 + ::: warning 164 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 165 + ::: 166 + 155 167 ### test.todo 156 168 157 169 - **Type:** `(name: string) => void` ··· 179 191 }) 180 192 ``` 181 193 194 + ::: warning 195 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 196 + ::: 197 + 182 198 ### test.each 183 199 - **Type:** `(cases: ReadonlyArray<T>) => void` 184 200 - **Alias:** `it.each` ··· 210 226 // ✓ add(2, 1) -> 3 211 227 ``` 212 228 229 + You can also access object properties with `$` prefix, if you are using objects as arguments: 230 + 231 + ```ts 232 + test.each([ 233 + { a: 1, b: 1, expected: 2 }, 234 + { a: 1, b: 2, expected: 3 }, 235 + { a: 2, b: 1, expected: 3 }, 236 + ])('add($a, $b) -> $expected', ({ a, b, expected }) => { 237 + expect(a + b).toBe(expected) 238 + }) 239 + 240 + // this will return 241 + // ✓ add(1, 1) -> 2 242 + // ✓ add(1, 2) -> 3 243 + // ✓ add(2, 1) -> 3 244 + ``` 245 + 213 246 If you want to have access to `TestContext`, use `describe.each` with a single test. 247 + 248 + ::: warning 249 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 250 + ::: 214 251 215 252 ## bench 216 253 ··· 472 509 describe.todo.concurrent(/* ... */) // or describe.concurrent.todo(/* ... */) 473 510 ``` 474 511 512 + ::: warning 513 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 514 + ::: 515 + 475 516 ### describe.shuffle 476 517 477 518 - **Type:** `(name: string, fn: TestFunction, options?: number | TestOptions) => void` ··· 488 529 ``` 489 530 490 531 `.skip`, `.only`, and `.todo` works with random suites. 532 + 533 + ::: warning 534 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 535 + ::: 491 536 492 537 ### describe.todo 493 538 ··· 526 571 }) 527 572 ``` 528 573 574 + ::: warning 575 + You cannot use this syntax, when using Vitest as [type checker](/guide/testing-types). 576 + ::: 577 + 529 578 ## expect 530 579 531 580 - **Type:** `ExpectStatic & (actual: any) => Assertions` ··· 546 595 Technically this example doesn't use [`test`](#test) function, so in the console you will see Nodejs error instead of Vitest output. To learn more about `test`, please read [next chapter](#test). 547 596 548 597 Also, `expect` can be used statically to access matchers functions, described later, and more. 598 + 599 + ::: warning 600 + `expect` has no effect on testing types, if expression doesn't have a type error. If you want to use Vitest as [type checker](/guide/testing-types), use [`expectTypeOf`](#expecttypeof) or [`assertType`](#asserttype). 601 + ::: 549 602 550 603 ### not 551 604 ··· 1776 1829 If you want to know more, checkout [guide on extending matchers](/guide/extending-matchers). 1777 1830 ::: 1778 1831 1832 + ## expectTypeOf 1833 + 1834 + - **Type:** `<T>(a: unknown) => ExpectTypeOf` 1835 + 1836 + ### not 1837 + 1838 + - **Type:** `ExpectTypeOf` 1839 + 1840 + You can negate all assertions, using `.not` property. 1841 + 1842 + ### toEqualTypeOf 1843 + 1844 + - **Type:** `<T>(expected: T) => void` 1845 + 1846 + This matcher will check, if types are fully equal to each other. This matcher will not fail, if two objects have different values, but the same type, but will fail, if object is missing a property. 1847 + 1848 + ```ts 1849 + import { expectTypeOf } from 'vitest' 1850 + 1851 + expectTypeOf({ a: 1 }).toEqualTypeOf<{ a: number }>() 1852 + expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 1 }) 1853 + expectTypeOf({ a: 1 }).toEqualTypeOf({ a: 2 }) 1854 + expectTypeOf({ a: 1, b: 1 }).not.toEqualTypeOf<{ a: number }>() 1855 + ``` 1856 + 1857 + ### toMatchTypeOf 1858 + 1859 + - **Type:** `<T>(expected: T) => void` 1860 + 1861 + This matcher checks if expect type extends provided type. It is different from `toEqual` and is more similar to expect's `toMatch`. With this matcher you can check, if an object "matches" a type. 1862 + 1863 + ```ts 1864 + import { expectTypeOf } from 'vitest' 1865 + 1866 + expectTypeOf({ a: 1, b: 1 }).toMatchTypeOf({ a: 1 }) 1867 + expectTypeOf<number>().toMatchTypeOf<string | number>() 1868 + expectTypeOf<string | number>().not.toMatchTypeOf<number>() 1869 + ``` 1870 + 1871 + ### extract 1872 + 1873 + - **Type:** `ExpectTypeOf<ExtractedUnion>` 1874 + 1875 + You can use `.extract` to narrow down types for further testing. 1876 + 1877 + ```ts 1878 + import { expectTypeOf } from 'vitest' 1879 + 1880 + type ResponsiveProp<T> = T | T[] | { xs?: T; sm?: T; md?: T } 1881 + const getResponsiveProp = <T>(_props: T): ResponsiveProp<T> => ({}) 1882 + interface CSSProperties { margin?: string; padding?: string } 1883 + 1884 + const cssProperties: CSSProperties = { margin: '1px', padding: '2px' } 1885 + 1886 + expectTypeOf(getResponsiveProp(cssProperties)) 1887 + .extract<{ xs?: any }>() // extracts the last type from a union 1888 + .toEqualTypeOf<{ xs?: CSSProperties; sm?: CSSProperties; md?: CSSProperties }>() 1889 + 1890 + expectTypeOf(getResponsiveProp(cssProperties)) 1891 + .extract<unknown[]>() // extracts an array from a union 1892 + .toEqualTypeOf<CSSProperties[]>() 1893 + ``` 1894 + 1895 + ::: warning 1896 + If no type is found in the union, `.extract` will return `never`. 1897 + ::: 1898 + 1899 + ### exclude 1900 + 1901 + - **Type:** `ExpectTypeOf<NonExcludedUnion>` 1902 + 1903 + You can use `.exclude` to remove types from a union for further testing. 1904 + 1905 + ```ts 1906 + import { expectTypeOf } from 'vitest' 1907 + 1908 + type ResponsiveProp<T> = T | T[] | { xs?: T; sm?: T; md?: T } 1909 + const getResponsiveProp = <T>(_props: T): ResponsiveProp<T> => ({}) 1910 + interface CSSProperties { margin?: string; padding?: string } 1911 + 1912 + const cssProperties: CSSProperties = { margin: '1px', padding: '2px' } 1913 + 1914 + expectTypeOf(getResponsiveProp(cssProperties)) 1915 + .exclude<unknown[]>() 1916 + .exclude<{ xs?: unknown }>() // or just .exclude<unknown[] | { xs?: unknown }>() 1917 + .toEqualTypeOf<CSSProperties>() 1918 + ``` 1919 + 1920 + ::: warning 1921 + If no type is found in the union, `.exclude` will return `never`. 1922 + ::: 1923 + 1924 + ### returns 1925 + 1926 + - **Type:** `ExpectTypeOf<ReturnValue>` 1927 + 1928 + You can use `.returns` to extract return value of a function type. 1929 + 1930 + ```ts 1931 + import { expectTypeOf } from 'vitest' 1932 + 1933 + expectTypeOf(() => {}).returns.toBeVoid() 1934 + expectTypeOf((a: number) => [a, a]).returns.toEqualTypeOf([1, 2]) 1935 + ``` 1936 + 1937 + ::: warning 1938 + If used on a non-function type, it will return `never`, so you won't be able to chain it with other matchers. 1939 + ::: 1940 + 1941 + ### parameters 1942 + 1943 + - **Type:** `ExpectTypeOf<Parameters>` 1944 + 1945 + You can extract function arguments with `.parameters` to perform assertions on its value. Parameters are returned as an array. 1946 + 1947 + ```ts 1948 + import { expectTypeOf } from 'vitest' 1949 + 1950 + type NoParam = () => void 1951 + type HasParam = (s: string) => void 1952 + 1953 + expectTypeOf<NoParam>().parameters.toEqualTypeOf<[]>() 1954 + expectTypeOf<HasParam>().parameters.toEqualTypeOf<[string]>() 1955 + ``` 1956 + 1957 + ::: warning 1958 + If used on a non-function type, it will return `never`, so you won't be able to chain it with other matchers. 1959 + ::: 1960 + 1961 + ::: tip 1962 + You can also use [`.toBeCallableWith`](#tobecallablewith) matcher as a more expressive assertion. 1963 + ::: 1964 + 1965 + ### parameter 1966 + 1967 + - **Type:** `(nth: number) => ExpectTypeOf` 1968 + 1969 + You can extract a certain function argument with `.parameter(number)` call to perform other assertions on it. 1970 + 1971 + ```ts 1972 + import { expectTypeOf } from 'vitest' 1973 + 1974 + const foo = (a: number, b: string) => [a, b] 1975 + 1976 + expectTypeOf(foo).parameter(0).toBeNumber() 1977 + expectTypeOf(foo).parameter(1).toBeString() 1978 + ``` 1979 + 1980 + ::: warning 1981 + If used on a non-function type, it will return `never`, so you won't be able to chain it with other matchers. 1982 + ::: 1983 + 1984 + ### constructorParameters 1985 + 1986 + - **Type:** `ExpectTypeOf<ConstructorParameters>` 1987 + 1988 + You can extract constructor parameters as an array of values and perform assertions on them with this method. 1989 + 1990 + ```ts 1991 + import { expectTypeOf } from 'vitest' 1992 + 1993 + expectTypeOf(Date).constructorParameters.toEqualTypeOf<[] | [string | number | Date]>() 1994 + ``` 1995 + 1996 + ::: warning 1997 + If used on a non-function type, it will return `never`, so you won't be able to chain it with other matchers. 1998 + ::: 1999 + 2000 + ::: tip 2001 + You can also use [`.toBeConstructibleWith`](#tobeconstructiblewith) matcher as a more expressive assertion. 2002 + ::: 2003 + 2004 + ### instance 2005 + 2006 + - **Type:** `ExpectTypeOf<ConstructableInstance>` 2007 + 2008 + This property gives access to matchers that can be performed on an instance of the provided class. 2009 + 2010 + ```ts 2011 + import { expectTypeOf } from 'vitest' 2012 + 2013 + expectTypeOf(Date).instance.toHaveProperty('toISOString') 2014 + ``` 2015 + 2016 + ::: warning 2017 + If used on a non-function type, it will return `never`, so you won't be able to chain it with other matchers. 2018 + ::: 2019 + 2020 + ### items 2021 + 2022 + - **Type:** `ExpectTypeOf<T>` 2023 + 2024 + You can get array item type with `.items` to perform further assertions. 2025 + 2026 + ```ts 2027 + import { expectTypeOf } from 'vitest' 2028 + 2029 + expectTypeOf([1, 2, 3]).items.toEqualTypeOf<number>() 2030 + expectTypeOf([1, 2, 3]).items.not.toEqualTypeOf<string>() 2031 + ``` 2032 + 2033 + ### resolves 2034 + 2035 + - **Type:** `ExpectTypeOf<ResolvedPromise>` 2036 + 2037 + This matcher extracts resolved value of a `Promise`, so you can perform other assertions on it. 2038 + 2039 + ```ts 2040 + import { expectTypeOf } from 'vitest' 2041 + 2042 + const asyncFunc = async () => 123 2043 + 2044 + expectTypeOf(asyncFunc).returns.resolves.toBeNumber() 2045 + expectTypeOf(Promise.resolve('string')).resolves.toBeString() 2046 + ``` 2047 + 2048 + ::: warning 2049 + If used on a non-promise type, it will return `never`, so you won't be able to chain it with other matchers. 2050 + ::: 2051 + 2052 + ### guards 2053 + 2054 + - **Type:** `ExpectTypeOf<Guard>` 2055 + 2056 + This matcher extracts guard value (e.g., `v is number`), so you can perform assertions on it. 2057 + 2058 + ```ts 2059 + import { expectTypeOf } from 'vitest' 2060 + 2061 + const isString = (v: any): v is string => typeof v === 'string' 2062 + expectTypeOf(isString).guards.toBeString() 2063 + ``` 2064 + 2065 + ::: warning 2066 + Returns `never`, if the value is not a guard function, so you won't be able to chain it with other matchers. 2067 + ::: 2068 + 2069 + ### asserts 2070 + 2071 + - **Type:** `ExpectTypeOf<Assert>` 2072 + 2073 + This matcher extracts assert value (e.g., `assert v is number`), so you can perform assertions on it. 2074 + 2075 + ```ts 2076 + import { expectTypeOf } from 'vitest' 2077 + 2078 + const assertNumber = (v: any): asserts v is number => { 2079 + if (typeof v !== 'number') 2080 + throw new TypeError('Nope !') 2081 + } 2082 + 2083 + expectTypeOf(assertNumber).asserts.toBeNumber() 2084 + ``` 2085 + 2086 + ::: warning 2087 + Returns `never`, if the value is not an assert function, so you won't be able to chain it with other matchers. 2088 + ::: 2089 + 2090 + ### toBeAny 2091 + 2092 + - **Type:** `() => void` 2093 + 2094 + With this matcher you can check, if provided type is `any` type. If the type is too specific, the test will fail. 2095 + 2096 + ```ts 2097 + import { expectTypeOf } from 'vitest' 2098 + 2099 + expectTypeOf<any>().toBeAny() 2100 + expectTypeOf({} as any).toBeAny() 2101 + expectTypeOf('string').not.toBeAny() 2102 + ``` 2103 + 2104 + ### toBeUnknown 2105 + 2106 + - **Type:** `() => void` 2107 + 2108 + This matcher checks, if provided type is `unknown` type. 2109 + 2110 + ```ts 2111 + import { expectTypeOf } from 'vitest' 2112 + 2113 + expectTypeOf().toBeUnknown() 2114 + expectTypeOf({} as unknown).toBeUnknown() 2115 + expectTypeOf('string').not.toBeUnknown() 2116 + ``` 2117 + 2118 + ### toBeNever 2119 + 2120 + - **Type:** `() => void` 2121 + 2122 + This matcher checks, if provided type is a `never` type. 2123 + 2124 + ```ts 2125 + import { expectTypeOf } from 'vitest' 2126 + 2127 + expectTypeOf<never>().toBeNever() 2128 + expectTypeOf((): never => {}).returns.toBeNever() 2129 + ``` 2130 + 2131 + ### toBeFunction 2132 + 2133 + - **Type:** `() => void` 2134 + 2135 + This matcher checks, if provided type is a `functon`. 2136 + 2137 + ```ts 2138 + import { expectTypeOf } from 'vitest' 2139 + 2140 + expectTypeOf(42).not.toBeFunction() 2141 + expectTypeOf((): never => {}).toBeFunction() 2142 + ``` 2143 + 2144 + ### toBeObject 2145 + 2146 + - **Type:** `() => void` 2147 + 2148 + This matcher checks, if provided type is an `object`. 2149 + 2150 + ```ts 2151 + import { expectTypeOf } from 'vitest' 2152 + 2153 + expectTypeOf(42).not.toBeObject() 2154 + expectTypeOf({}).toBeObject() 2155 + ``` 2156 + 2157 + ### toBeArray 2158 + 2159 + - **Type:** `() => void` 2160 + 2161 + This matcher checks, if provided type is `Array<T>`. 2162 + 2163 + ```ts 2164 + import { expectTypeOf } from 'vitest' 2165 + 2166 + expectTypeOf(42).not.toBeArray() 2167 + expectTypeOf([]).toBeArray() 2168 + expectTypeOf([1, 2]).toBeArray() 2169 + expectTypeOf([{}, 42]).toBeArray() 2170 + ``` 2171 + 2172 + ### toBeString 2173 + 2174 + - **Type:** `() => void` 2175 + 2176 + This matcher checks, if provided type is a `string`. 2177 + 2178 + ```ts 2179 + import { expectTypeOf } from 'vitest' 2180 + 2181 + expectTypeOf(42).not.toBeArray() 2182 + expectTypeOf([]).toBeArray() 2183 + expectTypeOf([1, 2]).toBeArray() 2184 + expectTypeOf<number[]>().toBeArray() 2185 + ``` 2186 + 2187 + ### toBeBoolean 2188 + 2189 + - **Type:** `() => void` 2190 + 2191 + This matcher checks, if provided type is `boolean`. 2192 + 2193 + ```ts 2194 + import { expectTypeOf } from 'vitest' 2195 + 2196 + expectTypeOf(42).not.toBeBoolean() 2197 + expectTypeOf(true).toBeBoolean() 2198 + expectTypeOf<boolean>().toBeBoolean() 2199 + ``` 2200 + 2201 + ### toBeVoid 2202 + 2203 + - **Type:** `() => void` 2204 + 2205 + This matcher checks, if provided type is `void`. 2206 + 2207 + ```ts 2208 + import { expectTypeOf } from 'vitest' 2209 + 2210 + expectTypeOf(() => {}).returns.toBeVoid() 2211 + expectTypeOf<void>().toBeVoid() 2212 + ``` 2213 + 2214 + ### toBeSymbol 2215 + 2216 + - **Type:** `() => void` 2217 + 2218 + This matcher checks, if provided type is a `symbol`. 2219 + 2220 + ```ts 2221 + import { expectTypeOf } from 'vitest' 2222 + 2223 + expectTypeOf(Symbol(1)).toBeSymbol() 2224 + expectTypeOf<symbol>().toBeSymbol() 2225 + ``` 2226 + 2227 + ### toBeNull 2228 + 2229 + - **Type:** `() => void` 2230 + 2231 + This matcher checks, if provided type is `null`. 2232 + 2233 + ```ts 2234 + import { expectTypeOf } from 'vitest' 2235 + 2236 + expectTypeOf(null).toBeNull() 2237 + expectTypeOf<null>().toBeNull() 2238 + expectTypeOf(undefined).not.toBeNull() 2239 + ``` 2240 + 2241 + ### toBeUndefined 2242 + 2243 + - **Type:** `() => void` 2244 + 2245 + This matcher checks, if provided type is `undefined`. 2246 + 2247 + ```ts 2248 + import { expectTypeOf } from 'vitest' 2249 + 2250 + expectTypeOf(undefined).toBeUndefined() 2251 + expectTypeOf<undefined>().toBeUndefined() 2252 + expectTypeOf(null).not.toBeUndefined() 2253 + ``` 2254 + 2255 + ### toBeNullable 2256 + 2257 + - **Type:** `() => void` 2258 + 2259 + This matcher checks, if you can use `null` or `undefined` with provided type. 2260 + 2261 + ```ts 2262 + import { expectTypeOf } from 'vitest' 2263 + 2264 + expectTypeOf<1 | undefined>().toBeNullable() 2265 + expectTypeOf<1 | null>().toBeNullable() 2266 + expectTypeOf<1 | undefined | null>().toBeNullable() 2267 + ``` 2268 + 2269 + ### toBeCallableWith 2270 + 2271 + - **Type:** `() => void` 2272 + 2273 + This matcher ensures you can call provided function with a set of parameters. 2274 + 2275 + ```ts 2276 + import { expectTypeOf } from 'vitest' 2277 + 2278 + type NoParam = () => void 2279 + type HasParam = (s: string) => void 2280 + 2281 + expectTypeOf<NoParam>().toBeCallableWith() 2282 + expectTypeOf<HasParam>().toBeCallableWith('some string') 2283 + ``` 2284 + 2285 + ::: warning 2286 + If used on a non-function type, it will return `never`, so you won't be able to chain it with other matchers. 2287 + ::: 2288 + 2289 + ### toBeConstructibleWith 2290 + 2291 + - **Type:** `() => void` 2292 + 2293 + This matcher ensures you can create a new instance with a set of constructor parameters. 2294 + 2295 + ```ts 2296 + import { expectTypeOf } from 'vitest' 2297 + 2298 + expectTypeOf(Date).toBeConstructibleWith(new Date()) 2299 + expectTypeOf(Date).toBeConstructibleWith('01-01-2000') 2300 + ``` 2301 + 2302 + ::: warning 2303 + If used on a non-function type, it will return `never`, so you won't be able to chain it with other matchers. 2304 + ::: 2305 + 2306 + ### toHaveProperty 2307 + 2308 + - **Type:** `<K extends keyof T>(property: K) => ExpectTypeOf<T[K>` 2309 + 2310 + This matcher checks if a property exists on provided object. If it exists, it also returns the same set of matchers for the type of this property, so you can chain assertions one after another. 2311 + 2312 + ```ts 2313 + import { expectTypeOf } from 'vitest' 2314 + 2315 + const obj = { a: 1, b: '' } 2316 + 2317 + expectTypeOf(obj).toHaveProperty('a') 2318 + expectTypeOf(obj).not.toHaveProperty('c') 2319 + 2320 + expectTypeOf(obj).toHaveProperty('a').toBeNumber() 2321 + expectTypeOf(obj).toHaveProperty('b').toBeString() 2322 + expectTypeOf(obj).toHaveProperty('a').not.toBeString() 2323 + ``` 2324 + 2325 + ## assertType 2326 + 2327 + - **Type:** `<T>(value: T): void` 2328 + 2329 + You can use this function as an alternative for `expectTypeOf` to easily assert that argument type is equal to provided generic. 2330 + 2331 + ```ts 2332 + import { assertType } from 'vitest' 2333 + 2334 + function concat(a: string, b: string): string 2335 + function concat(a: number, b: number): number 2336 + function concat(a: string | number, b: string | number): string | number 2337 + 2338 + assertType<string>(concat('a', 'b')) 2339 + assertType<number>(concat(1, 2)) 2340 + // @ts-expect-error wrong types 2341 + assertType(concat('a', 2)) 2342 + ``` 2343 + 1779 2344 ## Setup and Teardown 1780 2345 1781 - These functions allow you to hook into the life cycle of tests to avoid repeating setup and teardown code. They apply to the current context: the file if they are used at the top-level or the current suite if they are inside a `describe` block. 2346 + These functions allow you to hook into the life cycle of tests to avoid repeating setup and teardown code. They apply to the current context: the file if they are used at the top-level or the current suite if they are inside a `describe` block. These hooks are not called, when you are running Vitest as a type checker. 1782 2347 1783 2348 ### beforeEach 1784 2349 ··· 1970 2535 1971 2536 - **Type**: `(path: string, factory?: () => unknown) => void` 1972 2537 1973 - Makes all `imports` to passed module to be mocked. Inside a path you _can_ use configured Vite aliases. 2538 + Makes all `imports` to passed module to be mocked. Inside a path you _can_ use configured Vite aliases. The call to `vi.mock` is hoisted, so it doesn't matter where you call it. It will always be executed before all imports. 1974 2539 1975 - - If `factory` is defined, will return its result. Factory function can be asynchronous. You may call [`vi.importActual`](#vi-importactual) inside to get the original module. The call to `vi.mock` is hoisted to the top of the file, so you don't have access to variables declared in the global file scope! 2540 + - If `factory` is defined, will return its result. Factory function can be asynchronous. You may call [`vi.importActual`](#vi-importactual) inside to get the original module. Since the call to `vi.mock` is hoisted, you don't have access to variables declared in the global file scope! 1976 2541 - If mocking a module with a default export, you'll need to provide a `default` key within the returned factory function object. This is an ES modules specific caveat, therefore `jest` documentation may differ as `jest` uses commonJS modules. *Example:* 1977 2542 1978 2543 ```ts
+55
docs/config/index.md
··· 849 849 - **Default**: `Date.now()` 850 850 851 851 Sets the randomization seed, if tests are running in random order. 852 + 853 + ### typecheck 854 + 855 + Options for configuring [typechecking](/guide/testing-types) test environment. 856 + 857 + #### typecheck.checker 858 + 859 + - **Type**: `'tsc' | 'vue-tsc' | string` 860 + - **Default**: `tsc` 861 + 862 + What tools to use for type checking. Vitest will spawn a process with certain parameters for easier parsing, depending on the type. Checker should implement the same output format as `tsc`. 863 + 864 + You need to have a package installed to use typecheker: 865 + 866 + - `tsc` requires `typescript` package 867 + - `vue-tsc` requires `vue-tsc` package 868 + 869 + You can also pass down a path to custom binary or command name that produces the same output as `tsc --noEmit --pretty false`. 870 + 871 + #### typecheck.include 872 + 873 + - **Type**: `string[]` 874 + - **Default**: `['**/*.{test,spec}-d.{ts,js}']` 875 + 876 + Glob pattern for files that should be treated as test files 877 + 878 + #### typecheck.exclude 879 + 880 + - **Type**: `string[]` 881 + - **Default**: `['**/node_modules/**', '**/dist/**', '**/cypress/**', '**/.{idea,git,cache,output,temp}/**']` 882 + 883 + Glob pattern for files that should not be treated as test files 884 + 885 + #### typecheck.allowJs 886 + 887 + - **Type**: `boolean` 888 + - **Default**: `false` 889 + 890 + Check JS files that have `@ts-check` comment. If you have it enabled in tsconfig, this will not overwrite it. 891 + 892 + #### typecheck.ignoreSourceErrors 893 + 894 + - **Type**: `boolean` 895 + - **Default**: `false` 896 + 897 + Do not fail, if Vitest found errors outside the test files. This will not show you non-test errors at all. 898 + 899 + By default, if Vitest finds source error, it will fail test suite. 900 + 901 + #### typecheck.tsconfig 902 + 903 + - **Type**: `string` 904 + - **Default**: _tries to find closest tsconfig.json_ 905 + 906 + Path to custom tsconfig, relative to the project root.
+17
docs/guide/features.md
··· 200 200 }) 201 201 }) 202 202 ``` 203 + 204 + ## Type Testing <sup><code>experimental</code></sup> 205 + 206 + Since Vitest 0.25.0 you can [write tests](/guide/testing-types) to catch type regressions. Vitest comes with [`expect-type`](https://github.com/mmkal/expect-type) package to provide you with a similar and easy to understand API. 207 + 208 + ```ts 209 + import { assertType, expectTypeOf } from 'vitest' 210 + import { mount } from './mount.js' 211 + 212 + test('my types work properly', () => { 213 + expectTypeOf(mount).toBeFunction() 214 + expectTypeOf(mount).parameter(0).toMatchTypeOf<{ name: string }>() 215 + 216 + // @ts-expect-error name is a string 217 + assertType(mount({ name: 42 })) 218 + }) 219 + ```
+92
docs/guide/testing-types.md
··· 1 + --- 2 + title: Testing Types | Guide 3 + --- 4 + 5 + # Testing Types 6 + 7 + Vitest allows you to write tests for your types, using `expectTypeOf` or `assertType` syntaxes. By default all tests inside `*.test-d.ts` files are considered type tests, but you can change it with [`typecheck.include`](/config/#typecheck-include) config option. 8 + 9 + Under the hood Vitest calls `tsc` or `vue-tsc`, depending on your config, and parses results. Vitest will also print out type errors in your source code, if it finds any. You can disable it with [`typecheck.ignoreSourceErrors`](/config/#typecheck-ignoresourceerrors) config option. 10 + 11 + Keep in mind that Vitest doesn't run or compile these files, they are only statically analyzed by the compiler, and because of that you cannot use any dynamic statements. Meaning, you cannot use dynamic test names, and `test.each`, `test.runIf`, `test.skipIf`, `test.each`, `test.concurrent` APIs. But you can use other APIs, like `test`, `describe`, `.only`, `.skip` and `.todo`. 12 + 13 + Using CLI flags, like `--allowOnly` and `-t` are also supported for type checking. 14 + 15 + ```ts 16 + import { assertType, expectTypeOf } from 'vitest' 17 + import { mount } from './mount.js' 18 + 19 + test('my types work properly', () => { 20 + expectTypeOf(mount).toBeFunction() 21 + expectTypeOf(mount).parameter(0).toMatchTypeOf<{ name: string }>() 22 + 23 + // @ts-expect-error name is a string 24 + assertType(mount({ name: 42 })) 25 + }) 26 + ``` 27 + 28 + Any type error triggered inside a test file will be treated as a test error, so you can use any type trick you want to test types of your project. 29 + 30 + You can see a list of possible matchers in [API section](/api/#expecttypeof). 31 + 32 + ## Reading Errors 33 + 34 + If you are using `expectTypeOf` API, you might notice hard to read errors or unexpected: 35 + 36 + ```ts 37 + expectTypeOf(1).toEqualTypeOf<string>() 38 + // ^^^^^^^^^^^^^^^^^^^^^^ 39 + // index-c3943160.d.ts(90, 20): Arguments for the rest parameter 'MISMATCH' were not provided. 40 + ``` 41 + 42 + This is due to how [`expect-type`](https://github.com/mmkal/expect-type) handles type errors. 43 + 44 + Unfortunately, TypeScript doesn't provide type metadata without patching, so we cannot provide useful error messages at this point, but there are <a href="https://github.com/microsoft/TypeScript/pull/40468" tatger="_blank">works in TypeScript project</a> to fix this. If you want better messages, please, ask TypeScript team to have a look at mentioned PR. 45 + 46 + If you find it hard working with `expectTypeOf` API and figuring out errors, you can always use more simple `assertType` API: 47 + 48 + ```ts 49 + const answer = 42 50 + 51 + assertType<number>(answer) 52 + // @ts-expect-error answer is not a string 53 + assertType<string>(answer) 54 + ``` 55 + 56 + ::: tip 57 + When using `@ts-expect-error` syntax, you might want to make sure that you didn't make a typo. You can do that by including your type files in [`test.include`](/config/#include) config option, so Vitest will also actually *run* these tests and fail with `ReferenceError`. 58 + 59 + This will pass, because it expects an error, but the word “answer” has a typo, so it's a false positive error: 60 + 61 + ```ts 62 + // @ts-expect-error answer is not a string 63 + assertType<string>(answr) // 64 + ``` 65 + ::: 66 + 67 + ## Run typechecking 68 + 69 + Add this command to your `scripts` section in `package.json`: 70 + 71 + ```json 72 + { 73 + "scripts": { 74 + "typecheck": "vitest typecheck" 75 + } 76 + } 77 + ``` 78 + 79 + Now you can run typecheck: 80 + 81 + ```sh 82 + # npm 83 + npm run typecheck 84 + 85 + # yarn 86 + yarn typecheck 87 + 88 + # pnpm 89 + pnpm run typecheck 90 + ``` 91 + 92 + Vitest uses `tsc --noEmit` or `vue-tsc --noEmit`, depending on your configuration, so you can remove these scripts from your pipeline.
+4 -4
packages/ui/client/composables/summary.ts
··· 1 1 import { hasFailedSnapshot } from '@vitest/ws-client' 2 - import type { Benchmark, Task, Test } from 'vitest/src' 2 + import type { Benchmark, Task, Test, TypeCheck } from 'vitest/src' 3 3 import { files, testRunState } from '~/composables/client' 4 4 5 5 type Nullable<T> = T | null | undefined ··· 52 52 return array 53 53 return [array] 54 54 } 55 - function isAtomTest(s: Task): s is Test | Benchmark { 56 - return (s.type === 'test' || s.type === 'benchmark') 55 + function isAtomTest(s: Task): s is Test | Benchmark | TypeCheck { 56 + return (s.type === 'test' || s.type === 'benchmark' || s.type === 'typecheck') 57 57 } 58 - function getTests(suite: Arrayable<Task>): (Test | Benchmark)[] { 58 + function getTests(suite: Arrayable<Task>): (Test | Benchmark | TypeCheck)[] { 59 59 return toArray(suite).flatMap(s => isAtomTest(s) ? [s] : s.tasks.flatMap(c => isAtomTest(c) ? [c] : getTests(c))) 60 60 }
+2
packages/vite-node/package.json
··· 78 78 "debug": "^4.3.4", 79 79 "mlly": "^0.5.16", 80 80 "pathe": "^0.2.0", 81 + "source-map": "^0.6.1", 81 82 "source-map-support": "^0.5.21", 82 83 "vite": "^3.0.0" 83 84 }, 84 85 "devDependencies": { 85 86 "@types/debug": "^4.1.7", 87 + "@types/source-map": "^0.5.7", 86 88 "@types/source-map-support": "^0.5.6", 87 89 "cac": "^6.7.14", 88 90 "picocolors": "^1.0.0",
+238 -30
packages/vitest/LICENSE.md
··· 197 197 198 198 --------------------------------------- 199 199 200 - ## acorn 201 - License: MIT 202 - By: Marijn Haverbeke, Ingvar Stepanyan, Adrian Heine 203 - Repository: https://github.com/acornjs/acorn.git 204 - 205 - > MIT License 206 - > 207 - > Copyright (C) 2012-2022 by various contributors (see AUTHORS) 208 - > 209 - > Permission is hereby granted, free of charge, to any person obtaining a copy 210 - > of this software and associated documentation files (the "Software"), to deal 211 - > in the Software without restriction, including without limitation the rights 212 - > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 213 - > copies of the Software, and to permit persons to whom the Software is 214 - > furnished to do so, subject to the following conditions: 215 - > 216 - > The above copyright notice and this permission notice shall be included in 217 - > all copies or substantial portions of the Software. 218 - > 219 - > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 220 - > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 221 - > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 222 - > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 223 - > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 224 - > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 225 - > THE SOFTWARE. 226 - 227 - --------------------------------------- 228 - 229 200 ## ansi-escapes 230 201 License: MIT 231 202 By: Sindre Sorhus ··· 603 574 604 575 --------------------------------------- 605 576 577 + ## expect-type 578 + License: Apache-2.0 579 + Repository: https://github.com/mmkal/expect-type.git 580 + 581 + > Apache License 582 + > Version 2.0, January 2004 583 + > http://www.apache.org/licenses/ 584 + > 585 + > TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 586 + > 587 + > 1. Definitions. 588 + > 589 + > "License" shall mean the terms and conditions for use, reproduction, 590 + > and distribution as defined by Sections 1 through 9 of this document. 591 + > 592 + > "Licensor" shall mean the copyright owner or entity authorized by 593 + > the copyright owner that is granting the License. 594 + > 595 + > "Legal Entity" shall mean the union of the acting entity and all 596 + > other entities that control, are controlled by, or are under common 597 + > control with that entity. For the purposes of this definition, 598 + > "control" means (i) the power, direct or indirect, to cause the 599 + > direction or management of such entity, whether by contract or 600 + > otherwise, or (ii) ownership of fifty percent (50%) or more of the 601 + > outstanding shares, or (iii) beneficial ownership of such entity. 602 + > 603 + > "You" (or "Your") shall mean an individual or Legal Entity 604 + > exercising permissions granted by this License. 605 + > 606 + > "Source" form shall mean the preferred form for making modifications, 607 + > including but not limited to software source code, documentation 608 + > source, and configuration files. 609 + > 610 + > "Object" form shall mean any form resulting from mechanical 611 + > transformation or translation of a Source form, including but 612 + > not limited to compiled object code, generated documentation, 613 + > and conversions to other media types. 614 + > 615 + > "Work" shall mean the work of authorship, whether in Source or 616 + > Object form, made available under the License, as indicated by a 617 + > copyright notice that is included in or attached to the work 618 + > (an example is provided in the Appendix below). 619 + > 620 + > "Derivative Works" shall mean any work, whether in Source or Object 621 + > form, that is based on (or derived from) the Work and for which the 622 + > editorial revisions, annotations, elaborations, or other modifications 623 + > represent, as a whole, an original work of authorship. For the purposes 624 + > of this License, Derivative Works shall not include works that remain 625 + > separable from, or merely link (or bind by name) to the interfaces of, 626 + > the Work and Derivative Works thereof. 627 + > 628 + > "Contribution" shall mean any work of authorship, including 629 + > the original version of the Work and any modifications or additions 630 + > to that Work or Derivative Works thereof, that is intentionally 631 + > submitted to Licensor for inclusion in the Work by the copyright owner 632 + > or by an individual or Legal Entity authorized to submit on behalf of 633 + > the copyright owner. For the purposes of this definition, "submitted" 634 + > means any form of electronic, verbal, or written communication sent 635 + > to the Licensor or its representatives, including but not limited to 636 + > communication on electronic mailing lists, source code control systems, 637 + > and issue tracking systems that are managed by, or on behalf of, the 638 + > Licensor for the purpose of discussing and improving the Work, but 639 + > excluding communication that is conspicuously marked or otherwise 640 + > designated in writing by the copyright owner as "Not a Contribution." 641 + > 642 + > "Contributor" shall mean Licensor and any individual or Legal Entity 643 + > on behalf of whom a Contribution has been received by Licensor and 644 + > subsequently incorporated within the Work. 645 + > 646 + > 2. Grant of Copyright License. Subject to the terms and conditions of 647 + > this License, each Contributor hereby grants to You a perpetual, 648 + > worldwide, non-exclusive, no-charge, royalty-free, irrevocable 649 + > copyright license to reproduce, prepare Derivative Works of, 650 + > publicly display, publicly perform, sublicense, and distribute the 651 + > Work and such Derivative Works in Source or Object form. 652 + > 653 + > 3. Grant of Patent License. Subject to the terms and conditions of 654 + > this License, each Contributor hereby grants to You a perpetual, 655 + > worldwide, non-exclusive, no-charge, royalty-free, irrevocable 656 + > (except as stated in this section) patent license to make, have made, 657 + > use, offer to sell, sell, import, and otherwise transfer the Work, 658 + > where such license applies only to those patent claims licensable 659 + > by such Contributor that are necessarily infringed by their 660 + > Contribution(s) alone or by combination of their Contribution(s) 661 + > with the Work to which such Contribution(s) was submitted. If You 662 + > institute patent litigation against any entity (including a 663 + > cross-claim or counterclaim in a lawsuit) alleging that the Work 664 + > or a Contribution incorporated within the Work constitutes direct 665 + > or contributory patent infringement, then any patent licenses 666 + > granted to You under this License for that Work shall terminate 667 + > as of the date such litigation is filed. 668 + > 669 + > 4. Redistribution. You may reproduce and distribute copies of the 670 + > Work or Derivative Works thereof in any medium, with or without 671 + > modifications, and in Source or Object form, provided that You 672 + > meet the following conditions: 673 + > 674 + > (a) You must give any other recipients of the Work or 675 + > Derivative Works a copy of this License; and 676 + > 677 + > (b) You must cause any modified files to carry prominent notices 678 + > stating that You changed the files; and 679 + > 680 + > (c) You must retain, in the Source form of any Derivative Works 681 + > that You distribute, all copyright, patent, trademark, and 682 + > attribution notices from the Source form of the Work, 683 + > excluding those notices that do not pertain to any part of 684 + > the Derivative Works; and 685 + > 686 + > (d) If the Work includes a "NOTICE" text file as part of its 687 + > distribution, then any Derivative Works that You distribute must 688 + > include a readable copy of the attribution notices contained 689 + > within such NOTICE file, excluding those notices that do not 690 + > pertain to any part of the Derivative Works, in at least one 691 + > of the following places: within a NOTICE text file distributed 692 + > as part of the Derivative Works; within the Source form or 693 + > documentation, if provided along with the Derivative Works; or, 694 + > within a display generated by the Derivative Works, if and 695 + > wherever such third-party notices normally appear. The contents 696 + > of the NOTICE file are for informational purposes only and 697 + > do not modify the License. You may add Your own attribution 698 + > notices within Derivative Works that You distribute, alongside 699 + > or as an addendum to the NOTICE text from the Work, provided 700 + > that such additional attribution notices cannot be construed 701 + > as modifying the License. 702 + > 703 + > You may add Your own copyright statement to Your modifications and 704 + > may provide additional or different license terms and conditions 705 + > for use, reproduction, or distribution of Your modifications, or 706 + > for any such Derivative Works as a whole, provided Your use, 707 + > reproduction, and distribution of the Work otherwise complies with 708 + > the conditions stated in this License. 709 + > 710 + > 5. Submission of Contributions. Unless You explicitly state otherwise, 711 + > any Contribution intentionally submitted for inclusion in the Work 712 + > by You to the Licensor shall be under the terms and conditions of 713 + > this License, without any additional terms or conditions. 714 + > Notwithstanding the above, nothing herein shall supersede or modify 715 + > the terms of any separate license agreement you may have executed 716 + > with Licensor regarding such Contributions. 717 + > 718 + > 6. Trademarks. This License does not grant permission to use the trade 719 + > names, trademarks, service marks, or product names of the Licensor, 720 + > except as required for reasonable and customary use in describing the 721 + > origin of the Work and reproducing the content of the NOTICE file. 722 + > 723 + > 7. Disclaimer of Warranty. Unless required by applicable law or 724 + > agreed to in writing, Licensor provides the Work (and each 725 + > Contributor provides its Contributions) on an "AS IS" BASIS, 726 + > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 727 + > implied, including, without limitation, any warranties or conditions 728 + > of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 729 + > PARTICULAR PURPOSE. You are solely responsible for determining the 730 + > appropriateness of using or redistributing the Work and assume any 731 + > risks associated with Your exercise of permissions under this License. 732 + > 733 + > 8. Limitation of Liability. In no event and under no legal theory, 734 + > whether in tort (including negligence), contract, or otherwise, 735 + > unless required by applicable law (such as deliberate and grossly 736 + > negligent acts) or agreed to in writing, shall any Contributor be 737 + > liable to You for damages, including any direct, indirect, special, 738 + > incidental, or consequential damages of any character arising as a 739 + > result of this License or out of the use or inability to use the 740 + > Work (including but not limited to damages for loss of goodwill, 741 + > work stoppage, computer failure or malfunction, or any and all 742 + > other commercial damages or losses), even if such Contributor 743 + > has been advised of the possibility of such damages. 744 + > 745 + > 9. Accepting Warranty or Additional Liability. While redistributing 746 + > the Work or Derivative Works thereof, You may choose to offer, 747 + > and charge a fee for, acceptance of support, warranty, indemnity, 748 + > or other liability obligations and/or rights consistent with this 749 + > License. However, in accepting such obligations, You may act only 750 + > on Your own behalf and on Your sole responsibility, not on behalf 751 + > of any other Contributor, and only if You agree to indemnify, 752 + > defend, and hold each Contributor harmless for any liability 753 + > incurred by, or claims asserted against, such Contributor by reason 754 + > of your accepting any such warranty or additional liability. 755 + > 756 + > END OF TERMS AND CONDITIONS 757 + > 758 + > APPENDIX: How to apply the Apache License to your work. 759 + > 760 + > To apply the Apache License to your work, attach the following 761 + > boilerplate notice, with the fields enclosed by brackets "[]" 762 + > replaced with your own identifying information. (Don't include 763 + > the brackets!) The text should be enclosed in the appropriate 764 + > comment syntax for the file format. We also recommend that a 765 + > file or class name and description of purpose be included on the 766 + > same "printed page" as the copyright notice for easier 767 + > identification within third-party archives. 768 + > 769 + > Copyright [yyyy] [name of copyright owner] 770 + > 771 + > Licensed under the Apache License, Version 2.0 (the "License"); 772 + > you may not use this file except in compliance with the License. 773 + > You may obtain a copy of the License at 774 + > 775 + > http://www.apache.org/licenses/LICENSE-2.0 776 + > 777 + > Unless required by applicable law or agreed to in writing, software 778 + > distributed under the License is distributed on an "AS IS" BASIS, 779 + > WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 780 + > See the License for the specific language governing permissions and 781 + > limitations under the License. 782 + 783 + --------------------------------------- 784 + 606 785 ## fast-glob 607 786 License: MIT 608 787 By: Denis Malinochkin ··· 736 915 > The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 737 916 > 738 917 > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 918 + 919 + --------------------------------------- 920 + 921 + ## get-tsconfig 922 + License: MIT 923 + By: Hiroki Osame 924 + Repository: privatenumber/get-tsconfig 925 + 926 + > MIT License 927 + > 928 + > Copyright (c) Hiroki Osame <hiroki.osame@gmail.com> 929 + > 930 + > Permission is hereby granted, free of charge, to any person obtaining a copy 931 + > of this software and associated documentation files (the "Software"), to deal 932 + > in the Software without restriction, including without limitation the rights 933 + > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 934 + > copies of the Software, and to permit persons to whom the Software is 935 + > furnished to do so, subject to the following conditions: 936 + > 937 + > The above copyright notice and this permission notice shall be included in all 938 + > copies or substantial portions of the Software. 939 + > 940 + > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 941 + > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 942 + > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 943 + > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 944 + > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 945 + > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 946 + > SOFTWARE. 739 947 740 948 --------------------------------------- 741 949 ··· 1286 1494 1287 1495 > MIT License 1288 1496 > 1289 - > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (sindresorhus.com) 1497 + > Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com) 1290 1498 > 1291 1499 > 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: 1292 1500 >
+2
packages/vitest/globals.d.ts
··· 3 3 const test: typeof import('vitest')['test'] 4 4 const describe: typeof import('vitest')['describe'] 5 5 const it: typeof import('vitest')['it'] 6 + const expectTypeOf: typeof import('vitest')['expectTypeOf'] 7 + const assertType: typeof import('vitest')['assertType'] 6 8 const expect: typeof import('vitest')['expect'] 7 9 const assert: typeof import('vitest')['assert'] 8 10 const vitest: typeof import('vitest')['vitest']
+5
packages/vitest/package.json
··· 108 108 "@types/chai": "^4.3.3", 109 109 "@types/chai-subset": "^1.3.3", 110 110 "@types/node": "*", 111 + "acorn": "^8.8.0", 112 + "acorn-walk": "^8.2.0", 111 113 "chai": "^4.3.6", 112 114 "debug": "^4.3.4", 113 115 "local-pkg": "^0.4.2", 116 + "source-map": "^0.6.1", 114 117 "strip-literal": "^0.4.2", 115 118 "tinybench": "^2.3.1", 116 119 "tinypool": "^0.3.0", ··· 135 138 "diff": "^5.1.0", 136 139 "event-target-polyfill": "^0.0.3", 137 140 "execa": "^6.1.0", 141 + "expect-type": "^0.15.0", 138 142 "fast-glob": "^3.2.12", 139 143 "find-up": "^6.3.0", 140 144 "flatted": "^3.2.7", 145 + "get-tsconfig": "^4.2.0", 141 146 "happy-dom": "^7.6.6", 142 147 "jsdom": "^20.0.2", 143 148 "log-update": "^5.0.1",
+3
packages/vitest/src/constants.ts
··· 37 37 'chai', 38 38 'expect', 39 39 'assert', 40 + // typecheck 41 + 'expectTypeOf', 42 + 'assertType', 40 43 // utils 41 44 'vitest', 42 45 'vi',
+5
packages/vitest/src/defaults.ts
··· 87 87 fakeTimers: fakeTimersDefaults, 88 88 maxConcurrency: 5, 89 89 dangerouslyIgnoreUnhandledErrors: false, 90 + typecheck: { 91 + checker: 'tsc' as const, 92 + include: ['**/*.{test,spec}-d.{ts,js}'], 93 + exclude: defaultExclude, 94 + }, 90 95 } 91 96 92 97 export const configDefaults: Required<Pick<UserConfig, keyof typeof config>> = Object.freeze(config)
+1 -1
packages/vitest/src/node/cli-api.ts
··· 48 48 49 49 const ctx = await createVitest(mode, options, viteOverrides) 50 50 51 - if (mode !== 'benchmark' && ctx.config.coverage.enabled) { 51 + if (mode === 'test' && ctx.config.coverage.enabled) { 52 52 const provider = ctx.config.coverage.provider || 'c8' 53 53 if (typeof provider === 'string') { 54 54 const requiredPackages = CoverageProviderMap[provider]
+9
packages/vitest/src/node/cli.ts
··· 67 67 .action(benchmark) 68 68 69 69 cli 70 + .command('typecheck [...filters]') 71 + .action(typecheck) 72 + 73 + cli 70 74 .command('[...filters]') 71 75 .action((filters, options) => start('test', filters, options)) 72 76 ··· 91 95 async function benchmark(cliFilters: string[], options: CliOptions): Promise<void> { 92 96 console.warn(c.yellow('Benchmarking is an experimental feature.\nBreaking changes might not follow semver, please pin Vitest\'s version when using it.')) 93 97 await start('benchmark', cliFilters, options) 98 + } 99 + 100 + async function typecheck(cliFilters: string[] = [], options: CliOptions = {}) { 101 + console.warn(c.yellow('Testing types with tsc and vue-tsc is an experimental feature.\nBreaking changes might not follow semver, please pin Vitest\'s version when using it.')) 102 + await start('typecheck', cliFilters, options) 94 103 } 95 104 96 105 function normalizeOptions(argv: CliOptions): CliOptions {
+10
packages/vitest/src/node/config.ts
··· 229 229 : BaseSequencer 230 230 } 231 231 232 + resolved.typecheck = { 233 + ...configDefaults.typecheck, 234 + ...resolved.typecheck, 235 + } 236 + 237 + if (mode === 'typecheck') { 238 + resolved.include = resolved.typecheck.include 239 + resolved.exclude = resolved.typecheck.exclude 240 + } 241 + 232 242 return resolved 233 243 }
+47
packages/vitest/src/node/core.ts
··· 11 11 import { SnapshotManager } from '../integrations/snapshot/manager' 12 12 import { clearTimeout, deepMerge, hasFailed, noop, setTimeout, slash, toArray } from '../utils' 13 13 import { getCoverageProvider } from '../integrations/coverage' 14 + import { Typechecker } from '../typecheck/typechecker' 14 15 import { createPool } from './pool' 15 16 import type { WorkerPool } from './pool' 16 17 import { createBenchmarkReporters, createReporters } from './reporters/utils' ··· 33 34 coverageProvider: CoverageProvider | null | undefined 34 35 logger: Logger 35 36 pool: WorkerPool | undefined 37 + typechecker: Typechecker | undefined 36 38 37 39 vitenode: ViteNodeServer = undefined! 38 40 ··· 151 153 ) as ResolvedConfig 152 154 } 153 155 156 + async typecheck(filters?: string[]) { 157 + const testsFilesList = await this.globTestFiles(filters) 158 + const checker = new Typechecker(this, testsFilesList) 159 + this.typechecker = checker 160 + checker.onParseEnd(async ({ files, sourceErrors }) => { 161 + await this.report('onCollected', files) 162 + if (!files.length) 163 + this.logger.printNoTestFound() 164 + else 165 + await this.report('onFinished', files) 166 + if (sourceErrors.length && !this.config.typecheck.ignoreSourceErrors) { 167 + process.exitCode = 1 168 + await this.logger.printSourceTypeErrors(sourceErrors) 169 + } 170 + // if there are source errors, we are showing it, and then terminating process 171 + if (!files.length) { 172 + const exitCode = this.config.passWithNoTests ? (process.exitCode ?? 0) : 1 173 + process.exit(exitCode) 174 + } 175 + if (this.config.watch) { 176 + await this.report('onWatcherStart', files, [ 177 + ...sourceErrors, 178 + ...this.state.getUnhandledErrors(), 179 + ]) 180 + } 181 + }) 182 + checker.onParseStart(async () => { 183 + await this.report('onInit', this) 184 + await this.report('onCollected', checker.getTestFiles()) 185 + }) 186 + checker.onWatcherRerun(async () => { 187 + await this.report('onWatcherRerun', testsFilesList, 'File change detected. Triggering rerun.') 188 + await checker.collectTests() 189 + await this.report('onCollected', checker.getTestFiles()) 190 + }) 191 + await checker.collectTests() 192 + await checker.start() 193 + } 194 + 154 195 async start(filters?: string[]) { 196 + if (this.mode === 'typecheck') { 197 + await this.typecheck(filters) 198 + return 199 + } 200 + 155 201 try { 156 202 await this.initCoverageProvider() 157 203 await this.coverageProvider?.clean(this.config.coverage.clean) ··· 470 516 this.closingPromise = Promise.allSettled([ 471 517 this.pool?.close(), 472 518 this.server.close(), 519 + this.typechecker?.stop(), 473 520 ].filter(Boolean)).then((results) => { 474 521 results.filter(r => r.status === 'rejected').forEach((err) => { 475 522 this.logger.error('error during close', (err as PromiseRejectedResult).reason)
+15 -10
packages/vitest/src/node/error.ts
··· 7 7 import { lineSplitRE, parseStacktrace, posToNumber } from '../utils/source-map' 8 8 import { F_POINTER } from '../utils/figures' 9 9 import { stringify } from '../integrations/chai/jest-matcher-utils' 10 + import { TypeCheckError } from '../typecheck/typechecker' 10 11 import type { Vitest } from './core' 11 12 import { type DiffOptions, unifiedDiff } from './diff' 12 13 import { divider } from './reporters/renderers/utils' ··· 45 46 46 47 const stacks = parseStacktrace(e, fullStack) 47 48 48 - const nearest = stacks.find(stack => 49 - ctx.server.moduleGraph.getModuleById(stack.file) 49 + const nearest = error instanceof TypeCheckError 50 + ? error.stacks[0] 51 + : stacks.find(stack => 52 + ctx.server.moduleGraph.getModuleById(stack.file) 50 53 && existsSync(stack.file), 51 - ) 54 + ) 52 55 53 56 const errorProperties = getErrorProperties(e) 54 57 55 58 if (type) 56 59 printErrorType(type, ctx) 57 60 printErrorMessage(e, ctx.logger) 61 + 62 + // if the error provide the frame 58 63 if (e.frame) { 59 - ctx.logger.log(c.yellow(e.frame)) 64 + ctx.logger.error(c.yellow(e.frame)) 60 65 } 61 66 else { 62 67 printStack(ctx, stacks, nearest, errorProperties, (s, pos) => { ··· 64 69 const file = fileFromParsedStack(nearest) 65 70 // could point to non-existing original file 66 71 // for example, when there is a source map file, but no source in node_modules 67 - if (existsSync(file)) { 72 + if (nearest.file === file || existsSync(file)) { 68 73 const sourceCode = readFileSync(file, 'utf-8') 69 - ctx.logger.log(c.yellow(generateCodeFrame(sourceCode, 4, pos))) 74 + ctx.logger.error(c.yellow(generateCodeFrame(sourceCode, 4, pos))) 70 75 } 71 76 } 72 77 }) ··· 182 187 const file = fileFromParsedStack(frame) 183 188 const path = relative(ctx.config.root, file) 184 189 185 - logger.log(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`)) 190 + logger.error(color(` ${c.dim(F_POINTER)} ${[frame.method, c.dim(`${path}:${pos.line}:${pos.column}`)].filter(Boolean).join(' ')}`)) 186 191 onStack?.(frame, pos) 187 192 188 193 // reached at test file, skip the follow stack 189 194 if (frame.file in ctx.state.filesMap) 190 195 break 191 196 } 192 - logger.log() 197 + logger.error() 193 198 const hasProperties = Object.keys(errorProperties).length > 0 194 199 if (hasProperties) { 195 - logger.log(c.red(c.dim(divider()))) 200 + logger.error(c.red(c.dim(divider()))) 196 201 const propertiesString = stringify(errorProperties, 10, { printBasicPrototype: false }) 197 - logger.log(c.red(c.bold('Serialized Error:')), c.gray(propertiesString)) 202 + logger.error(c.red(c.bold('Serialized Error:')), c.gray(propertiesString)) 198 203 } 199 204 } 200 205
+13
packages/vitest/src/node/logger.ts
··· 2 2 import c from 'picocolors' 3 3 import { version } from '../../../../package.json' 4 4 import type { ErrorWithDiff } from '../types' 5 + import type { TypeCheckError } from '../typecheck/typechecker' 5 6 import { divider } from './reporters/renderers/utils' 6 7 import type { Vitest } from './core' 7 8 import { printError } from './error' ··· 126 127 this.log(errorMessage) 127 128 await Promise.all(errors.map(async (err) => { 128 129 await this.printError(err, true, (err as ErrorWithDiff).type || 'Unhandled Error') 130 + })) 131 + this.log(c.red(divider())) 132 + } 133 + 134 + async printSourceTypeErrors(errors: TypeCheckError[]) { 135 + const errorMessage = c.red(c.bold( 136 + `\nVitest found ${errors.length} error${errors.length > 1 ? 's' : ''} not related to your test files.`, 137 + )) 138 + this.log(c.red(divider(c.bold(c.inverse(' Source Errors '))))) 139 + this.log(errorMessage) 140 + await Promise.all(errors.map(async (err) => { 141 + await this.printError(err, true) 129 142 })) 130 143 this.log(c.red(divider())) 131 144 }
+1 -1
packages/vitest/src/node/plugins/mock.ts
··· 125 125 } 126 126 } 127 127 128 - beforeChar = code[index] 128 + beforeChar = char 129 129 index++ 130 130 } 131 131
+20 -9
packages/vitest/src/node/reporters/base.ts
··· 1 1 import { performance } from 'perf_hooks' 2 2 import c from 'picocolors' 3 3 import type { ErrorWithDiff, File, Reporter, Task, TaskResultPack, UserConsoleLog } from '../../types' 4 - import { clearInterval, getFullName, getSuites, getTests, hasFailed, hasFailedSnapshot, isNode, relativePath, setInterval } from '../../utils' 4 + import { clearInterval, getFullName, getSuites, getTests, getTypecheckTests, hasFailed, hasFailedSnapshot, isNode, relativePath, setInterval } from '../../utils' 5 5 import type { Vitest } from '../../node' 6 6 import { F_RIGHT } from '../../utils/figures' 7 7 import { divider, formatTimeString, getStateString, getStateSymbol, pointer, renderSnapshotSummary } from './renderers/utils' ··· 55 55 if (errors.length) { 56 56 if (!this.ctx.config.dangerouslyIgnoreUnhandledErrors) 57 57 process.exitCode = 1 58 - this.ctx.logger.printUnhandledErrors(errors) 58 + await this.ctx.logger.printUnhandledErrors(errors) 59 59 } 60 60 } 61 61 ··· 93 93 } 94 94 } 95 95 96 - async onWatcherStart() { 96 + async onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { 97 97 this.resetLastRunLog() 98 98 99 - const files = this.ctx.state.getFiles() 100 - const errors = this.ctx.state.getUnhandledErrors() 101 99 const failed = errors.length > 0 || hasFailed(files) 102 100 const failedSnap = hasFailedSnapshot(files) 103 101 if (failed) ··· 105 103 else 106 104 this.ctx.logger.log(WAIT_FOR_CHANGE_PASS) 107 105 108 - const hints = [HELP_HINT] 106 + const hints = [] 107 + // TODO typecheck doesn't support these for now 108 + if (this.mode !== 'typecheck') 109 + hints.push(HELP_HINT) 109 110 if (failedSnap) 110 111 hints.unshift(HELP_UPDATE_SNAP) 111 112 else ··· 201 202 } 202 203 203 204 async reportTestSummary(files: File[]) { 204 - const tests = getTests(files) 205 + const tests = this.mode === 'typecheck' ? getTypecheckTests(files) : getTests(files) 205 206 const logger = this.ctx.logger 206 207 207 208 const executionTime = this.end - this.start ··· 211 212 const transformTime = Array.from(this.ctx.vitenode.fetchCache.values()).reduce((a, b) => a + (b?.duration || 0), 0) 212 213 const threadTime = collectTime + testsTime + setupTime 213 214 214 - const padTitle = (str: string) => c.dim(`${str.padStart(10)} `) 215 + const padTitle = (str: string) => c.dim(`${str.padStart(11)} `) 215 216 const time = (time: number) => { 216 217 if (time > 1000) 217 218 return `${(time / 1000).toFixed(2)}s` ··· 238 239 239 240 logger.log(padTitle('Test Files'), getStateString(files)) 240 241 logger.log(padTitle('Tests'), getStateString(tests)) 242 + if (this.mode === 'typecheck') { 243 + // has only failed checks 244 + const typechecks = getTests(files).filter(t => t.type === 'typecheck') 245 + logger.log(padTitle('Type Errors'), getStateString(typechecks, 'errors', false)) 246 + } 241 247 logger.log(padTitle('Start at'), formatTimeString(this._timeStart)) 242 248 if (this.watchFilters) 243 249 logger.log(padTitle('Duration'), time(threadTime)) 250 + else if (this.mode === 'typecheck') 251 + logger.log(padTitle('Duration'), time(executionTime)) 244 252 else 245 253 logger.log(padTitle('Duration'), time(executionTime) + c.dim(` (transform ${time(transformTime)}, setup ${time(setupTime)}, collect ${time(collectTime)}, tests ${time(testsTime)})`)) 246 254 ··· 267 275 } 268 276 269 277 if (failedTests.length) { 270 - logger.error(c.red(divider(c.bold(c.inverse(` Failed Tests ${failedTests.length} `))))) 278 + const message = this.mode === 'typecheck' ? 'Type Errors' : 'Failed Tests' 279 + logger.error(c.red(divider(c.bold(c.inverse(` ${message} ${failedTests.length} `))))) 271 280 logger.error() 272 281 273 282 await this.printTaskErrors(failedTests, errorDivider) ··· 284 293 logger.log(`\n${c.cyan(c.inverse(c.bold(' BENCH ')))} ${c.cyan('Summary')}\n`) 285 294 for (const bench of topBenchs) { 286 295 const group = bench.suite 296 + if (!group) 297 + continue 287 298 const groupName = getFullName(group) 288 299 logger.log(` ${bench.name}${c.dim(` - ${groupName}`)}`) 289 300 const siblings = group.tasks
+1 -1
packages/vitest/src/node/reporters/benchmark/json.ts
··· 34 34 continue 35 35 if (!outputFile) 36 36 res.samples = 'ignore on terminal' as any 37 - testResults[test.suite.name] = (testResults[test.suite.name] || []).concat(res) 37 + testResults[test.suite!.name] = (testResults[test.suite!.name] || []).concat(res) 38 38 } 39 39 40 40 if (tests.some(t => t.result?.state === 'run')) {
+7 -5
packages/vitest/src/node/reporters/default.ts
··· 1 1 import c from 'picocolors' 2 - import type { UserConsoleLog } from '../../types' 2 + import type { File, UserConsoleLog } from '../../types' 3 3 import { BaseReporter } from './base' 4 4 import type { ListRendererOptions } from './renderers/listRenderer' 5 5 import { createListRenderer } from './renderers/listRenderer' ··· 18 18 super.onWatcherStart() 19 19 } 20 20 21 - onCollected() { 21 + onCollected(files?: File[]) { 22 22 if (this.isTTY) { 23 23 this.rendererOptions.logger = this.ctx.logger 24 24 this.rendererOptions.showHeap = this.ctx.config.logHeapUsage 25 - const files = this.ctx.state.getFiles(this.watchFilters) 25 + this.rendererOptions.mode = this.mode 26 + if (!files) 27 + files = this.ctx.state.getFiles(this.watchFilters) 26 28 if (!this.renderer) 27 29 this.renderer = createListRenderer(files, this.rendererOptions).start() 28 30 else ··· 36 38 await super.onFinished(files, errors) 37 39 } 38 40 39 - async onWatcherStart() { 41 + async onWatcherStart(files = this.ctx.state.getFiles(), errors = this.ctx.state.getUnhandledErrors()) { 40 42 await this.stopListRender() 41 - await super.onWatcherStart() 43 + await super.onWatcherStart(files, errors) 42 44 } 43 45 44 46 async stopListRender() {
+7 -4
packages/vitest/src/node/reporters/renderers/listRenderer.ts
··· 1 1 import c from 'picocolors' 2 2 import cliTruncate from 'cli-truncate' 3 3 import stripAnsi from 'strip-ansi' 4 - import type { Benchmark, BenchmarkResult, SuiteHooks, Task } from '../../../types' 5 - import { clearInterval, getTests, notNullish, setInterval } from '../../../utils' 4 + import type { Benchmark, BenchmarkResult, SuiteHooks, Task, VitestRunMode } from '../../../types' 5 + import { clearInterval, getTests, getTypecheckTests, isTypecheckTest, notNullish, setInterval } from '../../../utils' 6 6 import { F_RIGHT } from '../../../utils/figures' 7 7 import type { Logger } from '../../logger' 8 8 import { getCols, getHookStateSymbol, getStateSymbol } from './utils' ··· 11 11 renderSucceed?: boolean 12 12 logger: Logger 13 13 showHeap: boolean 14 + mode: VitestRunMode 14 15 } 15 16 16 17 const DURATION_LONG = 300 ··· 95 96 if (task.type === 'test' && task.result?.retryCount && task.result.retryCount > 1) 96 97 suffix += c.yellow(` (retry x${task.result.retryCount})`) 97 98 98 - if (task.type === 'suite') 99 - suffix += c.dim(` (${getTests(task).length})`) 99 + if (task.type === 'suite' && !isTypecheckTest(task)) { 100 + const tests = options.mode === 'typecheck' ? getTypecheckTests(task) : getTests(task) 101 + suffix += c.dim(` (${tests.length})`) 102 + } 100 103 101 104 if (task.mode === 'skip' || task.mode === 'todo') 102 105 suffix += ` ${c.dim(c.gray('[skipped]'))}`
+2 -2
packages/vitest/src/node/reporters/renderers/utils.ts
··· 91 91 return summary 92 92 } 93 93 94 - export function getStateString(tasks: Task[], name = 'tests') { 94 + export function getStateString(tasks: Task[], name = 'tests', showTotal = true) { 95 95 if (tasks.length === 0) 96 96 return c.dim(`no ${name}`) 97 97 ··· 105 105 passed.length ? c.bold(c.green(`${passed.length} passed`)) : null, 106 106 skipped.length ? c.yellow(`${skipped.length} skipped`) : null, 107 107 todo.length ? c.gray(`${todo.length} todo`) : null, 108 - ].filter(Boolean).join(c.dim(' | ')) + c.gray(` (${tasks.length})`) 108 + ].filter(Boolean).join(c.dim(' | ')) + (showTotal ? c.gray(` (${tasks.length})`) : '') 109 109 } 110 110 111 111 export function getStateSymbol(task: Task) {
+8 -3
packages/vitest/src/node/stdin.ts
··· 36 36 37 37 const name = key?.name 38 38 39 + // quit 40 + if (name === 'q') 41 + return ctx.exit(true) 42 + 43 + // TODO typechecking doesn't support shortcuts this yet 44 + if (ctx.mode === 'typecheck') 45 + return 46 + 39 47 // help 40 48 if (name === 'h') 41 49 return printShortcutsHelp() ··· 54 62 // change fileNamePattern 55 63 if (name === 'p') 56 64 return inputFilePattern() 57 - // quit 58 - if (name === 'q') 59 - return ctx.exit(true) 60 65 } 61 66 62 67 async function keypressHandler(str: string, key: any) {
+2 -70
packages/vitest/src/runtime/collect.ts
··· 1 - import type { File, ResolvedConfig, Suite, TaskBase } from '../types' 1 + import type { File, ResolvedConfig, Suite } from '../types' 2 2 import { getWorkerState, isBrowser, relativePath } from '../utils' 3 + import { interpretTaskModes, someTasksAreOnly } from '../utils/collect' 3 4 import { clearCollectorContext, defaultSuite } from './suite' 4 5 import { getHooks, setHooks } from './map' 5 6 import { processError } from './error' ··· 97 98 } 98 99 99 100 return files 100 - } 101 - 102 - /** 103 - * If any tasks been marked as `only`, mark all other tasks as `skip`. 104 - */ 105 - function interpretTaskModes(suite: Suite, namePattern?: string | RegExp, onlyMode?: boolean, parentIsOnly?: boolean, allowOnly?: boolean) { 106 - const suiteIsOnly = parentIsOnly || suite.mode === 'only' 107 - 108 - suite.tasks.forEach((t) => { 109 - // Check if either the parent suite or the task itself are marked as included 110 - const includeTask = suiteIsOnly || t.mode === 'only' 111 - if (onlyMode) { 112 - if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) { 113 - // Don't skip this suite 114 - if (t.mode === 'only') { 115 - checkAllowOnly(t, allowOnly) 116 - t.mode = 'run' 117 - } 118 - } 119 - else if (t.mode === 'run' && !includeTask) { t.mode = 'skip' } 120 - else if (t.mode === 'only') { 121 - checkAllowOnly(t, allowOnly) 122 - t.mode = 'run' 123 - } 124 - } 125 - if (t.type === 'test') { 126 - if (namePattern && !getTaskFullName(t).match(namePattern)) 127 - t.mode = 'skip' 128 - } 129 - else if (t.type === 'suite') { 130 - if (t.mode === 'skip') 131 - skipAllTasks(t) 132 - else 133 - interpretTaskModes(t, namePattern, onlyMode, includeTask, allowOnly) 134 - } 135 - }) 136 - 137 - // if all subtasks are skipped, mark as skip 138 - if (suite.mode === 'run') { 139 - if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) 140 - suite.mode = 'skip' 141 - } 142 - } 143 - 144 - function getTaskFullName(task: TaskBase): string { 145 - return `${task.suite ? `${getTaskFullName(task.suite)} ` : ''}${task.name}` 146 - } 147 - 148 - function someTasksAreOnly(suite: Suite): boolean { 149 - return suite.tasks.some(t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t))) 150 - } 151 - 152 - function skipAllTasks(suite: Suite) { 153 - suite.tasks.forEach((t) => { 154 - if (t.mode === 'run') { 155 - t.mode = 'skip' 156 - if (t.type === 'suite') 157 - skipAllTasks(t) 158 - } 159 - }) 160 - } 161 - 162 - function checkAllowOnly(task: TaskBase, allowOnly?: boolean) { 163 - if (allowOnly) 164 - return 165 - task.result = { 166 - state: 'fail', 167 - error: processError(new Error('[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error')), 168 - } 169 101 } 170 102 171 103 function calculateHash(parent: Suite) {
+7
packages/vitest/src/typecheck/assertType.ts
··· 1 + const noop = () => {} 2 + 3 + export interface AssertType { 4 + <T>(value: T): void 5 + } 6 + 7 + export const assertType: AssertType = noop
+142
packages/vitest/src/typecheck/collect.ts
··· 1 + import { relative } from 'pathe' 2 + import { parse as parseAst } from 'acorn' 3 + import { ancestor as walkAst } from 'acorn-walk' 4 + import type { RawSourceMap } from 'vite-node' 5 + 6 + import type { File, Suite, Vitest } from '../types' 7 + import { interpretTaskModes, someTasksAreOnly } from '../utils/collect' 8 + import { TYPECHECK_SUITE } from './constants' 9 + 10 + interface ParsedFile extends File { 11 + start: number 12 + end: number 13 + } 14 + 15 + interface ParsedSuite extends Suite { 16 + start: number 17 + end: number 18 + } 19 + 20 + interface LocalCallDefinition { 21 + start: number 22 + end: number 23 + name: string 24 + type: string 25 + mode: 'run' | 'skip' | 'only' | 'todo' 26 + task: ParsedSuite | ParsedFile 27 + } 28 + 29 + export interface FileInformation { 30 + file: ParsedFile 31 + filepath: string 32 + parsed: string 33 + map: RawSourceMap | null 34 + definitions: LocalCallDefinition[] 35 + } 36 + 37 + export async function collectTests(ctx: Vitest, filepath: string): Promise<null | FileInformation> { 38 + const request = await ctx.vitenode.transformRequest(filepath) 39 + if (!request) 40 + return null 41 + const ast = parseAst(request.code, { 42 + ecmaVersion: 'latest', 43 + allowAwaitOutsideFunction: true, 44 + }) 45 + const file: ParsedFile = { 46 + filepath, 47 + type: 'suite', 48 + id: '-1', 49 + name: relative(ctx.config.root, filepath), 50 + mode: 'run', 51 + tasks: [], 52 + start: ast.start, 53 + end: ast.end, 54 + result: { 55 + state: 'pass', 56 + }, 57 + } 58 + const definitions: LocalCallDefinition[] = [] 59 + const getName = (callee: any): string | null => { 60 + if (!callee) 61 + return null 62 + if (callee.type === 'Identifier') 63 + return callee.name 64 + if (callee.type === 'MemberExpression') { 65 + // direct call as `__vite_ssr_exports_0__.test()` 66 + if (callee.object?.name?.startsWith('__vite_ssr_')) 67 + return getName(callee.property) 68 + // call as `__vite_ssr__.test.skip()` 69 + return getName(callee.object?.property) 70 + } 71 + return null 72 + } 73 + walkAst(ast, { 74 + CallExpression(node) { 75 + const { callee } = node as any 76 + const name = getName(callee) 77 + if (!name) 78 + return 79 + if (!['it', 'test', 'describe', 'suite'].includes(name)) 80 + return 81 + const { arguments: [{ value: message }] } = node as any 82 + const property = callee?.property?.name 83 + const mode = !property || property === name ? 'run' : property 84 + if (!['run', 'skip', 'todo', 'only'].includes(mode)) 85 + throw new Error(`${name}.${mode} syntax is not supported when testing types`) 86 + definitions.push({ 87 + start: node.start, 88 + end: node.end, 89 + name: message, 90 + type: name, 91 + mode, 92 + } as LocalCallDefinition) 93 + }, 94 + }) 95 + let lastSuite: ParsedSuite = file 96 + const updateLatestSuite = (index: number) => { 97 + const suite = lastSuite 98 + while (lastSuite !== file && lastSuite.end < index) 99 + lastSuite = suite.suite as ParsedSuite 100 + return lastSuite 101 + } 102 + definitions.sort((a, b) => a.start - b.start).forEach((definition, idx) => { 103 + const latestSuite = updateLatestSuite(definition.start) 104 + let mode = definition.mode 105 + if (latestSuite.mode !== 'run') // inherit suite mode, if it's set 106 + mode = latestSuite.mode 107 + const state = mode === 'run' ? 'pass' : mode 108 + // expectTypeOf and any type error is actually a "test" ("typecheck"), 109 + // and all "test"s should be inside a "suite", so semantics inside typecheck for "test" changes 110 + // if we ever allow having multiple errors in a test, we can change type to "test" 111 + const task: ParsedSuite = { 112 + type: 'suite', 113 + id: idx.toString(), 114 + suite: latestSuite, 115 + file, 116 + tasks: [], 117 + mode, 118 + name: definition.name, 119 + end: definition.end, 120 + start: definition.start, 121 + result: { 122 + state, 123 + }, 124 + } 125 + definition.task = task 126 + latestSuite.tasks.push(task) 127 + if (definition.type === 'describe' || definition.type === 'suite') 128 + lastSuite = task 129 + else 130 + // to show correct amount of "tests" in summary, we mark this with a special symbol 131 + Object.defineProperty(task, TYPECHECK_SUITE, { value: true }) 132 + }) 133 + const hasOnly = someTasksAreOnly(file) 134 + interpretTaskModes(file, ctx.config.testNamePattern, hasOnly, false, ctx.config.allowOnly) 135 + return { 136 + file, 137 + parsed: request.code, 138 + filepath, 139 + map: request.map as RawSourceMap | null, 140 + definitions, 141 + } 142 + }
+2
packages/vitest/src/typecheck/constants.ts
··· 1 + export const EXPECT_TYPEOF_MATCHERS = Symbol('vitest:expect-typeof-matchers') 2 + export const TYPECHECK_SUITE = Symbol('vitest:typecheck-suite')
+1
packages/vitest/src/typecheck/expectTypeOf.ts
··· 1 + export { expectTypeOf, type ExpectTypeOf } from 'expect-type'
+126
packages/vitest/src/typecheck/parse.ts
··· 1 + import path from 'node:path' 2 + import url from 'node:url' 3 + import { writeFile } from 'node:fs/promises' 4 + import { getTsconfig } from 'get-tsconfig' 5 + import type { TypecheckConfig } from '../types' 6 + import type { RawErrsMap, TscErrorInfo } from './types' 7 + 8 + const __dirname = url.fileURLToPath(new URL('.', import.meta.url)) 9 + const newLineRegExp = /\r?\n/ 10 + const errCodeRegExp = /error TS(?<errCode>\d+)/ 11 + 12 + export async function makeTscErrorInfo( 13 + errInfo: string, 14 + ): Promise<[string, TscErrorInfo | null]> { 15 + const [errFilePathPos = '', ...errMsgRawArr] = errInfo.split(':') 16 + if ( 17 + !errFilePathPos 18 + || errMsgRawArr.length === 0 19 + || errMsgRawArr.join('').length === 0 20 + ) 21 + return ['unknown filepath', null] 22 + 23 + const errMsgRaw = errMsgRawArr.join('').trim() 24 + 25 + // get filePath, line, col 26 + const [errFilePath, errPos] = errFilePathPos 27 + .slice(0, -1) // removes the ')' 28 + .split('(') 29 + if (!errFilePath || !errPos) 30 + return ['unknown filepath', null] 31 + 32 + const [errLine, errCol] = errPos.split(',') 33 + if (!errLine || !errCol) 34 + return [errFilePath, null] 35 + 36 + // get errCode, errMsg 37 + const execArr = errCodeRegExp.exec(errMsgRaw) 38 + if (!execArr) 39 + return [errFilePath, null] 40 + 41 + const errCodeStr = execArr.groups?.errCode ?? '' 42 + if (!errCodeStr) 43 + return [errFilePath, null] 44 + 45 + const line = Number(errLine) 46 + const col = Number(errCol) 47 + const errCode = Number(errCodeStr) 48 + return [ 49 + errFilePath, 50 + { 51 + filePath: errFilePath, 52 + errCode, 53 + line, 54 + column: col, 55 + errMsg: errMsgRaw.slice(`error TS${errCode} `.length), 56 + }, 57 + ] 58 + } 59 + 60 + export async function getTsconfigPath(root: string, config: TypecheckConfig) { 61 + const tempConfigPath = path.join(root, 'tsconfig.temp.json') 62 + 63 + const configName = config.tsconfig?.includes('jsconfig.json') 64 + ? 'jsconfig.json' 65 + : undefined 66 + 67 + const tsconfig = getTsconfig(config.tsconfig || root, configName) 68 + 69 + if (!tsconfig) 70 + throw new Error('no tsconfig.json found') 71 + 72 + try { 73 + const tmpTsConfig: Record<string, any> = { ...tsconfig.config } 74 + 75 + tmpTsConfig.compilerOptions ??= {} 76 + tmpTsConfig.compilerOptions.emitDeclarationOnly = false 77 + tmpTsConfig.compilerOptions.incremental = true 78 + tmpTsConfig.compilerOptions.tsBuildInfoFile = path.join( 79 + __dirname, 80 + 'tsconfig.tmp.tsbuildinfo', 81 + ) 82 + 83 + const tsconfigFinalContent = JSON.stringify(tmpTsConfig, null, 2) 84 + await writeFile(tempConfigPath, tsconfigFinalContent) 85 + return tempConfigPath 86 + } 87 + catch (err) { 88 + throw new Error('failed to write tsconfig.temp.json', { cause: err }) 89 + } 90 + } 91 + 92 + export async function getRawErrsMapFromTsCompile( 93 + tscErrorStdout: string, 94 + ) { 95 + const rawErrsMap: RawErrsMap = new Map() 96 + 97 + // Merge details line with main line (i.e. which contains file path) 98 + const infos = await Promise.all( 99 + tscErrorStdout 100 + .split(newLineRegExp) 101 + .reduce<string[]>((prev, next) => { 102 + if (!next) 103 + return prev 104 + 105 + else if (!next.startsWith(' ')) 106 + prev.push(next) 107 + 108 + else 109 + prev[prev.length - 1] += `\n${next}` 110 + 111 + return prev 112 + }, []) 113 + .map(errInfoLine => makeTscErrorInfo(errInfoLine)), 114 + ) 115 + infos.forEach(([errFilePath, errInfo]) => { 116 + if (!errInfo) 117 + return 118 + 119 + if (!rawErrsMap.has(errFilePath)) 120 + rawErrsMap.set(errFilePath, [errInfo]) 121 + 122 + else 123 + rawErrsMap.get(errFilePath)?.push(errInfo) 124 + }) 125 + return rawErrsMap 126 + }
+244
packages/vitest/src/typecheck/typechecker.ts
··· 1 + import { rm } from 'fs/promises' 2 + import type { ExecaChildProcess } from 'execa' 3 + import { execa } from 'execa' 4 + import { resolve } from 'pathe' 5 + import { SourceMapConsumer } from 'source-map' 6 + import { ensurePackageInstalled } from '../utils' 7 + import type { Awaitable, File, ParsedStack, Task, TscErrorInfo, Vitest } from '../types' 8 + import { getRawErrsMapFromTsCompile, getTsconfigPath } from './parse' 9 + import { createIndexMap } from './utils' 10 + import type { FileInformation } from './collect' 11 + import { collectTests } from './collect' 12 + 13 + export class TypeCheckError extends Error { 14 + name = 'TypeCheckError' 15 + 16 + constructor(public message: string, public stacks: ParsedStack[]) { 17 + super(message) 18 + } 19 + } 20 + 21 + interface ErrorsCache { 22 + files: File[] 23 + sourceErrors: TypeCheckError[] 24 + } 25 + 26 + type Callback<Args extends Array<any> = []> = (...args: Args) => Awaitable<void> 27 + 28 + export class Typechecker { 29 + private _onParseStart?: Callback 30 + private _onParseEnd?: Callback<[ErrorsCache]> 31 + private _onWatcherRerun?: Callback 32 + private _result: ErrorsCache = { 33 + files: [], 34 + sourceErrors: [], 35 + } 36 + 37 + private _tests: Record<string, FileInformation> | null = {} 38 + private tempConfigPath?: string 39 + private process!: ExecaChildProcess 40 + 41 + constructor(protected ctx: Vitest, protected files: string[]) {} 42 + 43 + public onParseStart(fn: Callback) { 44 + this._onParseStart = fn 45 + } 46 + 47 + public onParseEnd(fn: Callback<[ErrorsCache]>) { 48 + this._onParseEnd = fn 49 + } 50 + 51 + public onWatcherRerun(fn: Callback) { 52 + this._onWatcherRerun = fn 53 + } 54 + 55 + protected async collectFileTests(filepath: string): Promise<FileInformation | null> { 56 + return collectTests(this.ctx, filepath) 57 + } 58 + 59 + public async collectTests() { 60 + const tests = (await Promise.all( 61 + this.files.map(filepath => this.collectFileTests(filepath)), 62 + )).reduce((acc, data) => { 63 + if (!data) 64 + return acc 65 + acc[data.filepath] = data 66 + return acc 67 + }, {} as Record<string, FileInformation>) 68 + this._tests = tests 69 + return tests 70 + } 71 + 72 + protected async prepareResults(output: string) { 73 + const typeErrors = await this.parseTscLikeOutput(output) 74 + const testFiles = new Set(this.files) 75 + 76 + if (!this._tests) 77 + this._tests = await this.collectTests() 78 + 79 + const sourceErrors: TypeCheckError[] = [] 80 + const files: File[] = [] 81 + 82 + testFiles.forEach((path) => { 83 + const { file, definitions, map, parsed } = this._tests![path] 84 + const errors = typeErrors.get(path) 85 + files.push(file) 86 + if (!errors) 87 + return 88 + const sortedDefinitions = [...definitions.sort((a, b) => b.start - a.start)] 89 + // has no map for ".js" files that use // @ts-check 90 + const mapConsumer = map && new SourceMapConsumer(map) 91 + const indexMap = createIndexMap(parsed) 92 + const markFailed = (task: Task) => { 93 + task.result = { 94 + state: task.mode === 'run' || task.mode === 'only' ? 'fail' : task.mode, 95 + } 96 + if (task.suite) 97 + markFailed(task.suite) 98 + } 99 + errors.forEach(({ error, originalError }, idx) => { 100 + const originalPos = mapConsumer?.generatedPositionFor({ 101 + line: originalError.line, 102 + column: originalError.column, 103 + source: path, 104 + }) || originalError 105 + const index = indexMap.get(`${originalPos.line}:${originalPos.column}`) 106 + const definition = (index != null && sortedDefinitions.find(def => def.start <= index && def.end >= index)) || file 107 + const suite = 'task' in definition ? definition.task : definition 108 + const state = suite.mode === 'run' || suite.mode === 'only' ? 'fail' : suite.mode 109 + const task: Task = { 110 + type: 'typecheck', 111 + id: idx.toString(), 112 + name: `error expect ${idx + 1}`, // TODO naming 113 + mode: suite.mode, 114 + file, 115 + suite, 116 + result: { 117 + state, 118 + error: state === 'fail' ? error : undefined, 119 + }, 120 + } 121 + if (state === 'fail') 122 + markFailed(suite) 123 + suite.tasks.push(task) 124 + }) 125 + }) 126 + 127 + typeErrors.forEach((errors, path) => { 128 + if (!testFiles.has(path)) 129 + sourceErrors.push(...errors.map(({ error }) => error)) 130 + }) 131 + 132 + return { 133 + files, 134 + sourceErrors, 135 + } 136 + } 137 + 138 + protected async parseTscLikeOutput(output: string) { 139 + const errorsMap = await getRawErrsMapFromTsCompile(output) 140 + const typesErrors = new Map<string, { error: TypeCheckError; originalError: TscErrorInfo }[]>() 141 + errorsMap.forEach((errors, path) => { 142 + const filepath = resolve(this.ctx.config.root, path) 143 + const suiteErrors = errors.map((info) => { 144 + const limit = Error.stackTraceLimit 145 + Error.stackTraceLimit = 0 146 + const error = new TypeCheckError(info.errMsg, [ 147 + { 148 + file: filepath, 149 + line: info.line, 150 + column: info.column, 151 + method: '', 152 + sourcePos: { 153 + line: info.line, 154 + column: info.column, 155 + }, 156 + }, 157 + ]) 158 + Error.stackTraceLimit = limit 159 + return { 160 + originalError: info, 161 + error, 162 + } 163 + }) 164 + typesErrors.set(filepath, suiteErrors) 165 + }) 166 + return typesErrors 167 + } 168 + 169 + public async clear() { 170 + if (this.tempConfigPath) 171 + await rm(this.tempConfigPath, { force: true }) 172 + } 173 + 174 + public async stop() { 175 + await this.clear() 176 + this.process?.kill() 177 + } 178 + 179 + protected async ensurePackageInstalled(root: string, checker: string) { 180 + if (checker !== 'tsc' && checker !== 'vue-tsc') 181 + return 182 + const packageName = checker === 'tsc' ? 'typescript' : 'vue-tsc' 183 + await ensurePackageInstalled(packageName, root) 184 + } 185 + 186 + public async start() { 187 + const { root, watch, typecheck } = this.ctx.config 188 + await this.ensurePackageInstalled(root, typecheck.checker) 189 + 190 + this.tempConfigPath = await getTsconfigPath(root, typecheck) 191 + const args = ['--noEmit', '--pretty', 'false', '-p', this.tempConfigPath] 192 + // use builtin watcher, because it's faster 193 + if (watch) 194 + args.push('--watch') 195 + if (typecheck.allowJs) 196 + args.push('--allowJs', '--checkJs') 197 + let output = '' 198 + const child = execa(typecheck.checker, args, { 199 + cwd: root, 200 + stdout: 'pipe', 201 + reject: false, 202 + }) 203 + this.process = child 204 + await this._onParseStart?.() 205 + let rerunTriggered = false 206 + child.stdout?.on('data', (chunk) => { 207 + output += chunk 208 + if (!watch) 209 + return 210 + if (output.includes('File change detected') && !rerunTriggered) { 211 + this._onWatcherRerun?.() 212 + this._result.sourceErrors = [] 213 + this._result.files = [] 214 + this._tests = null // test structure migh've changed 215 + rerunTriggered = true 216 + } 217 + if (/Found \w+ errors*. Watching for/.test(output)) { 218 + rerunTriggered = false 219 + this.prepareResults(output).then((result) => { 220 + this._result = result 221 + this._onParseEnd?.(result) 222 + }) 223 + output = '' 224 + } 225 + }) 226 + if (!watch) { 227 + await child 228 + this._result = await this.prepareResults(output) 229 + await this._onParseEnd?.(this._result) 230 + await this.clear() 231 + } 232 + } 233 + 234 + public getResult() { 235 + return this._result 236 + } 237 + 238 + public getTestFiles() { 239 + return Object.values(this._tests || {}).map(({ file }) => ({ 240 + ...file, 241 + result: undefined, 242 + })) 243 + } 244 + }
+25
packages/vitest/src/typecheck/types.ts
··· 1 + export type RawErrsMap = Map<string, TscErrorInfo[]> 2 + export interface TscErrorInfo { 3 + filePath: string 4 + errCode: number 5 + errMsg: string 6 + line: number 7 + column: number 8 + } 9 + export interface CollectLineNumbers { 10 + target: number 11 + next: number 12 + prev?: number 13 + } 14 + export type CollectLines = { 15 + [key in keyof CollectLineNumbers]: string 16 + } 17 + export interface RootAndTarget { 18 + root: string 19 + targetAbsPath: string 20 + } 21 + export type Context = RootAndTarget & { 22 + rawErrsMap: RawErrsMap 23 + openedDirs: Set<string> 24 + lastActivePath?: string 25 + }
+17
packages/vitest/src/typecheck/utils.ts
··· 1 + export const createIndexMap = (source: string) => { 2 + const map = new Map<string, number>() 3 + let index = 0 4 + let line = 1 5 + let column = 1 6 + for (const char of source) { 7 + map.set(`${line}:${column}`, index++) 8 + if (char === '\n' || char === '\r\n') { 9 + line++ 10 + column = 0 11 + } 12 + else { 13 + column++ 14 + } 15 + } 16 + return map 17 + }
+38 -7
packages/vitest/src/types/config.ts
··· 27 27 [x: string]: unknown 28 28 } 29 29 30 - export type VitestRunMode = 'test' | 'benchmark' 30 + export type VitestRunMode = 'test' | 'benchmark' | 'typecheck' 31 31 32 32 export interface InlineConfig { 33 33 /** ··· 443 443 * Ignore any unhandled errors that occur 444 444 */ 445 445 dangerouslyIgnoreUnhandledErrors?: boolean 446 + 447 + /** 448 + * Options for configuring typechecking test environment. 449 + */ 450 + typecheck?: Partial<TypecheckConfig> 451 + } 452 + 453 + export interface TypecheckConfig { 454 + /** 455 + * What tools to use for type checking. 456 + */ 457 + checker: 'tsc' | 'vue-tsc' | (string & Record<never, never>) 458 + /** 459 + * Pattern for files that should be treated as test files 460 + */ 461 + include: string[] 462 + /** 463 + * Pattern for files that should not be treated as test files 464 + */ 465 + exclude: string[] 466 + /** 467 + * Check JS files that have `@ts-check` comment. 468 + * If you have it enabled in tsconfig, this will not overwrite it. 469 + */ 470 + allowJs?: boolean 471 + /** 472 + * Do not fail, if Vitest found errors outside the test files. 473 + */ 474 + ignoreSourceErrors?: boolean 475 + /** 476 + * Path to tsconfig, relative to the project root. 477 + */ 478 + tsconfig?: string 446 479 } 447 480 448 481 export interface UserConfig extends InlineConfig { 449 482 /** 450 483 * Path to the config file. 451 484 * 452 - * Default resolving to one of: 453 - * - `vitest.config.js` 454 - * - `vitest.config.ts` 455 - * - `vite.config.js` 456 - * - `vite.config.ts` 485 + * Default resolving to `vitest.config.*`, `vite.config.*` 457 486 */ 458 487 config?: string | undefined 459 488 ··· 489 518 shard?: string 490 519 } 491 520 492 - export interface ResolvedConfig extends Omit<Required<UserConfig>, 'config' | 'filters' | 'coverage' | 'testNamePattern' | 'related' | 'api' | 'reporters' | 'resolveSnapshotPath' | 'benchmark' | 'shard' | 'cache' | 'sequence'> { 521 + export interface ResolvedConfig extends Omit<Required<UserConfig>, 'config' | 'filters' | 'coverage' | 'testNamePattern' | 'related' | 'api' | 'reporters' | 'resolveSnapshotPath' | 'benchmark' | 'shard' | 'cache' | 'sequence' | 'typecheck'> { 493 522 mode: VitestRunMode 494 523 495 524 base?: string ··· 526 555 shuffle?: boolean 527 556 seed?: number 528 557 } 558 + 559 + typecheck: TypecheckConfig 529 560 }
+3
packages/vitest/src/types/index.ts
··· 1 1 import './vite' 2 2 import './global' 3 3 4 + export { expectTypeOf, type ExpectTypeOf } from '../typecheck/expectTypeOf' 5 + export { assertType, type AssertType } from '../typecheck/assertType' 6 + export * from '../typecheck/types' 4 7 export * from './config' 5 8 export * from './tasks' 6 9 export * from './reporter'
+1 -1
packages/vitest/src/types/reporter.ts
··· 10 10 onTaskUpdate?: (packs: TaskResultPack[]) => Awaitable<void> 11 11 12 12 onTestRemoved?: (trigger?: string) => Awaitable<void> 13 - onWatcherStart?: () => Awaitable<void> 13 + onWatcherStart?: (files?: File[], errors?: unknown[]) => Awaitable<void> 14 14 onWatcherRerun?: (files: string[], trigger?: string) => Awaitable<void> 15 15 16 16 onServerRestart?: (reason?: string) => Awaitable<void>
+5 -1
packages/vitest/src/types/tasks.ts
··· 55 55 onFailed?: OnTestFailedHandler[] 56 56 } 57 57 58 - export type Task = Test | Suite | File | Benchmark 58 + export interface TypeCheck extends TaskBase { 59 + type: 'typecheck' 60 + } 61 + 62 + export type Task = Test | Suite | File | Benchmark | TypeCheck 59 63 60 64 export type DoneCallback = (error?: any) => void 61 65 export type TestFunction<ExtraContext = {}> = (context: TestContext & ExtraContext) => Awaitable<any> | void
+70
packages/vitest/src/utils/collect.ts
··· 1 + import type { Suite, TaskBase } from '../types' 2 + 3 + /** 4 + * If any tasks been marked as `only`, mark all other tasks as `skip`. 5 + */ 6 + export function interpretTaskModes(suite: Suite, namePattern?: string | RegExp, onlyMode?: boolean, parentIsOnly?: boolean, allowOnly?: boolean) { 7 + const suiteIsOnly = parentIsOnly || suite.mode === 'only' 8 + 9 + suite.tasks.forEach((t) => { 10 + // Check if either the parent suite or the task itself are marked as included 11 + const includeTask = suiteIsOnly || t.mode === 'only' 12 + if (onlyMode) { 13 + if (t.type === 'suite' && (includeTask || someTasksAreOnly(t))) { 14 + // Don't skip this suite 15 + if (t.mode === 'only') { 16 + checkAllowOnly(t, allowOnly) 17 + t.mode = 'run' 18 + } 19 + } 20 + else if (t.mode === 'run' && !includeTask) { t.mode = 'skip' } 21 + else if (t.mode === 'only') { 22 + checkAllowOnly(t, allowOnly) 23 + t.mode = 'run' 24 + } 25 + } 26 + if (t.type === 'test') { 27 + if (namePattern && !getTaskFullName(t).match(namePattern)) 28 + t.mode = 'skip' 29 + } 30 + else if (t.type === 'suite') { 31 + if (t.mode === 'skip') 32 + skipAllTasks(t) 33 + else 34 + interpretTaskModes(t, namePattern, onlyMode, includeTask, allowOnly) 35 + } 36 + }) 37 + 38 + // if all subtasks are skipped, mark as skip 39 + if (suite.mode === 'run') { 40 + if (suite.tasks.length && suite.tasks.every(i => i.mode !== 'run')) 41 + suite.mode = 'skip' 42 + } 43 + } 44 + 45 + function getTaskFullName(task: TaskBase): string { 46 + return `${task.suite ? `${getTaskFullName(task.suite)} ` : ''}${task.name}` 47 + } 48 + 49 + export function someTasksAreOnly(suite: Suite): boolean { 50 + return suite.tasks.some(t => t.mode === 'only' || (t.type === 'suite' && someTasksAreOnly(t))) 51 + } 52 + 53 + function skipAllTasks(suite: Suite) { 54 + suite.tasks.forEach((t) => { 55 + if (t.mode === 'run') { 56 + t.mode = 'skip' 57 + if (t.type === 'suite') 58 + skipAllTasks(t) 59 + } 60 + }) 61 + } 62 + 63 + function checkAllowOnly(task: TaskBase, allowOnly?: boolean) { 64 + if (allowOnly) 65 + return 66 + task.result = { 67 + state: 'fail', 68 + error: new Error('[Vitest] Unexpected .only modifier. Remove it or pass --allowOnly argument to bypass this error'), 69 + } 70 + }
+17 -4
packages/vitest/src/utils/tasks.ts
··· 1 - import type { Arrayable, Benchmark, Suite, Task, Test } from '../types' 1 + import type { Arrayable, Benchmark, Suite, Task, Test, TypeCheck } from '../types' 2 + import { TYPECHECK_SUITE } from '../typecheck/constants' 2 3 import { toArray } from './base' 3 4 4 - function isAtomTest(s: Task): s is Test | Benchmark { 5 - return (s.type === 'test' || s.type === 'benchmark') 5 + function isAtomTest(s: Task): s is Test | Benchmark | TypeCheck { 6 + return (s.type === 'test' || s.type === 'benchmark' || s.type === 'typecheck') 6 7 } 7 8 8 - export function getTests(suite: Arrayable<Task>): (Test | Benchmark)[] { 9 + export function getTests(suite: Arrayable<Task>): (Test | Benchmark | TypeCheck)[] { 9 10 return toArray(suite).flatMap(s => isAtomTest(s) ? [s] : s.tasks.flatMap(c => isAtomTest(c) ? [c] : getTests(c))) 11 + } 12 + 13 + export function isTypecheckTest(suite: Task): suite is Suite { 14 + return TYPECHECK_SUITE in suite 15 + } 16 + 17 + export function getTypecheckTests(suite: Arrayable<Task>): Suite[] { 18 + return toArray(suite).flatMap((s) => { 19 + if (s.type !== 'suite') 20 + return [] 21 + return TYPECHECK_SUITE in s ? [s, ...getTypecheckTests(s.tasks)] : getTypecheckTests(s.tasks) 22 + }) 10 23 } 11 24 12 25 export function getTasks(tasks: Arrayable<Task> = []): Task[] {
+97 -1
pnpm-lock.yaml
··· 748 748 packages/vite-node: 749 749 specifiers: 750 750 '@types/debug': ^4.1.7 751 + '@types/source-map': ^0.5.7 751 752 '@types/source-map-support': ^0.5.6 752 753 cac: ^6.7.14 753 754 debug: ^4.3.4 ··· 755 756 pathe: ^0.2.0 756 757 picocolors: ^1.0.0 757 758 rollup: ^2.79.1 759 + source-map: ^0.6.1 758 760 source-map-support: ^0.5.21 759 761 vite: ^3.2.3 760 762 dependencies: 761 763 debug: 4.3.4 762 764 mlly: 0.5.16 763 765 pathe: 0.2.0 766 + source-map: 0.6.1 764 767 source-map-support: 0.5.21 765 768 vite: 3.2.3 766 769 devDependencies: 767 770 '@types/debug': 4.1.7 771 + '@types/source-map': 0.5.7 768 772 '@types/source-map-support': 0.5.6 769 773 cac: 6.7.14 770 774 picocolors: 1.0.0 ··· 785 789 '@types/prompts': ^2.4.1 786 790 '@types/sinonjs__fake-timers': ^8.1.2 787 791 '@vitest/ui': workspace:* 792 + acorn: ^8.8.0 793 + acorn-walk: ^8.2.0 788 794 birpc: ^0.2.3 789 795 cac: ^6.7.14 790 796 chai: ^4.3.6 ··· 794 800 diff: ^5.1.0 795 801 event-target-polyfill: ^0.0.3 796 802 execa: ^6.1.0 803 + expect-type: ^0.15.0 797 804 fast-glob: ^3.2.12 798 805 find-up: ^6.3.0 799 806 flatted: ^3.2.7 807 + get-tsconfig: ^4.2.0 800 808 happy-dom: ^7.6.6 801 809 jsdom: ^20.0.2 802 810 local-pkg: ^0.4.2 ··· 812 820 pretty-format: ^27.5.1 813 821 prompts: ^2.4.2 814 822 rollup: ^2.79.1 823 + source-map: ^0.6.1 815 824 strip-ansi: ^7.0.1 816 825 strip-literal: ^0.4.2 817 826 tinybench: ^2.3.1 ··· 825 834 '@types/chai': 4.3.3 826 835 '@types/chai-subset': 1.3.3 827 836 '@types/node': 18.7.13 837 + acorn: 8.8.0 838 + acorn-walk: 8.2.0 828 839 chai: 4.3.6 829 840 debug: 4.3.4 830 841 local-pkg: 0.4.2 842 + source-map: 0.6.1 831 843 strip-literal: 0.4.2 832 844 tinybench: 2.3.1 833 845 tinypool: 0.3.0 ··· 851 863 diff: 5.1.0 852 864 event-target-polyfill: 0.0.3 853 865 execa: 6.1.0 866 + expect-type: 0.15.0 854 867 fast-glob: 3.2.12 855 868 find-up: 6.3.0 856 869 flatted: 3.2.7 870 + get-tsconfig: 4.2.0 857 871 happy-dom: 7.6.6 858 872 jsdom: 20.0.2 859 873 log-update: 5.0.1 ··· 1106 1120 devDependencies: 1107 1121 execa: 6.1.0 1108 1122 vitest: link:../../packages/vitest 1123 + 1124 + test/typescript: 1125 + specifiers: 1126 + execa: ^6.1.0 1127 + typescript: ^4.8.4 1128 + vitest: workspace:* 1129 + vue-tsc: ^0.40.13 1130 + dependencies: 1131 + vitest: link:../../packages/vitest 1132 + devDependencies: 1133 + execa: 6.1.0 1134 + typescript: 4.8.4 1135 + vue-tsc: 0.40.13_typescript@4.8.4 1109 1136 1110 1137 test/vite-config: 1111 1138 specifiers: ··· 7203 7230 source-map: 0.6.1 7204 7231 dev: true 7205 7232 7233 + /@types/source-map/0.5.7: 7234 + resolution: {integrity: sha512-LrnsgZIfJaysFkv9rRJp4/uAyqw87oVed3s1hhF83nwbo9c7MG9g5DqR0seHP+lkX4ldmMrVolPjQSe2ZfD0yA==} 7235 + deprecated: This is a stub types definition for source-map (https://github.com/mozilla/source-map). source-map provides its own type definitions, so you don't need @types/source-map installed! 7236 + dependencies: 7237 + source-map: 0.7.4 7238 + dev: true 7239 + 7206 7240 /@types/stack-utils/2.0.1: 7207 7241 resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==} 7208 7242 dev: true ··· 7836 7870 vue: 2.7.10 7837 7871 dev: true 7838 7872 7873 + /@volar/code-gen/0.40.13: 7874 + resolution: {integrity: sha512-4gShBWuMce868OVvgyA1cU5WxHbjfEme18Tw6uVMfweZCF5fB2KECG0iPrA9D54vHk3FeHarODNwgIaaFfUBlA==} 7875 + dependencies: 7876 + '@volar/source-map': 0.40.13 7877 + dev: true 7878 + 7879 + /@volar/source-map/0.40.13: 7880 + resolution: {integrity: sha512-dbdkAB2Nxb0wLjAY5O64o3ywVWlAGONnBIoKAkXSf6qkGZM+nJxcizsoiI66K+RHQG0XqlyvjDizfnTxr+6PWg==} 7881 + dependencies: 7882 + '@vue/reactivity': 3.2.38 7883 + dev: true 7884 + 7885 + /@volar/typescript-faster/0.40.13: 7886 + resolution: {integrity: sha512-uy+TlcFkKoNlKEnxA4x5acxdxLyVDIXGSc8cYDNXpPKjBKXrQaetzCzlO3kVBqu1VLMxKNGJMTKn35mo+ILQmw==} 7887 + dependencies: 7888 + semver: 7.3.8 7889 + dev: true 7890 + 7891 + /@volar/vue-language-core/0.40.13: 7892 + resolution: {integrity: sha512-QkCb8msi2KUitTdM6Y4kAb7/ZlEvuLcbBFOC2PLBlFuoZwyxvSP7c/dBGmKGtJlEvMX0LdCyrg5V2aBYxD38/Q==} 7893 + dependencies: 7894 + '@volar/code-gen': 0.40.13 7895 + '@volar/source-map': 0.40.13 7896 + '@vue/compiler-core': 3.2.41 7897 + '@vue/compiler-dom': 3.2.41 7898 + '@vue/compiler-sfc': 3.2.41 7899 + '@vue/reactivity': 3.2.41 7900 + '@vue/shared': 3.2.41 7901 + dev: true 7902 + 7903 + /@volar/vue-typescript/0.40.13: 7904 + resolution: {integrity: sha512-o7bNztwjs8JmbQjVkrnbZUOfm7q4B8ZYssETISN1tRaBdun6cfNqgpkvDYd+VUBh1O4CdksvN+5BUNnwAz4oCQ==} 7905 + dependencies: 7906 + '@volar/code-gen': 0.40.13 7907 + '@volar/typescript-faster': 0.40.13 7908 + '@volar/vue-language-core': 0.40.13 7909 + dev: true 7910 + 7839 7911 /@vue/babel-helper-vue-transform-on/1.0.2: 7840 7912 resolution: {integrity: sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==} 7841 7913 dev: true ··· 7954 8026 estree-walker: 2.0.2 7955 8027 magic-string: 0.25.9 7956 8028 8029 + /@vue/reactivity/3.2.38: 8030 + resolution: {integrity: sha512-6L4myYcH9HG2M25co7/BSo0skKFHpAN8PhkNPM4xRVkyGl1K5M3Jx4rp5bsYhvYze2K4+l+pioN4e6ZwFLUVtw==} 8031 + dependencies: 8032 + '@vue/shared': 3.2.38 8033 + dev: true 8034 + 7957 8035 /@vue/reactivity/3.2.39: 7958 8036 resolution: {integrity: sha512-vlaYX2a3qMhIZfrw3Mtfd+BuU+TZmvDrPMa+6lpfzS9k/LnGxkSuf0fhkP0rMGfiOHPtyKoU9OJJJFGm92beVQ==} 7959 8037 dependencies: ··· 8007 8085 '@vue/compiler-ssr': 3.2.41 8008 8086 '@vue/shared': 3.2.41 8009 8087 vue: 3.2.41 8088 + 8089 + /@vue/shared/3.2.38: 8090 + resolution: {integrity: sha512-dTyhTIRmGXBjxJE+skC8tTWCGLCVc4wQgRRLt8+O9p5ewBAjoBwtCAkLPrtToSr1xltoe3st21Pv953aOZ7alg==} 8091 + dev: true 8010 8092 8011 8093 /@vue/shared/3.2.39: 8012 8094 resolution: {integrity: sha512-D3dl2ZB9qE6mTuWPk9RlhDeP1dgNRUKC3NJxji74A4yL8M2MwlhLKUC/49WHjrNzSPug58fWx/yFbaTzGAQSBw==} ··· 8556 8638 /acorn-walk/8.2.0: 8557 8639 resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} 8558 8640 engines: {node: '>=0.4.0'} 8559 - dev: true 8560 8641 8561 8642 /acorn/6.4.2: 8562 8643 resolution: {integrity: sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==} ··· 12347 12428 to-regex: 3.0.2 12348 12429 transitivePeerDependencies: 12349 12430 - supports-color 12431 + dev: true 12432 + 12433 + /expect-type/0.15.0: 12434 + resolution: {integrity: sha512-yWnriYB4e8G54M5/fAFj7rCIBiKs1HAACaY13kCz6Ku0dezjS9aMcfcdVK2X8Tv2tEV1BPz/wKfQ7WA4S/d8aA==} 12350 12435 dev: true 12351 12436 12352 12437 /expect/29.0.1: ··· 20740 20825 de-indent: 1.0.2 20741 20826 he: 1.2.0 20742 20827 vue: 2.7.10 20828 + dev: true 20829 + 20830 + /vue-tsc/0.40.13_typescript@4.8.4: 20831 + resolution: {integrity: sha512-xzuN3g5PnKfJcNrLv4+mAjteMd5wLm5fRhW0034OfNJZY4WhB07vhngea/XeGn7wNYt16r7syonzvW/54dcNiA==} 20832 + hasBin: true 20833 + peerDependencies: 20834 + typescript: '*' 20835 + dependencies: 20836 + '@volar/vue-language-core': 0.40.13 20837 + '@volar/vue-typescript': 0.40.13 20838 + typescript: 4.8.4 20743 20839 dev: true 20744 20840 20745 20841 /vue/2.7.10:
+2 -2
test/stacktraces/test/__snapshots__/runner.test.ts.snap
··· 1 1 // Vitest Snapshot v1 2 2 3 3 exports[`stacktraces should pick error frame if present > frame.spec.imba > frame.spec.imba 1`] = ` 4 - " ❯ frame.spec.imba (0 test) 5 - 4 + " FAIL frame.spec.imba [ frame.spec.imba ] 5 + imba-parser error: Unexpected 'CALL_END' 6 6 4 | test(\\"1+1\\") do 7 7 5 | expect(1+1).toBe 2 8 8 6 | frame.
+11 -15
test/stacktraces/test/runner.test.ts
··· 13 13 if (process.platform === 'win32' && process.env.CI) 14 14 return 15 15 16 - let error: any 17 - await execa('npx', ['vitest', 'run', file], { 16 + const { stderr } = await execa('npx', ['vitest', 'run', file], { 18 17 cwd: root, 18 + reject: false, 19 + stdio: 'pipe', 19 20 env: { 20 21 ...process.env, 21 22 CI: 'true', 22 23 NO_COLOR: 'true', 23 24 }, 24 25 }) 25 - .catch((e) => { 26 - error = e 27 - }) 28 26 29 - expect(error).toBeTruthy() 30 - const lines = String(error).split(/\n/g) 27 + expect(stderr).toBeTruthy() 28 + const lines = String(stderr).split(/\n/g) 31 29 const index = lines.findIndex(val => val.includes(`${file}:`)) 32 30 const msg = lines.slice(index, index + 8).join('\n') 33 31 expect(msg).toMatchSnapshot(file) ··· 45 43 if (process.platform === 'win32' && process.env.CI) 46 44 return 47 45 48 - let error: any 49 - await execa('npx', ['vitest', 'run', file], { 46 + const { stderr } = await execa('npx', ['vitest', 'run', file], { 50 47 cwd: root, 48 + reject: false, 49 + stdio: 'pipe', 51 50 env: { 52 51 ...process.env, 53 52 CI: 'true', 54 53 NO_COLOR: 'true', 55 54 }, 56 55 }) 57 - .catch((e) => { 58 - error = e 59 - }) 60 56 61 - expect(error).toBeTruthy() 62 - const lines = String(error).split(/\n/g) 63 - const index = lines.findIndex(val => val.includes('(0 test)')) 57 + expect(stderr).toBeTruthy() 58 + const lines = String(stderr).split(/\n/g) 59 + const index = lines.findIndex(val => val.includes('FAIL')) 64 60 const msg = lines.slice(index, index + 8).join('\n') 65 61 expect(msg).toMatchSnapshot(file) 66 62 }, 30000)
+6
test/typescript/failing/expect-error.test-d.ts
··· 1 + import { expectTypeOf, test } from 'vitest' 2 + 3 + test('failing test with expect-error', () => { 4 + // @ts-expect-error expect nothing 5 + expectTypeOf(1).toEqualTypeOf<number>() 6 + })
+16
test/typescript/failing/fail.test-d.ts
··· 1 + import { describe, expectTypeOf, test } from 'vitest' 2 + 3 + test('failing test', () => { 4 + expectTypeOf(1).toEqualTypeOf<string>() 5 + }) 6 + 7 + describe('nested suite', () => { 8 + describe('nested 2', () => { 9 + test('failing test 2', () => { 10 + expectTypeOf(1).toBeVoid() 11 + }) 12 + }) 13 + 14 + expectTypeOf(1).toBeVoid() 15 + }) 16 +
+7
test/typescript/failing/js-fail.test-d.js
··· 1 + // @ts-check 2 + 3 + import { expectTypeOf, test } from 'vitest' 4 + 5 + test('js test fails', () => { 6 + expectTypeOf(1).toBeArray() 7 + })
+5
test/typescript/failing/only.test-d.ts
··· 1 + import { expectTypeOf, test } from 'vitest' 2 + 3 + test.only('failing test', () => { 4 + expectTypeOf(1).toEqualTypeOf<string>() 5 + })
+15
test/typescript/package.json
··· 1 + { 2 + "scripts": { 3 + "test": "vitest", 4 + "types": "vitest typecheck --run", 5 + "tsc": "tsc --watch --pretty false --noEmit" 6 + }, 7 + "dependencies": { 8 + "vitest": "workspace:*" 9 + }, 10 + "devDependencies": { 11 + "execa": "^6.1.0", 12 + "typescript": "^4.8.4", 13 + "vue-tsc": "^0.40.13" 14 + } 15 + }
+7
test/typescript/test-d/js.test-d.js
··· 1 + // @ts-check 2 + 3 + import { expectTypeOf, test } from 'vitest' 4 + 5 + test('js test also works', () => { 6 + expectTypeOf(1).toEqualTypeOf(2) 7 + })
+38
test/typescript/test-d/test.test-d.ts
··· 1 + /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 + import { describe, expectTypeOf, test } from 'vitest' 3 + 4 + describe('test', () => { 5 + test('some-test', () => { 6 + expectTypeOf(Date).toBeConstructibleWith(new Date()) 7 + expectTypeOf(Date).toBeConstructibleWith('01-01-2000') 8 + 9 + type ResponsiveProp<T> = T | T[] | { xs?: T; sm?: T; md?: T } 10 + const getResponsiveProp = <T>(_props: T): ResponsiveProp<T> => ({}) 11 + interface CSSProperties { margin?: string; padding?: string } 12 + const cssProperties: CSSProperties = { margin: '1px', padding: '2px' } 13 + expectTypeOf(getResponsiveProp(cssProperties)) 14 + .exclude<unknown[] | { xs?: unknown }>() 15 + // .exclude<{ xs?: unknown }>() 16 + .toEqualTypeOf<CSSProperties>() 17 + }) 18 + 19 + describe('test2', () => { 20 + test('some-test 2', () => { 21 + expectTypeOf(Promise.resolve('string')).resolves.toEqualTypeOf<string>() 22 + expectTypeOf(45).toEqualTypeOf(45) 23 + }) 24 + }) 25 + 26 + test('ignored error', () => { 27 + // eslint-disable-next-line @typescript-eslint/prefer-ts-expect-error 28 + // @ts-ignore 29 + expectTypeOf(45).toEqualTypeOf<string>() 30 + }) 31 + 32 + test('expected error', () => { 33 + // @ts-expect-error 34 + expectTypeOf(45).toEqualTypeOf<string>() 35 + }) 36 + }) 37 + 38 + expectTypeOf({ wolk: 'true' }).toHaveProperty('wolk')
+54
test/typescript/test/__snapshots__/runner.test.ts.snap
··· 1 + // Vitest Snapshot v1 2 + 3 + exports[`should fails > typecheck files > expect-error.test-d.ts 1`] = ` 4 + " ❯ expect-error.test-d.ts:4:3 5 + 2| 6 + 3| test('failing test with expect-error', () => { 7 + 4| // @ts-expect-error expect nothing 8 + | ^ 9 + 5| expectTypeOf(1).toEqualTypeOf<number>() 10 + 6| }) 11 + " 12 + `; 13 + 14 + exports[`should fails > typecheck files > fail.test-d.ts 1`] = ` 15 + " ❯ fail.test-d.ts:4:19 16 + 2| 17 + 3| test('failing test', () => { 18 + 4| expectTypeOf(1).toEqualTypeOf<string>() 19 + | ^ 20 + 5| }) 21 + 6| 22 + " 23 + `; 24 + 25 + exports[`should fails > typecheck files > js-fail.test-d.js 1`] = ` 26 + " ❯ js-fail.test-d.js:6:19 27 + 4| 28 + 5| test('js test fails', () => { 29 + 6| expectTypeOf(1).toBeArray() 30 + | ^ 31 + 7| }) 32 + 8| 33 + " 34 + `; 35 + 36 + exports[`should fails > typecheck files > only.test-d.ts 1`] = ` 37 + " ❯ only.test-d.ts:4:19 38 + 2| 39 + 3| test.only('failing test', () => { 40 + 4| expectTypeOf(1).toEqualTypeOf<string>() 41 + | ^ 42 + 5| }) 43 + 6| 44 + " 45 + `; 46 + 47 + exports[`should fails > typecheck files 1`] = ` 48 + "TypeCheckError: Expected 1 arguments, but got 0. 49 + TypeCheckError: Expected 1 arguments, but got 0. 50 + TypeCheckError: Expected 1 arguments, but got 0. 51 + TypeCheckError: Expected 1 arguments, but got 0. 52 + TypeCheckError: Expected 1 arguments, but got 0. 53 + TypeCheckError: Unused '@ts-expect-error' directive." 54 + `;
+48
test/typescript/test/runner.test.ts
··· 1 + import { resolve } from 'pathe' 2 + import fg from 'fast-glob' 3 + import { execa } from 'execa' 4 + import { describe, expect, it } from 'vitest' 5 + 6 + describe('should fails', async () => { 7 + const root = resolve(__dirname, '../failing') 8 + const files = await fg('*.test-d.*', { cwd: root }) 9 + 10 + it('typecheck files', async () => { 11 + // in Windows child_process is very unstable, we skip testing it 12 + if (process.platform === 'win32' && process.env.CI) 13 + return 14 + 15 + const { stderr } = await execa('npx', [ 16 + 'vitest', 17 + 'typecheck', 18 + '--dir', 19 + 'failing', 20 + '--config', 21 + resolve(__dirname, './vitest.config.ts'), 22 + ], { 23 + cwd: root, 24 + reject: false, 25 + env: { 26 + ...process.env, 27 + CI: 'true', 28 + NO_COLOR: 'true', 29 + }, 30 + }) 31 + 32 + expect(stderr).toBeTruthy() 33 + const lines = String(stderr).split(/\n/g) 34 + const msg = lines 35 + .filter(i => i.includes('TypeCheckError: ')) 36 + .reverse() 37 + .join('\n') 38 + .trim() 39 + .replace(root, '<rootDir>') 40 + expect(msg).toMatchSnapshot() 41 + 42 + files.forEach((file) => { 43 + const index = lines.findIndex(val => val.includes(`${file}:`)) 44 + const msg = lines.slice(index, index + 8).join('\n') 45 + expect(msg).toMatchSnapshot(file) 46 + }) 47 + }, 30_000) 48 + })
+10
test/typescript/test/vitest.config.ts
··· 1 + import { defineConfig } from 'vitest/config' 2 + 3 + export default defineConfig({ 4 + test: { 5 + typecheck: { 6 + allowJs: true, 7 + include: ['**/*.test-d.*'], 8 + }, 9 + }, 10 + })
+2 -1
tsconfig.json
··· 40 40 "./packages/vitest/*.d.ts", 41 41 "./packages/ui/client/**", 42 42 "./examples/**/*.*", 43 - "./bench/**" 43 + "./bench/**", 44 + "./test/typescript/**" 44 45 ] 45 46 }