@@ -210,6 +210,12 @@ export function initAllState(
210210 } ;
211211}
212212
213+ type DelayedCheck <
214+ Args extends readonly any [ ] ,
215+ R ,
216+ Fs extends ReadonlyArray < AllState [ number ] > ,
217+ > = ( env : Readonly < BaseEnvBuilder < Args , R , Fs > > ) => Promise < any > ;
218+
213219/**
214220 * Wraps a function that accepts an `ActionState` for testing in different environments.
215221 */
@@ -221,6 +227,7 @@ abstract class BaseEnvBuilder<
221227 protected readonly fn : ( state : ActionState < Fs > , ...args : Args ) => R ;
222228 private logger : RecordingLogger ;
223229 protected state : ActionState < AllState > ;
230+ protected checks : Array < DelayedCheck < Args , R , Fs > > ;
224231
225232 constructor (
226233 fn : ( state : ActionState < Fs > , ...args : Args ) => R ,
@@ -232,6 +239,7 @@ abstract class BaseEnvBuilder<
232239 cloneFrom !== undefined
233240 ? { ...cloneFrom . state , logger : this . logger }
234241 : initAllState ( { logger : this . logger } ) ;
242+ this . checks = [ ...( cloneFrom ?. checks ?? [ ] ) ] ;
235243 }
236244
237245 /**
@@ -270,6 +278,30 @@ abstract class BaseEnvBuilder<
270278 result . state . actions = actions ;
271279 return result ;
272280 }
281+
282+ /**
283+ * Adds a delayed check that `messages` are logged. The check will be
284+ * performed after the main assertion passes.
285+ */
286+ public logs ( t : ExecutionContext < unknown > , ...messages : string [ ] ) : this {
287+ const result = this . clone ( ) ;
288+ result . checks . push ( async ( env ) => {
289+ checkExpectedLogMessages ( t , env . getLogger ( ) . messages , messages ) ;
290+ } ) ;
291+ return result ;
292+ }
293+
294+ /**
295+ * Adds a delayed check that `messages` are not logged. The check will be
296+ * performed after the main assertion passes.
297+ */
298+ public notLogs ( t : ExecutionContext < unknown > , ...messages : string [ ] ) : this {
299+ const result = this . clone ( ) ;
300+ result . checks . push ( async ( env ) => {
301+ checkUnexpectedLogMessages ( t , env . getLogger ( ) . messages , messages ) ;
302+ } ) ;
303+ return result ;
304+ }
273305}
274306
275307class EnvBuilder <
@@ -322,8 +354,21 @@ class CallableEnvBuilder<
322354 assertion : ( val : Awaited < R > , ...assertionArgs : AArgs ) => AResult ,
323355 ...assertionArgs : AArgs
324356 ) : Promise < AResult > {
357+ // this.call() may or may not return a promise,
358+ // `Promise.resolve` turns the result into one if it isn't already,
359+ // and we then await it. That ensures that `result` is an `Awaited<R>`.
325360 const result = await Promise . resolve ( this . call ( ) ) ;
326- return assertion ( result , ...assertionArgs ) ;
361+
362+ // Run the main assertion on the `result`.
363+ const assertionResult = assertion ( result , ...assertionArgs ) ;
364+
365+ // Run other delayed checks.
366+ for ( const delayedCheck of this . checks ) {
367+ await delayedCheck ( this ) ;
368+ }
369+
370+ // Return the result of the main assertion.
371+ return assertionResult ;
327372 }
328373
329374 /**
@@ -337,7 +382,19 @@ class CallableEnvBuilder<
337382 t : ExecutionContext < unknown > ,
338383 expectations ?: ThrowsExpectation < ErrorType > ,
339384 ) : Promise < ThrownError < ErrorType > > {
340- return t . throwsAsync ( ( ) => Promise . resolve ( this . call ( ) ) , expectations ) ;
385+ // Run the main assertion.
386+ const error = t . throwsAsync (
387+ ( ) => Promise . resolve ( this . call ( ) ) ,
388+ expectations ,
389+ ) ;
390+
391+ // Run other delayed checks.
392+ for ( const delayedCheck of this . checks ) {
393+ await delayedCheck ( this ) ;
394+ }
395+
396+ // Return the error.
397+ return error ;
341398 }
342399}
343400
@@ -542,6 +599,34 @@ export function checkExpectedLogMessages(
542599 }
543600}
544601
602+ /**
603+ * Checks that `messages` contains none of `unexpectedMessages`.
604+ */
605+ export function checkUnexpectedLogMessages (
606+ t : ExecutionContext < any > ,
607+ messages : LoggedMessage [ ] ,
608+ unexpectedMessages : string [ ] ,
609+ ) {
610+ const presentMessages : string [ ] = [ ] ;
611+
612+ for ( const unexpectedMessage of unexpectedMessages ) {
613+ if ( hasLoggedMessage ( messages , unexpectedMessage ) ) {
614+ presentMessages . push ( unexpectedMessage ) ;
615+ }
616+ }
617+
618+ if ( presentMessages . length > 0 ) {
619+ const listify = ( lines : string [ ] ) =>
620+ lines . map ( ( m ) => ` - '${ m } '` ) . join ( "\n" ) ;
621+
622+ t . fail (
623+ `Did not expect\n\n${ listify ( presentMessages ) } \n\nin the logger output, but found them in:\n\n${ messages . map ( ( m ) => ` - '${ m . message } '` ) . join ( "\n" ) } ` ,
624+ ) ;
625+ } else {
626+ t . pass ( ) ;
627+ }
628+ }
629+
545630/**
546631 * Asserts that `message` should not have been logged to `logger`.
547632 */
0 commit comments