diff --git a/.gitignore b/.gitignore index a40e76edc2af..9f2984d59aad 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ test/**/__screenshots__/**/* test/browser/fixtures/update-snapshot/basic.test.ts test/cli/fixtures/browser-multiple/basic-* .vitest-reports +.attest diff --git a/packages/pretty-format/src/index.ts b/packages/pretty-format/src/index.ts index 710d1cd8b1f9..f22dfcd33854 100644 --- a/packages/pretty-format/src/index.ts +++ b/packages/pretty-format/src/index.ts @@ -332,6 +332,9 @@ function printPlugin( ) } catch (error: any) { + if (error instanceof PrettyFormatSkipSnapshotError) { + throw error + } throw new PrettyFormatPluginError(error.message, error.stack) } if (typeof printed !== 'string') { @@ -543,3 +546,5 @@ export const plugins: { ReactElement, ReactTestComponent, } + +export class PrettyFormatSkipSnapshotError extends Error {} diff --git a/packages/snapshot/src/index.ts b/packages/snapshot/src/index.ts index aaccc0bff63a..14fc782bbebe 100644 --- a/packages/snapshot/src/index.ts +++ b/packages/snapshot/src/index.ts @@ -1,7 +1,7 @@ export { SnapshotClient } from './client' export { stripSnapshotIndentation } from './port/inlineSnapshot' -export { addSerializer, getSerializers } from './port/plugins' +export { addSerializer, getSerializers, skipSnapshot } from './port/plugins' export { default as SnapshotState } from './port/state' export type { diff --git a/packages/snapshot/src/port/plugins.ts b/packages/snapshot/src/port/plugins.ts index a72fdb1f2d26..2cf0f61a3e65 100644 --- a/packages/snapshot/src/port/plugins.ts +++ b/packages/snapshot/src/port/plugins.ts @@ -9,7 +9,7 @@ import type { Plugin as PrettyFormatPlugin, Plugins as PrettyFormatPlugins, } from '@vitest/pretty-format' -import { plugins as prettyFormatPlugins } from '@vitest/pretty-format' +import { plugins as prettyFormatPlugins, PrettyFormatSkipSnapshotError } from '@vitest/pretty-format' import MockSerializer from './mockSerializer' @@ -39,3 +39,7 @@ export function addSerializer(plugin: PrettyFormatPlugin): void { export function getSerializers(): PrettyFormatPlugins { return PLUGINS } + +export function skipSnapshot(): never { + throw new PrettyFormatSkipSnapshotError() +} diff --git a/packages/snapshot/src/port/state.ts b/packages/snapshot/src/port/state.ts index d12f66746068..8df4818a96d2 100644 --- a/packages/snapshot/src/port/state.ts +++ b/packages/snapshot/src/port/state.ts @@ -17,10 +17,11 @@ import type { } from '../types' import type { InlineSnapshot } from './inlineSnapshot' import type { RawSnapshot, RawSnapshotInfo } from './rawSnapshot' +import { PrettyFormatSkipSnapshotError } from '@vitest/pretty-format' import { parseErrorStacktrace } from '../../../utils/src/source-map' + import { saveInlineSnapshots } from './inlineSnapshot' import { saveRawSnapshots } from './rawSnapshot' - import { addExtraLineBreaks, getSnapshotData, @@ -250,10 +251,19 @@ export default class SnapshotState { this._uncheckedKeys.delete(key) } - let receivedSerialized - = rawSnapshot && typeof received === 'string' - ? (received as string) - : serialize(received, undefined, this._snapshotFormat) + let receivedSerialized: string + try { + receivedSerialized + = rawSnapshot && typeof received === 'string' + ? (received as string) + : serialize(received, undefined, this._snapshotFormat) + } + catch (e) { + if (e instanceof PrettyFormatSkipSnapshotError) { + return { pass: true, actual: '', key, count } + } + throw e + } if (!rawSnapshot) { receivedSerialized = addExtraLineBreaks(receivedSerialized) diff --git a/packages/vitest/src/public/index.ts b/packages/vitest/src/public/index.ts index 4890d4f269e8..359ee1a2f634 100644 --- a/packages/vitest/src/public/index.ts +++ b/packages/vitest/src/public/index.ts @@ -321,6 +321,7 @@ export type { SnapshotUpdateState, UncheckedSnapshot, } from '@vitest/snapshot' +export { skipSnapshot } from '@vitest/snapshot' /** @deprecated import from `vitest/node` instead */ export type BrowserScript = BrowserScript_ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6c8bb9f19afd..b6a60d40687b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1040,6 +1040,12 @@ importers: specifier: workspace:* version: link:../runner + test/attest: + devDependencies: + '@ark/attest': + specifier: ^0.28.0 + version: 0.28.0(typescript@5.6.3) + test/benchmark: devDependencies: vitest: @@ -1581,6 +1587,21 @@ packages: peerDependencies: ajv: '>=8' + '@ark/attest@0.28.0': + resolution: {integrity: sha512-jEt8xfuMsL0L/nKuj4Jg0PswDrkOnyt4BjV76vJBQ+VEdQN3K1+18Yt5fzDGMY1RTZI6u6YAXJWBG60RHYhrxA==} + hasBin: true + peerDependencies: + typescript: '*' + + '@ark/fs@0.24.0': + resolution: {integrity: sha512-I9zI/qja1+K7RZW67CLjZRMrhZQmL9O4aFeIaoWH0BNkUn3baHDAHdM/2qcxgQojzmOkxx/ZsHBye+heq+ueLg==} + + '@ark/schema@0.24.0': + resolution: {integrity: sha512-L9aHo485uunP8WhQHH1ofd/DiwyAOo2WS3FMF8AebmIVMjqHOKCXnNBUV1rBnzVZXaVslPlTcgAKjCDmCGoHZg==} + + '@ark/util@0.24.0': + resolution: {integrity: sha512-YfXWkSinlhKdzoXAYxGVPvv2WMHzSN2Rrw5+Aoj79ksj8aPQdQagsMdnDTobaaynnlLxWz46hP/7JmVTo5QQ/A==} + '@babel/code-frame@7.24.2': resolution: {integrity: sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==} engines: {node: '>=6.9.0'} @@ -3132,6 +3153,11 @@ packages: '@polka/url@1.0.0-next.24': resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} + '@prettier/sync@0.5.2': + resolution: {integrity: sha512-Yb569su456XNx5BsH/Vyem7xD6g/y9iLmLUzRKM1a/dhU/D7HqqvkAG72znulXlMXztbV0iiu9O5AL8K98TzZQ==} + peerDependencies: + prettier: '*' + '@promptbook/utils@0.63.4': resolution: {integrity: sha512-ME3I9Twxu/d7hpnGTkNYMUyIY8IAwY5Mg86i4xpD1WSZKfYMTNQomvkyk2Fi33vZDu8NDwb6Quyd0zJ0T3xo9w==} @@ -3871,6 +3897,10 @@ packages: resolution: {integrity: sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typescript/analyze-trace@0.10.1': + resolution: {integrity: sha512-RnlSOPh14QbopGCApgkSx5UBgGda5MX1cHqp2fsqfiDyCwGL/m1jaeB9fzu7didVS81LQqGZZuxFBcg8YU8EVw==} + hasBin: true + '@typescript/vfs@1.6.0': resolution: {integrity: sha512-hvJUjNVeBMp77qPINuUvYXj4FyWeeMMKZkxEATEU3hqBAQ7qdTBCUFT7Sp0Zu0faeEtFf+ldXxMEDr/bk73ISg==} peerDependencies: @@ -4431,6 +4461,9 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + arktype@2.0.0-rc.24: + resolution: {integrity: sha512-uZB2XXDMzkM613MqhDMjB/y4RPSqS55W9Xn+GoVLgdat/Dr6MC/E90YqFsDCon9zusl3NB/A59MzXeR6PcTo4A==} + array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} @@ -4829,6 +4862,9 @@ packages: resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} engines: {node: '>= 12'} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -5736,6 +5772,10 @@ packages: resolution: {integrity: sha512-yKHlle2YGxZE842MERVIplWwNH5VYmqqcPFgtnlU//K8gxuFFXu0pwd/CrfXTumFpeEiufsP7+opT/bPJa1yVw==} engines: {node: ^18.19.0 || >=20.5.0} + exit@0.1.2: + resolution: {integrity: sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==} + engines: {node: '>= 0.8.0'} + expand-template@2.0.3: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} @@ -6793,10 +6833,19 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonparse@1.3.1: + resolution: {integrity: sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==} + engines: {'0': node >= 0.2.0} + jsonpointer@5.0.1: resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} engines: {node: '>=0.10.0'} + jsonstream-next@3.0.0: + resolution: {integrity: sha512-aAi6oPhdt7BKyQn1SrIIGZBt0ukKuOUE1qV6kJ3GgioSOYzsRc8z9Hfr1BVmacA/jLe9nARfmgMGgn68BqIAgg==} + engines: {node: '>=10'} + hasBin: true + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -6976,6 +7025,9 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + make-synchronized@0.2.9: + resolution: {integrity: sha512-4wczOs8SLuEdpEvp3vGo83wh8rjJ78UsIk7DIX5fxdfmfMJGog4bQzxfvOwq7Q3yCHLC4jp1urPHIxRS/A93gA==} + mark.js@8.11.1: resolution: {integrity: sha512-1I+1qpDt4idfgLQG+BNWmrqku+7/2bi5nLf4YwF8y8zXvmfiTBY3PV3ZibfrjBueCByROpuBjLLFCajqkgYoLQ==} @@ -7735,6 +7787,11 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + pretty-bytes@5.6.0: resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} engines: {node: '>=6'} @@ -8407,6 +8464,9 @@ packages: resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} engines: {node: '>=0.10.0'} + split2@3.2.2: + resolution: {integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==} + split2@4.2.0: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} @@ -8682,6 +8742,9 @@ packages: thread-stream@2.1.0: resolution: {integrity: sha512-5+Pf2Ya31CsZyIPYYkhINzdTZ3guL+jHq7D8lkBybgGcSQIKDbid3NJku3SpCKeE/gACWAccDA/rH2B6doC5aA==} + through2@4.0.2: + resolution: {integrity: sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw==} + through@2.3.8: resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} @@ -8793,6 +8856,10 @@ packages: traverse@0.3.9: resolution: {integrity: sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==} + treeify@1.1.0: + resolution: {integrity: sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A==} + engines: {node: '>=0.6'} + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -9614,10 +9681,18 @@ packages: engines: {node: '>= 14'} hasBin: true + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.1: resolution: {integrity: sha512-cwiTb08Xuv5fqF4AovYacTFNxk62th7LKJ6BL9IGUpTJrWoU7/7WdQGTP2SjKf1dUNBGzDd28p/Yfs/GI6JrLw==} engines: {node: '>=12'} @@ -9846,6 +9921,27 @@ snapshots: jsonpointer: 5.0.1 leven: 3.1.0 + '@ark/attest@0.28.0(typescript@5.6.3)': + dependencies: + '@ark/fs': 0.24.0 + '@ark/util': 0.24.0 + '@prettier/sync': 0.5.2(prettier@3.3.3) + '@typescript/analyze-trace': 0.10.1 + '@typescript/vfs': 1.6.0(typescript@5.6.3) + arktype: 2.0.0-rc.24 + prettier: 3.3.3 + typescript: 5.6.3 + transitivePeerDependencies: + - supports-color + + '@ark/fs@0.24.0': {} + + '@ark/schema@0.24.0': + dependencies: + '@ark/util': 0.24.0 + + '@ark/util@0.24.0': {} + '@babel/code-frame@7.24.2': dependencies: '@babel/highlight': 7.24.7 @@ -11424,6 +11520,11 @@ snapshots: '@polka/url@1.0.0-next.24': {} + '@prettier/sync@0.5.2(prettier@3.3.3)': + dependencies: + make-synchronized: 0.2.9 + prettier: 3.3.3 + '@promptbook/utils@0.63.4': dependencies: spacetrim: 0.11.39 @@ -12276,6 +12377,17 @@ snapshots: '@typescript-eslint/types': 8.8.1 eslint-visitor-keys: 3.4.3 + '@typescript/analyze-trace@0.10.1': + dependencies: + chalk: 4.1.2 + exit: 0.1.2 + jsonparse: 1.3.1 + jsonstream-next: 3.0.0 + p-limit: 3.1.0 + split2: 3.2.2 + treeify: 1.1.0 + yargs: 16.2.0 + '@typescript/vfs@1.6.0(typescript@5.6.3)': dependencies: debug: 4.3.7 @@ -13019,6 +13131,11 @@ snapshots: dependencies: dequal: 2.0.3 + arktype@2.0.0-rc.24: + dependencies: + '@ark/schema': 0.24.0 + '@ark/util': 0.24.0 + array-buffer-byte-length@1.0.0: dependencies: call-bind: 1.0.5 @@ -13501,6 +13618,12 @@ snapshots: cli-width@4.1.0: {} + cliui@7.0.4: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + cliui@8.0.1: dependencies: string-width: 4.2.3 @@ -14632,6 +14755,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.1 + exit@0.1.2: {} + expand-template@2.0.3: {} expect-type@1.1.0: {} @@ -15869,8 +15994,15 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonparse@1.3.1: {} + jsonpointer@5.0.1: {} + jsonstream-next@3.0.0: + dependencies: + jsonparse: 1.3.1 + through2: 4.0.2 + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -16060,6 +16192,8 @@ snapshots: dependencies: semver: 7.6.3 + make-synchronized@0.2.9: {} + mark.js@8.11.1: {} markdown-table@3.0.3: {} @@ -16969,6 +17103,8 @@ snapshots: prelude-ls@1.2.1: {} + prettier@3.3.3: {} + pretty-bytes@5.6.0: {} pretty-bytes@6.1.1: {} @@ -17775,6 +17911,10 @@ snapshots: speakingurl@14.0.1: {} + split2@3.2.2: + dependencies: + readable-stream: 3.6.0 + split2@4.2.0: {} splitpanes@3.1.5: {} @@ -18090,6 +18230,10 @@ snapshots: dependencies: real-require: 0.2.0 + through2@4.0.2: + dependencies: + readable-stream: 3.6.0 + through@2.3.8: {} tiny-glob@0.2.9: @@ -18182,6 +18326,8 @@ snapshots: traverse@0.3.9: {} + treeify@1.1.0: {} + trim-lines@3.0.1: {} ts-api-utils@1.3.0(typescript@5.6.3): @@ -19219,8 +19365,20 @@ snapshots: yaml@2.4.5: {} + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.1: dependencies: cliui: 8.0.1 diff --git a/test/attest/fixtures/setup-attest-analyze.ts b/test/attest/fixtures/setup-attest-analyze.ts new file mode 100644 index 000000000000..28f8a232011c --- /dev/null +++ b/test/attest/fixtures/setup-attest-analyze.ts @@ -0,0 +1,28 @@ +import type { GlobalSetupContext } from 'vitest/node' +import { execFileSync } from 'node:child_process' +import { mkdirSync } from 'node:fs' +import { getConfig } from '@ark/attest/internal/config.js' + +const config = getConfig() + +// TODO(attest): for now we use cli since running `setup` repeatedly doesn't work +// (probably ts server fs in memory is stale and we can invalidate on re-run via vfs.updateFile?) +// import { setup } from "@ark/attest"; + +async function setup() { + if (config.skipTypes) { + return + } + mkdirSync('.attest/assertions', { recursive: true }) + execFileSync('node', ['../node_modules/@ark/attest/out/cli/cli.js', 'precache', '.attest/assertions/typescript.json'], { + stdio: 'inherit', + }) +} + +export default async (ctx: GlobalSetupContext) => { + await setup() + + ctx.onTestsRerun(async () => { + await setup(); + }); +} diff --git a/test/attest/fixtures/setup-attest-snapshot.ts b/test/attest/fixtures/setup-attest-snapshot.ts new file mode 100644 index 000000000000..dc9c29815a00 --- /dev/null +++ b/test/attest/fixtures/setup-attest-snapshot.ts @@ -0,0 +1,56 @@ +import { ChainableAssertions } from '@ark/attest/internal/assert/chainableAssertions.js' +import { getConfig } from '@ark/attest/internal/config.js' +import { chainableNoOpProxy } from '@ark/attest/internal/utils.js' +import { expect, skipSnapshot } from 'vitest' + +const attestConfig = getConfig() + +// for `attest().type.toString` and `attest().type.errors` +expect.addSnapshotSerializer({ + test(val: unknown) { + // TODO(attest) more robust way to target attest object? + return ( + !!val + && (typeof val === 'object' || typeof val === 'function') + && typeof (val as any).unwrap === 'function' + ) + }, + serialize(val, config, indentation, depth, refs, printer) { + if (attestConfig.skipTypes) { + skipSnapshot() + } + const serialized = val.unwrap(); + if (typeof serialized === 'string') { + return serialized; + } + return printer( + serialized, + config, + indentation, + depth, + refs, + ) + }, +}) + +// make `attest(xxx)` to work like `attest().type.toString` +expect.addSnapshotSerializer({ + test(val: unknown) { + return val instanceof ChainableAssertions + }, + serialize(val, _config, _indentation, _depth, _refs, _printer) { + if (attestConfig.skipTypes) { + skipSnapshot() + } + return val.type.toString.serializedActual + }, +}) + +expect.addSnapshotSerializer({ + test(val: unknown) { + return val === chainableNoOpProxy + }, + serialize(_val, _config, _indentation, _depth, _refs, _printer) { + skipSnapshot() + }, +}) diff --git a/test/attest/fixtures/test/__snapshots__/snapshot.test.ts.snap b/test/attest/fixtures/test/__snapshots__/snapshot.test.ts.snap new file mode 100644 index 000000000000..6f038a618e6a --- /dev/null +++ b/test/attest/fixtures/test/__snapshots__/snapshot.test.ts.snap @@ -0,0 +1,9 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`file 1`] = `number`; + +exports[`file 2`] = `(fractionDigits?: number | undefined) => string`; + +exports[`mixed 2`] = `3`; + +exports[`mixed 4`] = `number`; diff --git a/test/attest/fixtures/test/assertion.test.ts b/test/attest/fixtures/test/assertion.test.ts new file mode 100644 index 000000000000..8fbdee5f1381 --- /dev/null +++ b/test/attest/fixtures/test/assertion.test.ts @@ -0,0 +1,35 @@ +import { attest } from "@ark/attest" +import { test, expect, expectTypeOf } from "vitest" + +test('compare with expect-type', () => { + const v = { hello: "world" } + + // + // type equality: value and type + // + expectTypeOf(v).toEqualTypeOf<{ hello: string }>() + + attest<{ hello: string }>(v); + attest(); + attest(v).type.toString("{ hello: string }"); + attest(v).type.toString.is("{ hello: string }"); + attest(v).type.toString.equals('{ hello: string }'); // what's the difference? + expect(attest(v)).toMatchInlineSnapshot(`{ hello: string }`); + expect(attest(v).type.toString).toMatchInlineSnapshot(`{ hello: string }`); + + + // + // type equality: value to value + // + expectTypeOf(v).toEqualTypeOf({ hello: "other-string" }) + // attest(v).type.toString(attest({ hello: "other-string" })) // how? + + + // + // assignability + // + expectTypeOf({ x: 0, y: 1 }).toMatchTypeOf<{ x: number }>() + expectTypeOf({ x: 0, y: 1 }).toMatchTypeOf({ x: 2 }) + + attest({ x: 0, y: 1 }).satisfies({ x: 'number' }) // is this arktype syntax? +}) diff --git a/test/attest/fixtures/test/snapshot.test.ts b/test/attest/fixtures/test/snapshot.test.ts new file mode 100644 index 000000000000..fcc9a0911fcb --- /dev/null +++ b/test/attest/fixtures/test/snapshot.test.ts @@ -0,0 +1,56 @@ +import { attest } from '@ark/attest' +import { expect, test } from 'vitest' + +test('inline', () => { + expect(attest(1 + 2)).toMatchInlineSnapshot(`number`) + expect(attest((1 + 2).toFixed)).toMatchInlineSnapshot( + `(fractionDigits?: number | undefined) => string`, + ) +}) + +test('file', () => { + expect(attest(1 + 2)).toMatchSnapshot() + expect(attest((1 + 2).toFixed)).toMatchSnapshot() +}) + +test('mixed', () => { + expect(1 + 2).toMatchInlineSnapshot(`3`) + expect(1 + 2).toMatchSnapshot() + expect(attest(1 + 2)).toMatchInlineSnapshot(`number`) + expect(attest(1 + 2)).toMatchSnapshot() +}) + +test('errors', () => { + expect(attest(1 / 2).type.toString).toMatchInlineSnapshot(`number`) + expect( + attest( + () => + // @ts-expect-error test errors + '1' / 2, + ).type.errors, + ).toMatchInlineSnapshot( + `The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.`, + ) + + // make it work like `.type.toString` by default + expect(attest(1 / 2)).toMatchInlineSnapshot(`number`) +}) + +test('completions', () => { + expect(attest( + () => + // @ts-expect-error test completions + // eslint-disable-next-line dot-notation + (1 + 2)['to'], + ).completions).toMatchInlineSnapshot(` + { + "to": [ + "toExponential", + "toFixed", + "toLocaleString", + "toPrecision", + "toString", + ], + } + `) +}) diff --git a/test/attest/fixtures/tsconfig.json b/test/attest/fixtures/tsconfig.json new file mode 100644 index 000000000000..030c885228b5 --- /dev/null +++ b/test/attest/fixtures/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "verbatimModuleSyntax": true + }, + "include": ["src", "test", "*.ts"] +} diff --git a/test/attest/fixtures/vite.config.ts b/test/attest/fixtures/vite.config.ts new file mode 100644 index 000000000000..786394dcf6d6 --- /dev/null +++ b/test/attest/fixtures/vite.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + setupFiles: ['./setup-attest-snapshot.ts'], + globalSetup: ['./setup-attest-analyze.ts'], + }, +}) diff --git a/test/attest/package.json b/test/attest/package.json new file mode 100644 index 000000000000..c1ea54161869 --- /dev/null +++ b/test/attest/package.json @@ -0,0 +1,13 @@ +{ + "name": "@vitest/test-attest", + "type": "module", + "private": true, + "scripts": { + "test": "vitest", + "test-fixtures": "cd fixtures && vitest", + "test-fixtures-skip-types": "cd fixtures && ATTEST_skipTypes=1 vitest" + }, + "devDependencies": { + "@ark/attest": "^0.28.0" + } +} diff --git a/test/attest/test/basic.test.ts b/test/attest/test/basic.test.ts new file mode 100644 index 000000000000..32e9cf876af1 --- /dev/null +++ b/test/attest/test/basic.test.ts @@ -0,0 +1,38 @@ +import { join } from 'node:path' +import { expect, it } from 'vitest' +import { editFile, runVitestCli } from '../../test-utils' + +it('skip types', { timeout: 20_000 }, async () => { + // for now, use cli since cwd is essential for attest + const dir = join(import.meta.dirname, '../fixtures') + + // normal run with correct snapshots + let result = await runVitestCli({ nodeOptions: { cwd: dir } }, 'run') + expect(result.stderr).toBe('') + expect(result.stdout).toContain('Waiting for TypeScript') + expect(result.stdout).toContain('Test Files 2 passed') + + // skipTypes run with wrong snapshots + editFile( + join(dir, 'test/__snapshots__/snapshot.test.ts.snap'), + s => s.replace('exports[`file 1`] = `number`', ''), + ) + result = await runVitestCli({ + nodeOptions: { + cwd: dir, + env: { ...process.env, ATTEST_skipTypes: '1' }, + }, + }, 'run', '--update') + expect(result.stderr).toBe('') + expect(result.stdout).not.toContain('Waiting for TypeScript') + expect(result.stdout).not.toContain('Snapshots 1 written') + expect(result.stdout).not.toContain('obsolete') + expect(result.stdout).toContain('Test Files 2 passed') + + // update snapshot + result = await runVitestCli({ nodeOptions: { cwd: dir } }, 'run', '--update') + expect(result.stderr).toBe('') + expect(result.stdout).toContain('Waiting for TypeScript') + expect(result.stdout).toContain('Snapshots 1 written') + expect(result.stdout).toContain('Test Files 2 passed') +}) diff --git a/test/attest/vite.config.ts b/test/attest/vite.config.ts new file mode 100644 index 000000000000..617561ce45f2 --- /dev/null +++ b/test/attest/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + exclude: ['**/fixtures/**'], + }, +})