55 isStandalone ,
66 NgZone ,
77 OnChanges ,
8+ OutputRef ,
9+ OutputRefSubscription ,
810 SimpleChange ,
911 SimpleChanges ,
1012 Type ,
@@ -25,9 +27,17 @@ import {
2527 waitForOptions as dtlWaitForOptions ,
2628 within as dtlWithin ,
2729} from '@testing-library/dom' ;
28- import { ComponentOverride , RenderComponentOptions , RenderResult , RenderTemplateOptions } from './models' ;
30+ import {
31+ ComponentOverride ,
32+ RenderComponentOptions ,
33+ RenderResult ,
34+ RenderTemplateOptions ,
35+ OutputRefKeysWithCallback ,
36+ } from './models' ;
2937import { getConfig } from './config' ;
3038
39+ type SubscribedOutput < T > = readonly [ key : keyof T , callback : ( v : any ) => void , subscription : OutputRefSubscription ] ;
40+
3141const mountedFixtures = new Set < ComponentFixture < any > > ( ) ;
3242const safeInject = TestBed . inject || TestBed . get ;
3343
@@ -57,6 +67,7 @@ export async function render<SutType, WrapperType = SutType>(
5767 componentProperties = { } ,
5868 componentInputs = { } ,
5969 componentOutputs = { } ,
70+ on = { } ,
6071 componentProviders = [ ] ,
6172 childComponentOverrides = [ ] ,
6273 componentImports : componentImports ,
@@ -165,7 +176,55 @@ export async function render<SutType, WrapperType = SutType>(
165176
166177 let detectChanges : ( ) => void ;
167178
168- const fixture = await renderFixture ( componentProperties , componentInputs , componentOutputs ) ;
179+ let renderedPropKeys = Object . keys ( componentProperties ) ;
180+ let renderedInputKeys = Object . keys ( componentInputs ) ;
181+ let renderedOutputKeys = Object . keys ( componentOutputs ) ;
182+ let subscribedOutputs : SubscribedOutput < SutType > [ ] = [ ] ;
183+
184+ const renderFixture = async (
185+ properties : Partial < SutType > ,
186+ inputs : Partial < SutType > ,
187+ outputs : Partial < SutType > ,
188+ subscribeTo : OutputRefKeysWithCallback < SutType > ,
189+ ) : Promise < ComponentFixture < SutType > > => {
190+ const createdFixture : ComponentFixture < SutType > = await createComponent ( componentContainer ) ;
191+ setComponentProperties ( createdFixture , properties ) ;
192+ setComponentInputs ( createdFixture , inputs ) ;
193+ setComponentOutputs ( createdFixture , outputs ) ;
194+ subscribedOutputs = subscribeToComponentOutputs ( createdFixture , subscribeTo ) ;
195+
196+ if ( removeAngularAttributes ) {
197+ createdFixture . nativeElement . removeAttribute ( 'ng-version' ) ;
198+ const idAttribute = createdFixture . nativeElement . getAttribute ( 'id' ) ;
199+ if ( idAttribute && idAttribute . startsWith ( 'root' ) ) {
200+ createdFixture . nativeElement . removeAttribute ( 'id' ) ;
201+ }
202+ }
203+
204+ mountedFixtures . add ( createdFixture ) ;
205+
206+ let isAlive = true ;
207+ createdFixture . componentRef . onDestroy ( ( ) => ( isAlive = false ) ) ;
208+
209+ if ( hasOnChangesHook ( createdFixture . componentInstance ) && Object . keys ( properties ) . length > 0 ) {
210+ const changes = getChangesObj ( null , componentProperties ) ;
211+ createdFixture . componentInstance . ngOnChanges ( changes ) ;
212+ }
213+
214+ detectChanges = ( ) => {
215+ if ( isAlive ) {
216+ createdFixture . detectChanges ( ) ;
217+ }
218+ } ;
219+
220+ if ( detectChangesOnRender ) {
221+ detectChanges ( ) ;
222+ }
223+
224+ return createdFixture ;
225+ } ;
226+
227+ const fixture = await renderFixture ( componentProperties , componentInputs , componentOutputs , on ) ;
169228
170229 if ( deferBlockStates ) {
171230 if ( Array . isArray ( deferBlockStates ) ) {
@@ -177,13 +236,10 @@ export async function render<SutType, WrapperType = SutType>(
177236 }
178237 }
179238
180- let renderedPropKeys = Object . keys ( componentProperties ) ;
181- let renderedInputKeys = Object . keys ( componentInputs ) ;
182- let renderedOutputKeys = Object . keys ( componentOutputs ) ;
183239 const rerender = async (
184240 properties ?: Pick <
185241 RenderTemplateOptions < SutType > ,
186- 'componentProperties' | 'componentInputs' | 'componentOutputs' | 'detectChangesOnRender'
242+ 'componentProperties' | 'componentInputs' | 'componentOutputs' | 'on' | ' detectChangesOnRender'
187243 > & { partialUpdate ?: boolean } ,
188244 ) => {
189245 const newComponentInputs = properties ?. componentInputs ?? { } ;
@@ -205,6 +261,22 @@ export async function render<SutType, WrapperType = SutType>(
205261 setComponentOutputs ( fixture , newComponentOutputs ) ;
206262 renderedOutputKeys = Object . keys ( newComponentOutputs ) ;
207263
264+ // first unsubscribe the no longer available or changed callback-fns
265+ const newObservableSubscriptions : OutputRefKeysWithCallback < SutType > = properties ?. on ?? { } ;
266+ for ( const [ key , cb , subscription ] of subscribedOutputs ) {
267+ // when no longer provided or when the callback has changed
268+ if ( ! ( key in newObservableSubscriptions ) || cb !== ( newObservableSubscriptions as any ) [ key ] ) {
269+ subscription . unsubscribe ( ) ;
270+ }
271+ }
272+ // then subscribe the new callback-fns
273+ subscribedOutputs = Object . entries ( newObservableSubscriptions ) . map ( ( [ key , cb ] ) => {
274+ const existing = subscribedOutputs . find ( ( [ k ] ) => k === key ) ;
275+ return existing && existing [ 1 ] === cb
276+ ? existing // nothing to do
277+ : subscribeToComponentOutput ( fixture , key as keyof SutType , cb as ( v : any ) => void ) ;
278+ } ) ;
279+
208280 const newComponentProps = properties ?. componentProperties ?? { } ;
209281 const changesInComponentProps = update (
210282 fixture ,
@@ -249,47 +321,6 @@ export async function render<SutType, WrapperType = SutType>(
249321 : console . log ( dtlPrettyDOM ( element , maxLength , options ) ) ,
250322 ...replaceFindWithFindAndDetectChanges ( dtlGetQueriesForElement ( fixture . nativeElement , queries ) ) ,
251323 } ;
252-
253- async function renderFixture (
254- properties : Partial < SutType > ,
255- inputs : Partial < SutType > ,
256- outputs : Partial < SutType > ,
257- ) : Promise < ComponentFixture < SutType > > {
258- const createdFixture = await createComponent ( componentContainer ) ;
259- setComponentProperties ( createdFixture , properties ) ;
260- setComponentInputs ( createdFixture , inputs ) ;
261- setComponentOutputs ( createdFixture , outputs ) ;
262-
263- if ( removeAngularAttributes ) {
264- createdFixture . nativeElement . removeAttribute ( 'ng-version' ) ;
265- const idAttribute = createdFixture . nativeElement . getAttribute ( 'id' ) ;
266- if ( idAttribute && idAttribute . startsWith ( 'root' ) ) {
267- createdFixture . nativeElement . removeAttribute ( 'id' ) ;
268- }
269- }
270-
271- mountedFixtures . add ( createdFixture ) ;
272-
273- let isAlive = true ;
274- createdFixture . componentRef . onDestroy ( ( ) => ( isAlive = false ) ) ;
275-
276- if ( hasOnChangesHook ( createdFixture . componentInstance ) && Object . keys ( properties ) . length > 0 ) {
277- const changes = getChangesObj ( null , componentProperties ) ;
278- createdFixture . componentInstance . ngOnChanges ( changes ) ;
279- }
280-
281- detectChanges = ( ) => {
282- if ( isAlive ) {
283- createdFixture . detectChanges ( ) ;
284- }
285- } ;
286-
287- if ( detectChangesOnRender ) {
288- detectChanges ( ) ;
289- }
290-
291- return createdFixture ;
292- }
293324}
294325
295326async function createComponent < SutType > ( component : Type < SutType > ) : Promise < ComponentFixture < SutType > > {
@@ -355,6 +386,27 @@ function setComponentInputs<SutType>(
355386 }
356387}
357388
389+ function subscribeToComponentOutputs < SutType > (
390+ fixture : ComponentFixture < SutType > ,
391+ listeners : OutputRefKeysWithCallback < SutType > ,
392+ ) : SubscribedOutput < SutType > [ ] {
393+ // with Object.entries we lose the type information of the key and callback, therefore we need to cast them
394+ return Object . entries ( listeners ) . map ( ( [ key , cb ] ) =>
395+ subscribeToComponentOutput ( fixture , key as keyof SutType , cb as ( v : any ) => void ) ,
396+ ) ;
397+ }
398+
399+ function subscribeToComponentOutput < SutType > (
400+ fixture : ComponentFixture < SutType > ,
401+ key : keyof SutType ,
402+ cb : ( val : any ) => void ,
403+ ) : SubscribedOutput < SutType > {
404+ const eventEmitter = ( fixture . componentInstance as any ) [ key ] as OutputRef < any > ;
405+ const subscription = eventEmitter . subscribe ( cb ) ;
406+ fixture . componentRef . onDestroy ( subscription . unsubscribe . bind ( subscription ) ) ;
407+ return [ key , cb , subscription ] ;
408+ }
409+
358410function overrideComponentImports < SutType > ( sut : Type < SutType > | string , imports : ( Type < any > | any [ ] ) [ ] | undefined ) {
359411 if ( imports ) {
360412 if ( typeof sut === 'function' && isStandalone ( sut ) ) {
0 commit comments