2024-01-01 21:11:32 +00:00
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
2022-08-11 11:51:20 +00:00
/ * * A s n a p s h o t t i n g l i b r a r y .
2022-11-25 11:40:23 +00:00
*
* The ` assertSnapshot ` function will create a snapshot of a value and compare it
* to a reference snapshot , which is stored alongside the test file in the
* ` __snapshots__ ` directory .
*
* ` ` ` ts
* // example_test.ts
2024-04-29 02:57:30 +00:00
* import { assertSnapshot } from "@std/testing/snapshot" ;
2022-11-25 11:40:23 +00:00
*
* Deno . test ( "isSnapshotMatch" , async function ( t ) : Promise < void > {
* const a = {
* hello : "world!" ,
* example : 123 ,
* } ;
* await assertSnapshot ( t , a ) ;
* } ) ;
* ` ` `
*
2024-09-19 23:29:31 +00:00
* ` ` ` ts no-assert
2022-11-25 11:40:23 +00:00
* // __snapshots__/example_test.ts.snap
2024-09-19 23:29:31 +00:00
* export const snapshot : Record < string , string > = { } ;
2022-11-25 11:40:23 +00:00
*
2024-09-19 23:29:31 +00:00
* snapshot [ "isSnapshotMatch 1" ] = `
2022-11-25 11:40:23 +00:00
* {
* example : 123 ,
* hello : "world!" ,
* }
* ` ;
* ` ` `
*
* Calling ` assertSnapshot ` in a test will throw an ` AssertionError ` , causing the
* test to fail , if the snapshot created during the test does not match the one in
* the snapshot file .
*
* # # Updating Snapshots :
*
* When adding new snapshot assertions to your test suite , or when intentionally
* making changes which cause your snapshots to fail , you can update your snapshots
* by running the snapshot tests in update mode . Tests can be run in update mode by
* passing the ` --update ` or ` -u ` flag as an argument when running the test . When
* this flag is passed , then any snapshots which do not match will be updated .
*
* ` ` ` sh
* deno test -- allow - all -- -- update
* ` ` `
*
* Additionally , new snapshots will only be created when this flag is present .
*
* # # Permissions :
*
* When running snapshot tests , the ` --allow-read ` permission must be enabled , or
* else any calls to ` assertSnapshot ` will fail due to insufficient permissions .
* Additionally , when updating snapshots , the ` --allow-write ` permission must also
* be enabled , as this is required in order to update snapshot files .
*
* The ` assertSnapshot ` function will only attempt to read from and write to
* snapshot files . As such , the allow list for ` --allow-read ` and ` --allow-write `
* can be limited to only include existing snapshot files , if so desired .
*
* # # Options :
*
* The ` assertSnapshot ` function optionally accepts an options object .
*
* ` ` ` ts
* // example_test.ts
2024-04-29 02:57:30 +00:00
* import { assertSnapshot } from "@std/testing/snapshot" ;
2022-11-25 11:40:23 +00:00
*
* Deno . test ( "isSnapshotMatch" , async function ( t ) : Promise < void > {
* const a = {
* hello : "world!" ,
* example : 123 ,
* } ;
* await assertSnapshot ( t , a , {
* // options
* } ) ;
* } ) ;
* ` ` `
*
* You can also configure default options for ` assertSnapshot ` .
*
* ` ` ` ts
* // example_test.ts
2024-04-29 02:57:30 +00:00
* import { createAssertSnapshot } from "@std/testing/snapshot" ;
2022-11-25 11:40:23 +00:00
*
* const assertSnapshot = createAssertSnapshot ( {
* // options
* } ) ;
* ` ` `
*
* When configuring default options like this , the resulting ` assertSnapshot `
* function will function the same as the default function exported from the
* snapshot module . If passed an optional options object , this will take precedence
2023-06-21 16:27:37 +00:00
* over the default options , where the value provided for an option differs .
2022-11-25 11:40:23 +00:00
*
* It is possible to "extend" an ` assertSnapshot ` function which has been
* configured with default options .
*
* ` ` ` ts
* // example_test.ts
2024-04-29 02:57:30 +00:00
* import { createAssertSnapshot } from "@std/testing/snapshot" ;
* import { stripAnsiCode } from "@std/fmt/colors" ;
2022-11-25 11:40:23 +00:00
*
* const assertSnapshot = createAssertSnapshot ( {
* dir : ".snaps" ,
* } ) ;
*
* const assertMonochromeSnapshot = createAssertSnapshot < string > (
2023-12-06 06:58:42 +00:00
* { serializer : stripAnsiCode } ,
2022-11-25 11:40:23 +00:00
* assertSnapshot ,
* ) ;
*
* Deno . test ( "isSnapshotMatch" , async function ( t ) : Promise < void > {
2024-09-16 22:48:56 +00:00
* const a = "\x1b[32mThis green text has had its colors stripped\x1b[39m" ;
2022-11-25 11:40:23 +00:00
* await assertMonochromeSnapshot ( t , a ) ;
* } ) ;
* ` ` `
*
2024-09-19 23:29:31 +00:00
* ` ` ` ts no-assert
2022-11-25 11:40:23 +00:00
* // .snaps/example_test.ts.snap
2024-09-19 23:29:31 +00:00
* export const snapshot : Record < string , string > = { } ;
2022-11-25 11:40:23 +00:00
*
2024-09-19 23:29:31 +00:00
* snapshot [ "isSnapshotMatch 1" ] = "This green text has had its colors stripped" ;
2022-11-25 11:40:23 +00:00
* ` ` `
*
* # # Version Control :
*
2023-06-21 16:27:37 +00:00
* Snapshot testing works best when changes to snapshot files are committed
2022-11-25 11:40:23 +00:00
* alongside other code changes . This allows for changes to reference snapshots to
* be reviewed along side the code changes that caused them , and ensures that when
* others pull your changes , their tests will pass without needing to update
* snapshots locally .
2022-08-11 11:51:20 +00:00
*
* @module
* /
2024-04-29 02:57:30 +00:00
import { fromFileUrl } from "@std/path/from-file-url" ;
import { parse } from "@std/path/parse" ;
import { resolve } from "@std/path/resolve" ;
import { toFileUrl } from "@std/path/to-file-url" ;
import { ensureFile , ensureFileSync } from "@std/fs/ensure-file" ;
2024-07-02 04:07:46 +00:00
import { assert } from "@std/assert/assert" ;
import { AssertionError } from "@std/assert/assertion-error" ;
import { equal } from "@std/assert/equal" ;
2024-06-02 10:28:31 +00:00
import { diff } from "@std/internal/diff" ;
import { diffStr } from "@std/internal/diff-str" ;
import { buildMessage } from "@std/internal/build-message" ;
2022-04-20 11:46:21 +00:00
const SNAPSHOT_DIR = "__snapshots__" ;
2022-05-06 13:05:27 +00:00
const SNAPSHOT_EXT = "snap" ;
2022-04-20 11:46:21 +00:00
2024-06-17 02:31:31 +00:00
/** The mode of snapshot testing. */
2022-05-06 13:05:27 +00:00
export type SnapshotMode = "assert" | "update" ;
2024-06-17 02:31:31 +00:00
/** The options for {@linkcode assertSnapshot}. */
2022-05-06 13:05:27 +00:00
export type SnapshotOptions < T = unknown > = {
/ * *
* Snapshot output directory . Snapshot files will be written to this directory .
* This can be relative to the test directory or an absolute path .
*
* If both ` dir ` and ` path ` are specified , the ` dir ` option will be ignored and
* the ` path ` option will be handled as normal .
* /
dir? : string ;
/ * *
* Snapshot mode . Defaults to ` assert ` , unless the ` -u ` or ` --update ` flag is
* passed , in which case this will be set to ` update ` . This option takes higher
* priority than the update flag . If the ` --update ` flag is passed , it will be
* ignored if the ` mode ` option is set .
* /
mode? : SnapshotMode ;
/ * *
* Failure message to log when the assertion fails . Specifying this option will
* cause the diff not to be logged .
* /
msg? : string ;
/ * *
* Name of the snapshot to use in the snapshot file .
* /
name? : string ;
/ * *
2023-06-21 16:27:37 +00:00
* Snapshot output path . The snapshot will be written to this file . This can be
2022-05-06 13:05:27 +00:00
* a path relative to the test directory or an absolute path .
*
* If both ` dir ` and ` path ` are specified , the ` dir ` option will be ignored and
* the ` path ` option will be handled as normal .
* /
path? : string ;
/ * *
2024-07-31 07:45:22 +00:00
* Function to use when serializing the snapshot . The default is { @linkcode serialize } .
2022-05-06 13:05:27 +00:00
* /
serializer ? : ( actual : T ) = > string ;
2022-04-20 11:46:21 +00:00
} ;
2022-05-06 13:05:27 +00:00
function getErrorMessage ( message : string , options : SnapshotOptions ) {
return typeof options . msg === "string" ? options.msg : message ;
}
2022-04-20 11:46:21 +00:00
2022-05-06 13:05:27 +00:00
/ * *
* Default serializer for ` assertSnapshot ` .
2024-06-17 02:31:31 +00:00
*
* @example Usage
* ` ` ` ts
* import { serialize } from "@std/testing/snapshot" ;
refactor(assert,async,bytes,cli,collections,crypto,csv,data-structures,datetime,dotenv,encoding,expect,fmt,front-matter,fs,html,http,ini,internal,io,json,jsonc,log,media-types,msgpack,net,path,semver,streams,testing,text,toml,ulid,url,uuid,webgpu,yaml): import from `@std/assert` (#5199)
* refactor: import from `@std/assert`
* update
2024-06-30 08:30:10 +00:00
* import { assertEquals } from "@std/assert" ;
2024-06-17 02:31:31 +00:00
*
* assertEquals ( serialize ( { foo : 42 } ) , "{\n foo: 42,\n}" )
* ` ` `
*
* @param actual The value to serialize
* @returns The serialized string
2022-05-06 13:05:27 +00:00
* /
export function serialize ( actual : unknown ) : string {
2022-04-26 03:54:45 +00:00
return Deno . inspect ( actual , {
depth : Infinity ,
sorted : true ,
trailingComma : true ,
compact : false ,
iterableLimit : Infinity ,
strAbbreviateSize : Infinity ,
2023-07-29 09:30:50 +00:00
breakLength : Infinity ,
2023-07-29 09:24:09 +00:00
escapeSequences : false ,
2024-07-08 10:33:33 +00:00
} ) . replaceAll ( "\r" , "\\r" ) ;
2022-04-26 03:54:45 +00:00
}
2022-04-20 11:46:21 +00:00
/ * *
2022-05-06 13:05:27 +00:00
* Converts a string to a valid JavaScript string which can be wrapped in backticks .
*
* @example
2022-04-20 11:46:21 +00:00
*
2022-05-06 13:05:27 +00:00
* "special characters (\ ` $) will be escaped" - > "special characters (\\ \` \$) will be escaped"
* /
function escapeStringForJs ( str : string ) {
return str
. replace ( /\\/g , "\\\\" )
. replace ( /`/g , "\\`" )
. replace ( /\$/g , "\\$" ) ;
}
let _mode : SnapshotMode ;
/ * *
* Get the snapshot mode .
2022-04-20 11:46:21 +00:00
* /
2022-05-06 13:05:27 +00:00
function getMode ( options : SnapshotOptions ) {
if ( options . mode ) {
return options . mode ;
} else if ( _mode ) {
return _mode ;
} else {
_mode = Deno . args . some ( ( arg ) = > arg === "--update" || arg === "-u" )
? "update"
: "assert" ;
return _mode ;
2022-04-20 11:46:21 +00:00
}
}
/ * *
2022-05-06 13:05:27 +00:00
* Return ` true ` when snapshot mode is ` update ` .
2022-04-20 11:46:21 +00:00
* /
2022-05-06 13:05:27 +00:00
function getIsUpdate ( options : SnapshotOptions ) {
return getMode ( options ) === "update" ;
}
class AssertSnapshotContext {
static contexts = new Map < string , AssertSnapshotContext > ( ) ;
/ * *
* Returns an instance of ` AssertSnapshotContext ` . This will be retrieved from
* a cache if an instance was already created for a given snapshot file path .
* /
static fromOptions (
testContext : Deno.TestContext ,
options : SnapshotOptions ,
) : AssertSnapshotContext {
let path : string ;
const testFilePath = fromFileUrl ( testContext . origin ) ;
const { dir , base } = parse ( testFilePath ) ;
if ( options . path ) {
path = resolve ( dir , options . path ) ;
} else if ( options . dir ) {
path = resolve ( dir , options . dir , ` ${ base } . ${ SNAPSHOT_EXT } ` ) ;
} else {
path = resolve ( dir , SNAPSHOT_DIR , ` ${ base } . ${ SNAPSHOT_EXT } ` ) ;
}
let context = this . contexts . get ( path ) ;
if ( context ) {
return context ;
}
context = new this ( toFileUrl ( path ) ) ;
this . contexts . set ( path , context ) ;
return context ;
}
2022-05-27 12:27:13 +00:00
# teardownRegistered = false ;
# currentSnapshots : Map < string , string | undefined > | undefined ;
# updatedSnapshots = new Map < string , string > ( ) ;
# snapshotCounts = new Map < string , number > ( ) ;
# snapshotsUpdated = new Array < string > ( ) ;
# snapshotFileUrl : URL ;
snapshotUpdateQueue = new Array < string > ( ) ;
2022-05-06 13:05:27 +00:00
constructor ( snapshotFileUrl : URL ) {
2022-05-27 12:27:13 +00:00
this . # snapshotFileUrl = snapshotFileUrl ;
2022-05-06 13:05:27 +00:00
}
/ * *
* Asserts that ` this.#currentSnapshots ` has been initialized and then returns it .
*
* Should only be called when ` this.#currentSnapshots ` has already been initialized .
* /
2022-05-27 12:27:13 +00:00
# getCurrentSnapshotsInitialized() {
2022-05-06 13:05:27 +00:00
assert (
2022-05-27 12:27:13 +00:00
this . # currentSnapshots ,
2022-05-06 13:05:27 +00:00
"Snapshot was not initialized. This is a bug in `assertSnapshot`." ,
) ;
2022-05-27 12:27:13 +00:00
return this . # currentSnapshots ;
2022-05-06 13:05:27 +00:00
}
/ * *
* Write updates to the snapshot file and log statistics .
* /
2022-05-27 12:27:13 +00:00
# teardown = ( ) = > {
2022-05-06 13:05:27 +00:00
const buf = [ "export const snapshot = {};" ] ;
2022-05-27 12:27:13 +00:00
const currentSnapshots = this . # getCurrentSnapshotsInitialized ( ) ;
2023-06-10 10:02:09 +00:00
const currentSnapshotNames = Array . from ( currentSnapshots . keys ( ) ) ;
const removedSnapshotNames = currentSnapshotNames . filter ( ( name ) = >
! this . snapshotUpdateQueue . includes ( name )
) ;
2022-05-06 13:05:27 +00:00
this . snapshotUpdateQueue . forEach ( ( name ) = > {
2022-05-27 12:27:13 +00:00
const updatedSnapshot = this . # updatedSnapshots . get ( name ) ;
2022-05-06 13:05:27 +00:00
const currentSnapshot = currentSnapshots . get ( name ) ;
let formattedSnapshot : string ;
if ( typeof updatedSnapshot === "string" ) {
formattedSnapshot = updatedSnapshot ;
} else if ( typeof currentSnapshot === "string" ) {
formattedSnapshot = currentSnapshot ;
} else {
// This occurs when `assertSnapshot` is called in "assert" mode but
// the snapshot doesn't exist and `assertSnapshot` is also called in
// "update" mode. In this case, we have nothing to write to the
// snapshot file so we can just exit early
return ;
}
formattedSnapshot = escapeStringForJs ( formattedSnapshot ) ;
formattedSnapshot = formattedSnapshot . includes ( "\n" )
? ` \ n ${ formattedSnapshot } \ n `
: formattedSnapshot ;
const formattedName = escapeStringForJs ( name ) ;
buf . push ( ` \ nsnapshot[ \` ${ formattedName } \` ] = \` ${ formattedSnapshot } \` ; ` ) ;
} ) ;
2022-05-27 12:27:13 +00:00
const snapshotFilePath = fromFileUrl ( this . # snapshotFileUrl ) ;
2022-05-06 13:05:27 +00:00
ensureFileSync ( snapshotFilePath ) ;
Deno . writeTextFileSync ( snapshotFilePath , buf . join ( "\n" ) + "\n" ) ;
2023-06-07 05:05:47 +00:00
const updated = this . getUpdatedCount ( ) ;
if ( updated > 0 ) {
2024-09-13 05:43:13 +00:00
// deno-lint-ignore no-console
2023-06-07 05:05:47 +00:00
console . log (
2024-06-26 05:23:17 +00:00
` %c \ n > ${ updated } ${
updated === 1 ? "snapshot" : "snapshots"
} updated . ` ,
"color: green; font-weight: bold;" ,
2023-06-07 05:05:47 +00:00
) ;
2022-05-06 13:05:27 +00:00
}
2023-06-10 10:02:09 +00:00
const removed = removedSnapshotNames . length ;
if ( removed > 0 ) {
2024-09-13 05:43:13 +00:00
// deno-lint-ignore no-console
2023-06-10 10:02:09 +00:00
console . log (
2024-06-26 05:23:17 +00:00
` %c \ n > ${ removed } ${
removed === 1 ? "snapshot" : "snapshots"
} removed . ` ,
"color: red; font-weight: bold;" ,
2023-06-10 10:02:09 +00:00
) ;
for ( const snapshotName of removedSnapshotNames ) {
2024-09-13 05:43:13 +00:00
// deno-lint-ignore no-console
2024-06-26 05:23:17 +00:00
console . log ( ` %c • ${ snapshotName } ` , "color: red;" ) ;
2023-06-10 10:02:09 +00:00
}
}
2022-05-06 13:05:27 +00:00
} ;
/ * *
* Returns ` this.#currentSnapshots ` and if necessary , tries to initialize it by reading existing
* snapshots from the snapshot file . If the snapshot mode is ` update ` and the snapshot file does
* not exist then it will be created .
* /
2022-05-27 12:27:13 +00:00
async # readSnapshotFile ( options : SnapshotOptions ) {
if ( this . # currentSnapshots ) {
return this . # currentSnapshots ;
2022-05-06 13:05:27 +00:00
}
if ( getIsUpdate ( options ) ) {
2022-05-27 12:27:13 +00:00
await ensureFile ( fromFileUrl ( this . # snapshotFileUrl ) ) ;
2022-04-20 11:46:21 +00:00
}
2022-05-06 13:05:27 +00:00
try {
2022-05-27 12:27:13 +00:00
const snapshotFileUrl = this . # snapshotFileUrl . toString ( ) ;
2022-05-06 13:05:27 +00:00
const { snapshot } = await import ( snapshotFileUrl ) ;
2022-05-27 12:27:13 +00:00
this . # currentSnapshots = typeof snapshot === "undefined"
2022-05-06 13:05:27 +00:00
? new Map ( )
: new Map (
Object . entries ( snapshot ) . map ( ( [ name , snapshot ] ) = > {
if ( typeof snapshot !== "string" ) {
throw new AssertionError (
getErrorMessage (
` Corrupt snapshot: \ n \ t( ${ name } ) \ n \ t ${ snapshotFileUrl } ` ,
options ,
) ,
) ;
}
return [
name ,
snapshot . includes ( "\n" ) ? snapshot . slice ( 1 , - 1 ) : snapshot ,
] ;
} ) ,
) ;
2022-05-27 12:27:13 +00:00
return this . # currentSnapshots ;
2022-05-06 13:05:27 +00:00
} catch ( error ) {
if (
error instanceof TypeError &&
error . message . startsWith ( "Module not found" )
) {
throw new AssertionError (
getErrorMessage (
"Missing snapshot file." ,
options ,
) ,
) ;
}
throw error ;
}
}
/ * *
* Register a teardown function which writes the snapshot file to disk and logs the number
* of snapshots updated after all tests have run .
*
* This method can safely be called more than once and will only register the teardown
2023-06-07 05:05:47 +00:00
* function once in a context .
2022-05-06 13:05:27 +00:00
* /
2024-07-01 02:14:37 +00:00
async registerTeardown() {
2022-05-27 12:27:13 +00:00
if ( ! this . # teardownRegistered ) {
2024-07-01 02:14:37 +00:00
const permission = await Deno . permissions . query ( {
name : "write" ,
path : this. # snapshotFileUrl ,
} ) ;
if ( permission . state !== "granted" ) {
throw new Deno . errors . PermissionDenied (
` Missing write access to snapshot file ( ${ this . # snapshotFileUrl } ). This is required because assertSnapshot was called in update mode. Please pass the --allow-write flag. ` ,
) ;
}
2022-05-27 12:27:13 +00:00
globalThis . addEventListener ( "unload" , this . # teardown ) ;
this . # teardownRegistered = true ;
2022-05-06 13:05:27 +00:00
}
}
/ * *
* Gets the number of snapshots which have been created with the same name and increments
* the count by 1 .
* /
2024-06-17 03:11:31 +00:00
getCount ( snapshotName : string ) {
refactor(archive,async,cli,csv,dotenv,encoding,expect,fmt,front-matter,fs,http,internal,log,net,path,semver,testing,text,webgpu,yaml): enable `"exactOptionalPropertyTypes"` option (#5892)
2024-09-04 05:15:01 +00:00
let count = this . # snapshotCounts . get ( snapshotName ) ? ? 0 ;
2022-05-27 12:27:13 +00:00
this . # snapshotCounts . set ( snapshotName , ++ count ) ;
2022-05-06 13:05:27 +00:00
return count ;
}
/ * *
* Get an existing snapshot by name or returns ` undefined ` if the snapshot does not exist .
* /
2024-06-17 03:11:31 +00:00
async getSnapshot ( snapshotName : string , options : SnapshotOptions ) {
2022-05-27 12:27:13 +00:00
const snapshots = await this . # readSnapshotFile ( options ) ;
2022-05-06 13:05:27 +00:00
return snapshots . get ( snapshotName ) ;
}
/ * *
* Update a snapshot by name . Updates will be written to the snapshot file when all tests
* have run . If the snapshot does not exist , it will be created .
*
* Should only be called when mode is ` update ` .
* /
2024-06-17 03:11:31 +00:00
updateSnapshot ( snapshotName : string , snapshot : string ) {
2022-05-27 12:27:13 +00:00
if ( ! this . # snapshotsUpdated . includes ( snapshotName ) ) {
this . # snapshotsUpdated . push ( snapshotName ) ;
2022-05-06 13:05:27 +00:00
}
2022-05-27 12:27:13 +00:00
const currentSnapshots = this . # getCurrentSnapshotsInitialized ( ) ;
2022-05-06 13:05:27 +00:00
if ( ! currentSnapshots . has ( snapshotName ) ) {
currentSnapshots . set ( snapshotName , undefined ) ;
}
2022-05-27 12:27:13 +00:00
this . # updatedSnapshots . set ( snapshotName , snapshot ) ;
2022-05-06 13:05:27 +00:00
}
/ * *
* Get the number of updated snapshots .
* /
2024-06-17 03:11:31 +00:00
getUpdatedCount() {
2022-05-27 12:27:13 +00:00
return this . # snapshotsUpdated . length ;
2022-05-06 13:05:27 +00:00
}
/ * *
* Add a snapshot to the update queue .
*
* Tracks the order in which snapshots were created so that they can be written to
* the snapshot file in the correct order .
*
* Should be called with each snapshot , regardless of the mode , as a future call to
* ` assertSnapshot ` could cause updates to be written to the snapshot file if the
* ` update ` mode is passed in the options .
* /
2024-06-17 03:11:31 +00:00
pushSnapshotToUpdateQueue ( snapshotName : string ) {
2022-05-06 13:05:27 +00:00
this . snapshotUpdateQueue . push ( snapshotName ) ;
}
2022-05-30 13:21:05 +00:00
/ * *
* Check if exist snapshot
* /
2024-06-17 03:11:31 +00:00
hasSnapshot ( snapshotName : string ) : boolean {
2022-05-30 13:21:05 +00:00
return this . # currentSnapshots
? this . # currentSnapshots . has ( snapshotName )
: false ;
}
2022-04-20 11:46:21 +00:00
}
/ * *
* Make an assertion that ` actual ` matches a snapshot . If the snapshot and ` actual ` do
2024-09-16 22:46:48 +00:00
* not match , then throw .
2022-04-20 11:46:21 +00:00
*
* Type parameter can be specified to ensure values under comparison have the same type .
2022-11-25 11:40:23 +00:00
*
2024-06-17 02:31:31 +00:00
* @example Usage
2022-04-20 11:46:21 +00:00
* ` ` ` ts
2024-04-29 02:57:30 +00:00
* import { assertSnapshot } from "@std/testing/snapshot" ;
2022-04-20 11:46:21 +00:00
*
2024-06-17 02:31:31 +00:00
* Deno . test ( "snapshot" , async ( t ) = > {
* await assertSnapshot < number > ( t , 2 ) ;
2022-04-20 11:46:21 +00:00
* } ) ;
* ` ` `
2024-06-17 02:31:31 +00:00
* @typeParam T The type of the snapshot
* @param context The test context
* @param actual The actual value to compare
* @param options The options
2022-04-20 11:46:21 +00:00
* /
2022-05-06 13:05:27 +00:00
export async function assertSnapshot < T > (
2022-07-02 09:24:42 +00:00
context : Deno.TestContext ,
2022-05-06 13:05:27 +00:00
actual : T ,
options : SnapshotOptions < T > ,
2022-04-20 11:46:21 +00:00
) : Promise < void > ;
2024-06-17 02:31:31 +00:00
/ * *
* Make an assertion that ` actual ` matches a snapshot . If the snapshot and ` actual ` do
2024-09-16 22:46:48 +00:00
* not match , then throw .
2024-06-17 02:31:31 +00:00
*
* Type parameter can be specified to ensure values under comparison have the same type .
*
* @example Usage
* ` ` ` ts
* import { assertSnapshot } from "@std/testing/snapshot" ;
*
* Deno . test ( "snapshot" , async ( t ) = > {
* await assertSnapshot < number > ( t , 2 ) ;
* } ) ;
* ` ` `
*
* @typeParam T The type of the snapshot
* @param context The test context
* @param actual The actual value to compare
2024-11-05 01:16:19 +00:00
* @param message The optional assertion message
2024-06-17 02:31:31 +00:00
* /
2022-04-20 11:46:21 +00:00
export async function assertSnapshot < T > (
context : Deno.TestContext ,
actual : T ,
2022-05-06 13:05:27 +00:00
message? : string ,
2022-04-20 11:46:21 +00:00
) : Promise < void > ;
export async function assertSnapshot (
context : Deno.TestContext ,
actual : unknown ,
2022-05-06 13:05:27 +00:00
msgOrOpts? : string | SnapshotOptions < unknown > ,
2022-08-24 01:21:57 +00:00
) {
2022-05-06 13:05:27 +00:00
const options = getOptions ( ) ;
const assertSnapshotContext = AssertSnapshotContext . fromOptions (
context ,
options ,
) ;
const testName = getTestName ( context , options ) ;
const count = assertSnapshotContext . getCount ( testName ) ;
const name = ` ${ testName } ${ count } ` ;
const snapshot = await assertSnapshotContext . getSnapshot (
name ,
options ,
) ;
assertSnapshotContext . pushSnapshotToUpdateQueue ( name ) ;
const _serialize = options . serializer || serialize ;
const _actual = _serialize ( actual ) ;
if ( getIsUpdate ( options ) ) {
2024-07-01 02:14:37 +00:00
await assertSnapshotContext . registerTeardown ( ) ;
2022-05-06 13:05:27 +00:00
if ( ! equal ( _actual , snapshot ) ) {
assertSnapshotContext . updateSnapshot ( name , _actual ) ;
2022-04-20 11:46:21 +00:00
}
} else {
2022-05-30 13:21:05 +00:00
if (
! assertSnapshotContext . hasSnapshot ( name ) ||
typeof snapshot === "undefined"
) {
2022-05-06 13:05:27 +00:00
throw new AssertionError (
getErrorMessage ( ` Missing snapshot: ${ name } ` , options ) ,
) ;
2022-04-20 11:46:21 +00:00
}
2022-05-06 13:05:27 +00:00
if ( equal ( _actual , snapshot ) ) {
2022-04-20 11:46:21 +00:00
return ;
}
2024-06-02 10:28:31 +00:00
const stringDiff = ! _actual . includes ( "\n" ) ;
const diffResult = stringDiff
? diffStr ( _actual , snapshot )
: diff ( _actual . split ( "\n" ) , snapshot . split ( "\n" ) ) ;
const diffMsg = buildMessage ( diffResult , { stringDiff } ) . join ( "\n" ) ;
const message = ` Snapshot does not match: \ n ${ diffMsg } ` ;
2022-05-06 13:05:27 +00:00
throw new AssertionError (
getErrorMessage ( message , options ) ,
) ;
2022-04-20 11:46:21 +00:00
}
2022-05-06 13:05:27 +00:00
function getOptions ( ) : SnapshotOptions {
if ( typeof msgOrOpts === "object" && msgOrOpts !== null ) {
return msgOrOpts ;
2022-04-20 11:46:21 +00:00
}
2022-05-06 13:05:27 +00:00
return {
refactor(archive,async,cli,csv,dotenv,encoding,expect,fmt,front-matter,fs,http,internal,log,net,path,semver,testing,text,webgpu,yaml): enable `"exactOptionalPropertyTypes"` option (#5892)
2024-09-04 05:15:01 +00:00
msg : msgOrOpts ! ,
2022-05-06 13:05:27 +00:00
} ;
2022-04-20 11:46:21 +00:00
}
2022-05-06 13:05:27 +00:00
function getTestName (
context : Deno.TestContext ,
options? : SnapshotOptions ,
) : string {
if ( options && options . name ) {
return options . name ;
} else if ( context . parent ) {
2022-04-20 11:46:21 +00:00
return ` ${ getTestName ( context . parent ) } > ${ context . name } ` ;
}
return context . name ;
}
}
2022-07-02 09:24:42 +00:00
2024-06-17 02:31:31 +00:00
/ * *
* Create { @linkcode assertSnapshot } function with the given options .
*
* The specified option becomes the default for returned { @linkcode assertSnapshot }
*
* @example Usage
* ` ` ` ts
* import { createAssertSnapshot } from "@std/testing/snapshot" ;
*
* const assertSnapshot = createAssertSnapshot ( {
* // Uses the custom directory for saving snapshot files.
* dir : "my_snapshot_dir" ,
* } ) ;
*
* Deno . test ( "a snapshot test case" , async ( t ) = > {
* await assertSnapshot ( t , {
* foo : "Hello" ,
* bar : "World" ,
* } ) ;
* } )
* ` ` `
*
* @typeParam T The type of the snapshot
* @param options The options
* @param baseAssertSnapshot { @linkcode assertSnapshot } function implementation . Default to the original { @linkcode assertSnapshot }
* @returns { @linkcode assertSnapshot } function with the given default options .
* /
2022-07-02 09:24:42 +00:00
export function createAssertSnapshot < T > (
options : SnapshotOptions < T > ,
baseAssertSnapshot : typeof assertSnapshot = assertSnapshot ,
) : typeof assertSnapshot {
return async function _assertSnapshot (
context : Deno.TestContext ,
actual : T ,
messageOrOptions? : string | SnapshotOptions < T > ,
2022-08-24 01:21:57 +00:00
) {
2022-07-02 09:24:42 +00:00
const mergedOptions : SnapshotOptions < T > = {
. . . options ,
. . . ( typeof messageOrOptions === "string"
? {
msg : messageOrOptions ,
}
: messageOrOptions ) ,
} ;
await baseAssertSnapshot ( context , actual , mergedOptions ) ;
} ;
}