1- import { assertEquals , assertExists , assertInstanceOf , assertObjectMatch } from "jsr:@std/assert" ;
2- import { afterEach , beforeEach , describe , it } from "jsr:@std/testing/bdd" ;
1+ /// <reference path="./globals.d.ts" />
2+
3+ type TestCallback = ( ) => void | Promise < void > ;
4+
5+ type Suite = {
6+ name : string ;
7+ afterEach : TestCallback [ ] ;
8+ beforeEach : TestCallback [ ] ;
9+ } ;
310
411type MockImplementation < TArgs extends unknown [ ] = unknown [ ] , TResult = unknown > = (
512 ...args : TArgs
@@ -9,6 +16,7 @@ type MockFunction<TArgs extends unknown[] = unknown[], TResult = unknown> = ((
916 ...args : TArgs
1017) => TResult ) & {
1118 mockClear : ( ) => void ;
19+ mockRejectedValueOnce : ( value : unknown ) => MockFunction < TArgs , TResult > ;
1220 mockResolvedValueOnce : ( value : Awaited < TResult > ) => MockFunction < TArgs , TResult > ;
1321 mockImplementation : (
1422 implementation : MockImplementation < TArgs , TResult > ,
@@ -18,6 +26,7 @@ type MockFunction<TArgs extends unknown[] = unknown[], TResult = unknown> = ((
1826type AnyMockFunction = MockFunction < any [ ] , any > ;
1927
2028const registeredMocks = new Set < AnyMockFunction > ( ) ;
29+ const suiteStack : Suite [ ] = [ ] ;
2130
2231function isRecord ( value : unknown ) : value is Record < string , unknown > {
2332 return typeof value === "object" && value !== null ;
@@ -27,6 +36,51 @@ function createAssertionError(message: string): Error {
2736 return new Error ( message ) ;
2837}
2938
39+ function deepEqual ( a : unknown , b : unknown ) : boolean {
40+ if ( Object . is ( a , b ) ) {
41+ return true ;
42+ }
43+
44+ if ( a instanceof Date && b instanceof Date ) {
45+ return a . getTime ( ) === b . getTime ( ) ;
46+ }
47+
48+ if ( Array . isArray ( a ) && Array . isArray ( b ) ) {
49+ return a . length === b . length && a . every ( ( value , index ) => deepEqual ( value , b [ index ] ) ) ;
50+ }
51+
52+ if ( isRecord ( a ) && isRecord ( b ) ) {
53+ const aKeys = Object . keys ( a ) ;
54+ const bKeys = Object . keys ( b ) ;
55+
56+ return (
57+ aKeys . length === bKeys . length &&
58+ aKeys . every ( ( key ) => bKeys . includes ( key ) && deepEqual ( a [ key ] , b [ key ] ) )
59+ ) ;
60+ }
61+
62+ return false ;
63+ }
64+
65+ function objectMatches (
66+ actual : Record < string , unknown > ,
67+ expected : Record < string , unknown > ,
68+ ) : boolean {
69+ return Object . entries ( expected ) . every ( ( [ key , value ] ) => {
70+ if ( ! ( key in actual ) ) {
71+ return false ;
72+ }
73+
74+ const actualValue = actual [ key ] ;
75+
76+ if ( isRecord ( value ) && isRecord ( actualValue ) ) {
77+ return objectMatches ( actualValue , value ) ;
78+ }
79+
80+ return deepEqual ( actualValue , value ) ;
81+ } ) ;
82+ }
83+
3084function createMock < TArgs extends unknown [ ] = unknown [ ] , TResult = unknown > (
3185 implementation ?: MockImplementation < TArgs , TResult > ,
3286) : MockFunction < TArgs , TResult > {
@@ -55,6 +109,12 @@ function createMock<TArgs extends unknown[] = unknown[], TResult = unknown>(
55109 return mockFn ;
56110 } ;
57111
112+ mockFn . mockRejectedValueOnce = ( value : unknown ) => {
113+ queue . push ( ( ) => Promise . reject ( value ) as TResult ) ;
114+
115+ return mockFn ;
116+ } ;
117+
58118 mockFn . mockImplementation = ( nextImplementation : MockImplementation < TArgs , TResult > ) => {
59119 implementation = nextImplementation ;
60120
@@ -74,10 +134,14 @@ function createExpect(actual: unknown) {
74134 }
75135 } ,
76136 toEqual ( expected : unknown ) {
77- assertEquals ( actual , expected ) ;
137+ if ( ! deepEqual ( actual , expected ) ) {
138+ throw createAssertionError ( "Expected values to be deeply equal" ) ;
139+ }
78140 } ,
79141 toStrictEqual ( expected : unknown ) {
80- assertEquals ( actual , expected ) ;
142+ if ( ! deepEqual ( actual , expected ) ) {
143+ throw createAssertionError ( "Expected values to be strictly equal" ) ;
144+ }
81145 } ,
82146 toContain ( expected : unknown ) {
83147 if ( typeof actual === "string" ) {
@@ -97,17 +161,7 @@ function createExpect(actual: unknown) {
97161 throw createAssertionError ( "Expected value to be an array" ) ;
98162 }
99163
100- const found = actual . some ( ( value ) => {
101- try {
102- assertEquals ( value , expected ) ;
103-
104- return true ;
105- } catch {
106- return false ;
107- }
108- } ) ;
109-
110- if ( ! found ) {
164+ if ( ! actual . some ( ( value ) => deepEqual ( value , expected ) ) ) {
111165 throw createAssertionError ( "Expected array to contain a deeply equal value" ) ;
112166 }
113167 } ,
@@ -128,10 +182,24 @@ function createExpect(actual: unknown) {
128182 }
129183 } ,
130184 toBeDefined ( ) {
131- assertExists ( actual ) ;
185+ if ( actual === undefined || actual === null ) {
186+ throw createAssertionError ( "Expected value to be defined" ) ;
187+ }
188+ } ,
189+ toBeUndefined ( ) {
190+ if ( actual !== undefined ) {
191+ throw createAssertionError ( `Expected ${ String ( actual ) } to be undefined` ) ;
192+ }
193+ } ,
194+ toBeTruthy ( ) {
195+ if ( ! actual ) {
196+ throw createAssertionError ( `Expected ${ String ( actual ) } to be truthy` ) ;
197+ }
132198 } ,
133199 toBeInstanceOf ( expected : new ( ...args : any [ ] ) => unknown ) {
134- assertInstanceOf ( actual , expected ) ;
200+ if ( ! ( actual instanceof expected ) ) {
201+ throw createAssertionError ( `Expected value to be instance of ${ expected . name } ` ) ;
202+ }
135203 } ,
136204 toBeGreaterThan ( expected : number ) {
137205 if ( ! ( typeof actual === "number" && actual > expected ) ) {
@@ -153,20 +221,18 @@ function createExpect(actual: unknown) {
153221 }
154222 } ,
155223 toMatchObject ( expected : Record < string , unknown > ) {
156- if ( ! isRecord ( actual ) ) {
157- throw createAssertionError ( "Expected value to be an object " ) ;
224+ if ( ! isRecord ( actual ) || ! objectMatches ( actual , expected ) ) {
225+ throw createAssertionError ( "Expected object to match " ) ;
158226 }
159-
160- assertObjectMatch ( actual , expected ) ;
161227 } ,
162228 get rejects ( ) {
163229 return {
164230 async toThrow ( expected ?: new ( ...args : any [ ] ) => unknown ) {
165231 try {
166232 await ( actual as Promise < unknown > ) ;
167233 } catch ( error ) {
168- if ( expected ) {
169- assertInstanceOf ( error , expected ) ;
234+ if ( expected && ! ( error instanceof expected ) ) {
235+ throw createAssertionError ( `Expected error to be instance of ${ expected . name } ` ) ;
170236 }
171237
172238 return ;
@@ -179,7 +245,76 @@ function createExpect(actual: unknown) {
179245 } ;
180246}
181247
182- export { afterEach , beforeEach , describe , it } ;
248+ async function runHooks ( hooks : TestCallback [ ] ) {
249+ for ( const hook of hooks ) {
250+ await hook ( ) ;
251+ }
252+ }
253+
254+ function currentSuiteChain ( ) {
255+ return [ ...suiteStack ] ;
256+ }
257+
258+ type DescribeFunction = ( ( name : string , callback : TestCallback ) => void ) & {
259+ skip : ( name : string , callback : TestCallback ) => void ;
260+ } ;
261+
262+ const describe : DescribeFunction = ( name , callback ) => {
263+ suiteStack . push ( {
264+ afterEach : [ ] ,
265+ beforeEach : [ ] ,
266+ name,
267+ } ) ;
268+
269+ try {
270+ callback ( ) ;
271+ } finally {
272+ suiteStack . pop ( ) ;
273+ }
274+ } ;
275+
276+ describe . skip = ( ) => { } ;
277+
278+ export function beforeEach ( callback : TestCallback ) {
279+ const currentSuite = suiteStack . at ( - 1 ) ;
280+
281+ if ( ! currentSuite ) {
282+ throw new Error ( "beforeEach must be used inside describe" ) ;
283+ }
284+
285+ currentSuite . beforeEach . push ( callback ) ;
286+ }
287+
288+ export function afterEach ( callback : TestCallback ) {
289+ const currentSuite = suiteStack . at ( - 1 ) ;
290+
291+ if ( ! currentSuite ) {
292+ throw new Error ( "afterEach must be used inside describe" ) ;
293+ }
294+
295+ currentSuite . afterEach . push ( callback ) ;
296+ }
297+
298+ export function it ( name : string , callback : TestCallback , timeout ?: number ) {
299+ const suites = currentSuiteChain ( ) ;
300+ const testName = [ ...suites . map ( ( suite ) => suite . name ) , name ] . join ( " > " ) ;
301+
302+ Deno . test ( {
303+ fn : async ( ) => {
304+ await runHooks ( suites . flatMap ( ( suite ) => suite . beforeEach ) ) ;
305+
306+ try {
307+ await callback ( ) ;
308+ } finally {
309+ await runHooks ( [ ...suites ] . reverse ( ) . flatMap ( ( suite ) => suite . afterEach ) ) ;
310+ }
311+ } ,
312+ name : testName ,
313+ sanitizeOps : false ,
314+ sanitizeResources : false ,
315+ ...( timeout ? { sanitizeExit : false } : { } ) ,
316+ } ) ;
317+ }
183318
184319export const test = it ;
185320
@@ -193,3 +328,5 @@ export const vi = {
193328 }
194329 } ,
195330} ;
331+
332+ export { describe } ;
0 commit comments