import { moduleFor, ApplicationTest } from '../../utils/test-case'; import { strip } from '../../utils/abstract-test-case'; import { compile } from '../../utils/helpers'; import Controller from '@ember/controller'; import { RSVP } from 'ember-runtime'; import { Component } from 'ember-glimmer'; import Engine from '@ember/engine'; import { Route } from 'ember-routing'; import { next } from '@ember/runloop'; moduleFor( 'Application test: engine rendering', class extends ApplicationTest { get routerOptions() { return { location: 'none', // This creates a handler function similar to what is in use by ember-engines // internally. Specifically, it returns a promise when transitioning _into_ // the first engine route, but returns the synchronously available handler // _after_ the engine has been resolved. _getHandlerFunction() { let syncHandler = this._super(...arguments); this._enginePromises = Object.create(null); this._resolvedEngines = Object.create(null); return name => { let engineInfo = this._engineInfoByRoute[name]; if (!engineInfo) { return syncHandler(name); } let engineName = engineInfo.name; if (this._resolvedEngines[engineName]) { return syncHandler(name); } let enginePromise = this._enginePromises[engineName]; if (!enginePromise) { enginePromise = new RSVP.Promise(resolve => { setTimeout(() => { this._resolvedEngines[engineName] = true; resolve(); }, 1); }); this._enginePromises[engineName] = enginePromise; } return enginePromise.then(() => syncHandler(name)); }; }, }; } setupAppAndRoutableEngine(hooks = []) { let self = this; this.addTemplate('application', 'Application{{outlet}}'); this.router.map(function() { this.mount('blog'); }); this.add('route-map:blog', function() { this.route('post', function() { this.route('comments'); this.route('likes'); }); this.route('category', { path: 'category/:id' }); this.route('author', { path: 'author/:id' }); }); this.add( 'route:application', Route.extend({ model() { hooks.push('application - application'); }, }) ); this.add( 'engine:blog', Engine.extend({ init() { this._super(...arguments); this.register( 'controller:application', Controller.extend({ queryParams: ['lang'], lang: '', }) ); this.register( 'controller:category', Controller.extend({ queryParams: ['type'], }) ); this.register( 'controller:authorKtrl', Controller.extend({ queryParams: ['official'], }) ); this.register('template:application', compile('Engine{{lang}}{{outlet}}')); this.register( 'route:application', Route.extend({ model() { hooks.push('engine - application'); }, }) ); this.register( 'route:author', Route.extend({ controllerName: 'authorKtrl', }) ); if (self._additionalEngineRegistrations) { self._additionalEngineRegistrations.call(this); } }, }) ); } setupAppAndRoutelessEngine(hooks) { this.setupRoutelessEngine(hooks); this.add( 'engine:chat-engine', Engine.extend({ init() { this._super(...arguments); this.register('template:application', compile('Engine')); this.register( 'controller:application', Controller.extend({ init() { this._super(...arguments); hooks.push('engine - application'); }, }) ); }, }) ); } setupAppAndRoutableEngineWithPartial(hooks) { this.addTemplate('application', 'Application{{outlet}}'); this.router.map(function() { this.mount('blog'); }); this.add('route-map:blog', function() {}); this.add( 'route:application', Route.extend({ model() { hooks.push('application - application'); }, }) ); this.add( 'engine:blog', Engine.extend({ init() { this._super(...arguments); this.register('template:foo', compile('foo partial')); this.register('template:application', compile('Engine{{outlet}} {{partial "foo"}}')); this.register( 'route:application', Route.extend({ model() { hooks.push('engine - application'); }, }) ); }, }) ); } setupRoutelessEngine(hooks) { this.addTemplate('application', 'Application{{mount "chat-engine"}}'); this.add( 'route:application', Route.extend({ model() { hooks.push('application - application'); }, }) ); } setupAppAndRoutlessEngineWithPartial(hooks) { this.setupRoutelessEngine(hooks); this.add( 'engine:chat-engine', Engine.extend({ init() { this._super(...arguments); this.register('template:foo', compile('foo partial')); this.register('template:application', compile('Engine {{partial "foo"}}')); this.register( 'controller:application', Controller.extend({ init() { this._super(...arguments); hooks.push('engine - application'); }, }) ); }, }) ); } additionalEngineRegistrations(callback) { this._additionalEngineRegistrations = callback; } setupEngineWithAttrs() { this.addTemplate('application', 'Application{{mount "chat-engine"}}'); this.add( 'engine:chat-engine', Engine.extend({ init() { this._super(...arguments); this.register('template:components/foo-bar', compile(`{{partial "troll"}}`)); this.register('template:troll', compile('{{attrs.wat}}')); this.register( 'controller:application', Controller.extend({ contextType: 'Engine', }) ); this.register('template:application', compile('Engine {{foo-bar wat=contextType}}')); }, }) ); } stringsEndWith(str, suffix) { return str.indexOf(suffix, str.length - suffix.length) !== -1; } ['@test attrs in an engine']() { this.setupEngineWithAttrs([]); return this.visit('/').then(() => { this.assertText('ApplicationEngine Engine'); }); } ['@test sharing a template between engine and application has separate refinements']() { this.assert.expect(1); let sharedTemplate = compile(strip`

{{contextType}}

{{ambiguous-curlies}} {{outlet}} `); this.add('template:application', sharedTemplate); this.add( 'controller:application', Controller.extend({ contextType: 'Application', 'ambiguous-curlies': 'Controller Data!', }) ); this.router.map(function() { this.mount('blog'); }); this.add('route-map:blog', function() {}); this.add( 'engine:blog', Engine.extend({ init() { this._super(...arguments); this.register( 'controller:application', Controller.extend({ contextType: 'Engine', }) ); this.register('template:application', sharedTemplate); this.register( 'template:components/ambiguous-curlies', compile(strip`

Component!

`) ); }, }) ); return this.visit('/blog').then(() => { this.assertText('ApplicationController Data!EngineComponent!'); }); } ['@test sharing a layout between engine and application has separate refinements']() { this.assert.expect(1); let sharedLayout = compile(strip` {{ambiguous-curlies}} `); let sharedComponent = Component.extend({ layout: sharedLayout, }); this.addTemplate( 'application', strip`

Application

{{my-component ambiguous-curlies="Local Data!"}} {{outlet}} ` ); this.add('component:my-component', sharedComponent); this.router.map(function() { this.mount('blog'); }); this.add('route-map:blog', function() {}); this.add( 'engine:blog', Engine.extend({ init() { this._super(...arguments); this.register( 'template:application', compile(strip`

Engine

{{my-component}} {{outlet}} `) ); this.register('component:my-component', sharedComponent); this.register( 'template:components/ambiguous-curlies', compile(strip`

Component!

`) ); }, }) ); return this.visit('/blog').then(() => { this.assertText('ApplicationLocal Data!EngineComponent!'); }); } ['@test visit() with `shouldRender: true` returns a promise that resolves when application and engine templates have rendered']( assert ) { assert.expect(2); let hooks = []; this.setupAppAndRoutableEngine(hooks); return this.visit('/blog', { shouldRender: true }).then(() => { this.assertText('ApplicationEngine'); this.assert.deepEqual( hooks, ['application - application', 'engine - application'], 'the expected model hooks were fired' ); }); } ['@test visit() with `shouldRender: false` returns a promise that resolves without rendering']( assert ) { assert.expect(2); let hooks = []; this.setupAppAndRoutableEngine(hooks); return this.visit('/blog', { shouldRender: false }).then(() => { assert.strictEqual( document.getElementById('qunit-fixture').children.length, 0, `there are no elements in the qunit-fixture element` ); this.assert.deepEqual( hooks, ['application - application', 'engine - application'], 'the expected model hooks were fired' ); }); } ['@test visit() with `shouldRender: true` returns a promise that resolves when application and routeless engine templates have rendered']( assert ) { assert.expect(2); let hooks = []; this.setupAppAndRoutelessEngine(hooks); return this.visit('/', { shouldRender: true }).then(() => { this.assertText('ApplicationEngine'); this.assert.deepEqual( hooks, ['application - application', 'engine - application'], 'the expected hooks were fired' ); }); } ['@test visit() with partials in routable engine'](assert) { assert.expect(2); let hooks = []; this.setupAppAndRoutableEngineWithPartial(hooks); return this.visit('/blog', { shouldRender: true }).then(() => { this.assertText('ApplicationEngine foo partial'); this.assert.deepEqual( hooks, ['application - application', 'engine - application'], 'the expected hooks were fired' ); }); } ['@test visit() with partials in non-routable engine'](assert) { assert.expect(2); let hooks = []; this.setupAppAndRoutlessEngineWithPartial(hooks); return this.visit('/', { shouldRender: true }).then(() => { this.assertText('ApplicationEngine foo partial'); this.assert.deepEqual( hooks, ['application - application', 'engine - application'], 'the expected hooks were fired' ); }); } ['@test deactivate should be called on Engine Routes before destruction'](assert) { assert.expect(3); this.setupAppAndRoutableEngine(); this.add( 'engine:blog', Engine.extend({ init() { this._super(...arguments); this.register('template:application', compile('Engine{{outlet}}')); this.register( 'route:application', Route.extend({ deactivate() { assert.notOk(this.isDestroyed, 'Route is not destroyed'); assert.notOk(this.isDestroying, 'Route is not being destroyed'); }, }) ); }, }) ); return this.visit('/blog').then(() => { this.assertText('ApplicationEngine'); }); } ['@test engine should lookup and use correct controller']() { this.setupAppAndRoutableEngine(); return this.visit('/blog?lang=English').then(() => { this.assertText('ApplicationEngineEnglish'); }); } ['@test error substate route works for the application route of an Engine'](assert) { assert.expect(2); let errorEntered = RSVP.defer(); this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register( 'route:application_error', Route.extend({ activate() { next(errorEntered.resolve); }, }) ); this.register('template:application_error', compile('Error! {{model.message}}')); this.register( 'route:post', Route.extend({ model() { return RSVP.reject(new Error('Oh, noes!')); }, }) ); }); return this.visit('/') .then(() => { this.assertText('Application'); return this.transitionTo('blog.post'); }) .then(() => { return errorEntered.promise; }) .then(() => { this.assertText('ApplicationError! Oh, noes!'); }); } ['@test error route works for the application route of an Engine'](assert) { assert.expect(2); let errorEntered = RSVP.defer(); this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register( 'route:error', Route.extend({ activate() { next(errorEntered.resolve); }, }) ); this.register('template:error', compile('Error! {{model.message}}')); this.register( 'route:post', Route.extend({ model() { return RSVP.reject(new Error('Oh, noes!')); }, }) ); }); return this.visit('/') .then(() => { this.assertText('Application'); return this.transitionTo('blog.post'); }) .then(() => { return errorEntered.promise; }) .then(() => { this.assertText('ApplicationEngineError! Oh, noes!'); }); } ['@test error substate route works for a child route of an Engine'](assert) { assert.expect(2); let errorEntered = RSVP.defer(); this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register( 'route:post_error', Route.extend({ activate() { next(errorEntered.resolve); }, }) ); this.register('template:post_error', compile('Error! {{model.message}}')); this.register( 'route:post', Route.extend({ model() { return RSVP.reject(new Error('Oh, noes!')); }, }) ); }); return this.visit('/') .then(() => { this.assertText('Application'); return this.transitionTo('blog.post'); }) .then(() => { return errorEntered.promise; }) .then(() => { this.assertText('ApplicationEngineError! Oh, noes!'); }); } ['@test error route works for a child route of an Engine'](assert) { assert.expect(2); let errorEntered = RSVP.defer(); this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register( 'route:post.error', Route.extend({ activate() { next(errorEntered.resolve); }, }) ); this.register('template:post.error', compile('Error! {{model.message}}')); this.register( 'route:post.comments', Route.extend({ model() { return RSVP.reject(new Error('Oh, noes!')); }, }) ); }); return this.visit('/') .then(() => { this.assertText('Application'); return this.transitionTo('blog.post.comments'); }) .then(() => { return errorEntered.promise; }) .then(() => { this.assertText('ApplicationEngineError! Oh, noes!'); }); } ['@test loading substate route works for the application route of an Engine'](assert) { assert.expect(3); let done = assert.async(); let loadingEntered = RSVP.defer(); let resolveLoading = RSVP.defer(); this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register( 'route:application_loading', Route.extend({ activate() { next(loadingEntered.resolve); }, }) ); this.register('template:application_loading', compile('Loading')); this.register('template:post', compile('Post')); this.register( 'route:post', Route.extend({ model() { return resolveLoading.promise; }, }) ); }); return this.visit('/').then(() => { this.assertText('Application'); let transition = this.transitionTo('blog.post'); loadingEntered.promise.then(() => { this.assertText('ApplicationLoading'); resolveLoading.resolve(); return this.runTaskNext().then(() => { this.assertText('ApplicationEnginePost'); done(); }); }); return transition; }); } ['@test loading route works for the application route of an Engine'](assert) { assert.expect(3); let done = assert.async(); let loadingEntered = RSVP.defer(); let resolveLoading = RSVP.defer(); this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register( 'route:loading', Route.extend({ activate() { next(loadingEntered.resolve); }, }) ); this.register('template:loading', compile('Loading')); this.register('template:post', compile('Post')); this.register( 'route:post', Route.extend({ model() { return resolveLoading.promise; }, }) ); }); return this.visit('/').then(() => { this.assertText('Application'); let transition = this.transitionTo('blog.post'); loadingEntered.promise.then(() => { this.assertText('ApplicationEngineLoading'); resolveLoading.resolve(); return this.runTaskNext().then(() => { this.assertText('ApplicationEnginePost'); done(); }); }); return transition; }); } ['@test loading substate route works for a child route of an Engine'](assert) { assert.expect(3); let resolveLoading; this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register('template:post', compile('{{outlet}}')); this.register('template:post.comments', compile('Comments')); this.register('template:post.likes_loading', compile('Loading')); this.register('template:post.likes', compile('Likes')); this.register( 'route:post.likes', Route.extend({ model() { return new RSVP.Promise(resolve => { resolveLoading = resolve; }); }, }) ); }); return this.visit('/blog/post/comments').then(() => { this.assertText('ApplicationEngineComments'); let transition = this.transitionTo('blog.post.likes'); this.runTaskNext().then(() => { this.assertText('ApplicationEngineLoading'); resolveLoading(); }); return transition .then(() => this.runTaskNext()) .then(() => this.assertText('ApplicationEngineLikes')); }); } ['@test loading route works for a child route of an Engine'](assert) { assert.expect(3); let done = assert.async(); let loadingEntered = RSVP.defer(); let resolveLoading = RSVP.defer(); this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register('template:post', compile('{{outlet}}')); this.register('template:post.comments', compile('Comments')); this.register( 'route:post.loading', Route.extend({ activate() { next(loadingEntered.resolve); }, }) ); this.register('template:post.loading', compile('Loading')); this.register('template:post.likes', compile('Likes')); this.register( 'route:post.likes', Route.extend({ model() { return resolveLoading.promise; }, }) ); }); return this.visit('/blog/post/comments').then(() => { this.assertText('ApplicationEngineComments'); let transition = this.transitionTo('blog.post.likes'); loadingEntered.promise.then(() => { this.assertText('ApplicationEngineLoading'); resolveLoading.resolve(); return this.runTaskNext().then(() => { this.assertText('ApplicationEngineLikes'); done(); }); }); return transition; }); } ["@test query params don't have stickiness by default between model"](assert) { assert.expect(1); let tmpl = '{{#link-to "blog.category" 1337}}Category 1337{{/link-to}}'; this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register('template:category', compile(tmpl)); }); return this.visit('/blog/category/1?type=news').then(() => { let suffix = '/blog/category/1337'; let href = this.element.querySelector('a').href; // check if link ends with the suffix assert.ok(this.stringsEndWith(href, suffix)); }); } ['@test query params in customized controllerName have stickiness by default between model']( assert ) { assert.expect(2); let tmpl = '{{#link-to "blog.author" 1337 class="author-1337"}}Author 1337{{/link-to}}{{#link-to "blog.author" 1 class="author-1"}}Author 1{{/link-to}}'; this.setupAppAndRoutableEngine(); this.additionalEngineRegistrations(function() { this.register('template:author', compile(tmpl)); }); return this.visit('/blog/author/1?official=true').then(() => { let suffix1 = '/blog/author/1?official=true'; let href1 = this.element.querySelector('.author-1').href; let suffix1337 = '/blog/author/1337'; let href1337 = this.element.querySelector('.author-1337').href; // check if link ends with the suffix assert.ok(this.stringsEndWith(href1, suffix1), `${href1} ends with ${suffix1}`); assert.ok(this.stringsEndWith(href1337, suffix1337), `${href1337} ends with ${suffix1337}`); }); } ['@test visit() routable engine which errors on init'](assert) { assert.expect(1); let hooks = []; this.additionalEngineRegistrations(function() { this.register( 'route:application', Route.extend({ init() { throw new Error('Whoops! Something went wrong...'); }, }) ); }); this.setupAppAndRoutableEngine(hooks); return this.visit('/', { shouldRender: true }) .then(() => { return this.visit('/blog'); }) .catch(error => { assert.equal(error.message, 'Whoops! Something went wrong...'); }); } } );