1+ import { run } from '@ember/runloop' ;
12import type { SetupContextOptions , TestContext as EmberTestContext } from '@ember/test-helpers' ;
23import { getTestMetadata , hasCalledSetupRenderingContext , setupContext , teardownContext } from '@ember/test-helpers' ;
34import type { Owner } from '@ember/test-helpers/build-owner' ;
45
6+ import { hbs } from 'ember-cli-htmlbars' ;
57import AbstractTestLoader from 'ember-cli-test-loader/test-support/index' ;
68
79import { module as _module , skip as _skip , test as _test , todo as _todo } from './-define' ;
10+ import isComponent from './-ember/is-component' ;
811import type { Hooks , ModuleCallback , TestCallback } from './-types' ;
912import { setupGlobalHooks } from './internals/config' ;
1013import { PublicTestInfo } from './internals/run' ;
1114
15+ // const OUTLET_TEMPLATE = hbs`{{outlet}}`;
16+ const INVOKE_PROVIDED_COMPONENT = hbs `<this.ProvidedComponent />` as object ;
17+
1218export interface TestContext extends EmberTestContext {
1319 element : HTMLDivElement ;
1420}
21+ export interface RenderingTestContext extends TestContext {
22+ [ hasCalledSetupRenderingContext ] : boolean ;
23+ render ( template : object ) : Promise < void > ;
24+ }
1525
1626export function module < TC extends TestContext = TestContext > ( name : string , cb : ModuleCallback < TC > ) : void {
1727 return _module < TC > ( name , cb ) ;
@@ -67,12 +77,13 @@ export function setupTest<TC extends TestContext>(hooks: Hooks<TC>, opts?: Setup
6777 } ) ;
6878}
6979
70- function upgradeContext ( context : TestContext ) : asserts context is TestContext & {
71- [ hasCalledSetupRenderingContext ] : boolean ;
80+ type Outlet = { appendTo : ( element : Element ) => void ; setOutletState : ( state : object ) => void } ;
81+
82+ function upgradeContext ( context : TestContext ) : asserts context is RenderingTestContext & {
7283 [ PublicTestInfo ] : { id : string ; name : string } ;
7384 rootElement : HTMLDivElement ;
7485} {
75- ( context as unknown as { [ hasCalledSetupRenderingContext ] : boolean } ) [ hasCalledSetupRenderingContext ] = true ;
86+ ( context as unknown as RenderingTestContext ) [ hasCalledSetupRenderingContext ] = true ;
7687}
7788
7889function upgradeOwner ( owner : Owner ) : asserts owner is FullOwner { }
@@ -85,6 +96,7 @@ export function setupRenderingTest<TC extends TestContext>(hooks: Hooks<TC>, opt
8596
8697 hooks . beforeEach ( async function ( ) {
8798 upgradeContext ( this ) ;
99+ this . render = ( template : object ) => render ( this , template ) ;
88100 const opts = Object . assign ( { } , _options ) ;
89101 const testMetadata = getTestMetadata ( this ) ;
90102 testMetadata . setupTypes . push ( 'setupRenderingContext' ) ;
@@ -102,7 +114,7 @@ export function setupRenderingTest<TC extends TestContext>(hooks: Hooks<TC>, opt
102114 const { owner } = this ;
103115 upgradeOwner ( owner ) ;
104116
105- const OutletView = owner . factoryFor ( 'view:-outlet' ) ;
117+ const OutletView = owner . factoryFor ( 'view:-outlet' ) ! ;
106118 const environment = owner . lookup ( '-environment:main' ) ;
107119 const template = owner . lookup ( 'template:-outlet' ) ;
108120 testContainer . setAttribute ( 'test-id' , this [ PublicTestInfo ] . id ) ;
@@ -111,7 +123,7 @@ export function setupRenderingTest<TC extends TestContext>(hooks: Hooks<TC>, opt
111123 const toplevelView = OutletView . create ( {
112124 template,
113125 environment,
114- } ) as { appendTo : ( element : Element ) => void } ;
126+ } ) as Outlet ;
115127
116128 owner . register ( '-top-level-view:main' , {
117129 create ( ) {
@@ -177,3 +189,99 @@ export function configure() {
177189
178190 loadTests ( ) ;
179191}
192+
193+ export function isRenderingTestContext ( context : TestContext ) : context is RenderingTestContext {
194+ return hasCalledSetupRenderingContext in context ;
195+ }
196+
197+ function isTemplateFunction ( template : unknown ) : template is ( owner : Owner ) => object {
198+ return typeof template === 'function' ;
199+ }
200+
201+ function lookupTemplate ( owner : Owner , templateFullName : RegistryKey ) : object | undefined {
202+ upgradeOwner ( owner ) ;
203+ const template = owner . lookup ( templateFullName ) as object | ( ( owner : Owner ) => object ) | undefined ;
204+ if ( isTemplateFunction ( template ) ) return template ( owner ) ;
205+ return template ;
206+ }
207+
208+ function lookupOutletTemplate ( owner : Owner ) : object {
209+ upgradeOwner ( owner ) ;
210+ const OutletTemplate = lookupTemplate ( owner , 'template:-outlet' ) ;
211+ if ( ! OutletTemplate ) {
212+ throw new Error ( `Could not find -outlet template` ) ;
213+ // owner.register('template:-outlet', OUTLET_TEMPLATE);
214+ // OutletTemplate = lookupTemplate(owner, 'template:-outlet');
215+ }
216+
217+ return OutletTemplate ;
218+ }
219+
220+ let templateId = 0 ;
221+ // eslint-disable-next-line @typescript-eslint/require-await
222+ export async function render ( context : TestContext , template : object ) : Promise < void > {
223+ if ( ! template ) {
224+ throw new Error ( 'you must pass a template to `render()`' ) ;
225+ }
226+
227+ if ( ! context || ! isRenderingTestContext ( context ) ) {
228+ throw new Error ( 'Cannot call `render` without having first called `setupRenderingContext`.' ) ;
229+ }
230+
231+ const { owner } = context ;
232+ upgradeOwner ( owner ) ;
233+ const testMetadata = getTestMetadata ( context ) ;
234+ testMetadata . usedHelpers . push ( 'render' ) ;
235+
236+ // SAFETY: this is all wildly unsafe, because it is all using private API.
237+ // At some point we should define a path forward for this kind of internal
238+ // API. For now, just flagging it as *NOT* being safe!
239+
240+ const toplevelView = owner . lookup ( '-top-level-view:main' ) as Outlet ;
241+ const OutletTemplate = lookupOutletTemplate ( owner ) ;
242+
243+ let controllerContext : object = context ;
244+ if ( isComponent ( template ) ) {
245+ controllerContext = {
246+ ProvidedComponent : template ,
247+ } ;
248+ template = INVOKE_PROVIDED_COMPONENT ;
249+ }
250+
251+ templateId += 1 ;
252+ const templateFullName = `template:-undertest-${ templateId } ` as const ;
253+ owner . register ( templateFullName , template ) ;
254+ const finalTemplate = lookupTemplate ( owner , templateFullName ) ;
255+
256+ const outletState = {
257+ render : {
258+ owner,
259+ into : undefined ,
260+ outlet : 'main' ,
261+ name : 'application' ,
262+ controller : undefined ,
263+ ViewClass : undefined ,
264+ template : OutletTemplate ,
265+ } ,
266+
267+ outlets : {
268+ main : {
269+ render : {
270+ owner,
271+ into : undefined ,
272+ outlet : 'main' ,
273+ name : 'index' ,
274+ controller : controllerContext ,
275+ ViewClass : undefined ,
276+ template : finalTemplate ,
277+ outlets : { } ,
278+ } ,
279+ outlets : { } ,
280+ } ,
281+ } ,
282+ } ;
283+
284+ run ( ( ) => {
285+ toplevelView . setOutletState ( outletState ) ;
286+ } ) ;
287+ }
0 commit comments