diff --git a/packages/@ember/-internals/routing/lib/services/router.ts b/packages/@ember/-internals/routing/lib/services/router.ts index f3aa549e9c9..14739d18fb7 100644 --- a/packages/@ember/-internals/routing/lib/services/router.ts +++ b/packages/@ember/-internals/routing/lib/services/router.ts @@ -1,4 +1,6 @@ +import { getOwner, Owner } from '@ember/-internals/owner'; import { Evented } from '@ember/-internals/runtime'; +import { symbol } from '@ember/-internals/utils'; import { assert } from '@ember/debug'; import { readOnly } from '@ember/object/computed'; import { assign } from '@ember/polyfills'; @@ -9,6 +11,8 @@ import { Transition } from 'router_js'; import EmberRouter, { QueryParam } from '../system/router'; import { extractRouteArgs, resemblesURL, shallowEqual } from '../utils'; +const ROUTER = symbol('ROUTER') as string; + let freezeRouteInfo: Function; if (DEBUG) { freezeRouteInfo = (transition: Transition) => { @@ -62,19 +66,30 @@ function cleanURL(url: string, rootURL: string) { @class RouterService */ export default class RouterService extends Service { - _router!: EmberRouter; + get _router(): EmberRouter { + let router = this[ROUTER]; + if (router !== undefined) { + return router; + } + const owner = getOwner(this) as Owner; + router = owner.lookup('router:main') as EmberRouter; + router.setupRouter(); + return (this[ROUTER] = router); + } + + constructor(owner: Owner) { + super(owner); - init() { - super.init(...arguments); + const router = owner.lookup('router:main') as EmberRouter; - this._router.on('routeWillChange', (transition: Transition) => { + router.on('routeWillChange', (transition: Transition) => { if (DEBUG) { freezeRouteInfo(transition); } this.trigger('routeWillChange', transition); }); - this._router.on('routeDidChange', (transition: Transition) => { + router.on('routeDidChange', (transition: Transition) => { if (DEBUG) { freezeRouteInfo(transition); } diff --git a/packages/@ember/-internals/routing/lib/system/router.ts b/packages/@ember/-internals/routing/lib/system/router.ts index 8e08c6ca085..f816378a7ed 100644 --- a/packages/@ember/-internals/routing/lib/system/router.ts +++ b/packages/@ember/-internals/routing/lib/system/router.ts @@ -127,6 +127,7 @@ class EmberRouter extends EmberObject { location!: string | IEmberLocation; rootURL!: string; _routerMicrolib!: Router; + _didSetupRouter = false; currentURL: string | null = null; currentRouteName: string | null = null; @@ -397,9 +398,8 @@ class EmberRouter extends EmberObject { @private */ startRouting() { - let initialURL = get(this, 'initialURL'); - if (this.setupRouter()) { + let initialURL = get(this, 'initialURL'); if (initialURL === undefined) { initialURL = get(this, 'location').getURL(); } @@ -411,6 +411,10 @@ class EmberRouter extends EmberObject { } setupRouter() { + if (this._didSetupRouter) { + return false; + } + this._didSetupRouter = true; this._setupLocation(); let location = get(this, 'location'); @@ -480,7 +484,9 @@ class EmberRouter extends EmberObject { this._toplevelView = OutletView.create(); this._toplevelView.setOutletState(liveRoutes as GlimmerOutletState); let instance: any = owner.lookup('-application-instance:main'); - instance.didCreateRootView(this._toplevelView); + if (instance) { + instance.didCreateRootView(this._toplevelView); + } } else { this._toplevelView.setOutletState(liveRoutes as GlimmerOutletState); } @@ -607,6 +613,7 @@ class EmberRouter extends EmberObject { @method reset */ reset() { + this._didSetupRouter = false; if (this._routerMicrolib) { this._routerMicrolib.reset(); } diff --git a/packages/@ember/-internals/routing/tests/system/router_test.js b/packages/@ember/-internals/routing/tests/system/router_test.js index 7d8716f8e78..544e8dc2fc2 100644 --- a/packages/@ember/-internals/routing/tests/system/router_test.js +++ b/packages/@ember/-internals/routing/tests/system/router_test.js @@ -61,6 +61,20 @@ moduleFor( assert.ok(!router._routerMicrolib); } + ['@test should create a router.js instance after setupRouter'](assert) { + let router = createRouter(undefined, { disableSetup: false }); + + assert.ok(router._didSetupRouter); + assert.ok(router._routerMicrolib); + } + + ['@test should return false if setupRouter is called multiple times'](assert) { + let router = createRouter(undefined, { disableSetup: true }); + + assert.ok(router.setupRouter()); + assert.notOk(router.setupRouter()); + } + ['@test should not reify location until setupRouter is called'](assert) { let router = createRouter(undefined, { disableSetup: true }); assert.equal(typeof router.location, 'string', 'location is specified as a string'); diff --git a/packages/@ember/application/instance.js b/packages/@ember/application/instance.js index ce37621baa5..4bfe80f66ae 100644 --- a/packages/@ember/application/instance.js +++ b/packages/@ember/application/instance.js @@ -159,7 +159,6 @@ const ApplicationInstance = EngineInstance.extend({ */ startRouting() { this.router.startRouting(); - this._didSetupRouter = true; }, /** @@ -168,23 +167,18 @@ const ApplicationInstance = EngineInstance.extend({ Because setup should only occur once, multiple calls to `setupRouter` beyond the first call have no effect. - + This is commonly used in order to confirm things that rely on the router are functioning properly from tests that are primarily rendering related. - + For example, from within [ember-qunit](https://github.com/emberjs/ember-qunit)'s `setupRenderingTest` calling `this.owner.setupRouter()` would allow that rendering test to confirm that any ``'s that are rendered have the correct URL. - + @public */ setupRouter() { - if (this._didSetupRouter) { - return; - } - this._didSetupRouter = true; - this.router.setupRouter(); }, diff --git a/packages/@ember/application/lib/application.js b/packages/@ember/application/lib/application.js index 13183a45ad2..eb5404e0bfa 100644 --- a/packages/@ember/application/lib/application.js +++ b/packages/@ember/application/lib/application.js @@ -1143,7 +1143,7 @@ Application.reopenClass({ }); function commonSetupRegistry(registry) { - registry.register('router:main', Router.extend()); + registry.register('router:main', Router); registry.register('-view-registry:main', { create() { return dictionary(null); @@ -1167,7 +1167,6 @@ function commonSetupRegistry(registry) { }); registry.register('service:router', RouterService); - registry.injection('service:router', '_router', 'router:main'); } function registerLibraries() { diff --git a/packages/ember/tests/routing/router_service_test/non_application_test_test.js b/packages/ember/tests/routing/router_service_test/non_application_test_test.js new file mode 100644 index 00000000000..ffaf14b3c10 --- /dev/null +++ b/packages/ember/tests/routing/router_service_test/non_application_test_test.js @@ -0,0 +1,106 @@ +import { inject as injectService } from '@ember/service'; +import { Router, NoneLocation } from '@ember/-internals/routing'; +import { get } from '@ember/-internals/metal'; +import { run } from '@ember/runloop'; +import { Component } from '@ember/-internals/glimmer'; +import { RouterNonApplicationTestCase, moduleFor } from 'internal-test-helpers'; + +moduleFor( + 'Router Service - non application test', + class extends RouterNonApplicationTestCase { + constructor() { + super(...arguments); + + this.resolver.add('router:main', Router.extend(this.routerOptions)); + this.router.map(function () { + this.route('parent', { path: '/' }, function () { + this.route('child'); + this.route('sister'); + this.route('brother'); + }); + this.route('dynamic', { path: '/dynamic/:dynamic_id' }); + this.route('dynamicWithChild', { path: '/dynamic-with-child/:dynamic_id' }, function () { + this.route('child', { path: '/:child_id' }); + }); + }); + } + + get routerOptions() { + return { + location: 'none', + }; + } + + get router() { + return this.owner.resolveRegistration('router:main'); + } + + get routerService() { + return this.owner.lookup('service:router'); + } + + ['@test RouterService can be instantiated in non application test'](assert) { + assert.ok(this.routerService); + } + + ['@test RouterService properties can be accessed with default'](assert) { + assert.expect(5); + assert.equal(this.routerService.get('currentRouteName'), null); + assert.equal(this.routerService.get('currentURL'), null); + assert.ok(this.routerService.get('location') instanceof NoneLocation); + assert.equal(this.routerService.get('rootURL'), '/'); + assert.equal(this.routerService.get('currentRoute'), null); + } + + ['@test RouterService#urlFor returns url'](assert) { + assert.equal(this.routerService.urlFor('parent.child'), '/child'); + } + + ['@test RouterService#transitionTo with basic route'](assert) { + assert.expect(2); + + let componentInstance; + + this.addTemplate('parent.index', '{{foo-bar}}'); + + this.addComponent('foo-bar', { + ComponentClass: Component.extend({ + routerService: injectService('router'), + init() { + this._super(...arguments); + componentInstance = this; + }, + actions: { + transitionToSister() { + get(this, 'routerService').transitionTo('parent.sister'); + }, + }, + }), + template: `foo-bar`, + }); + + this.render('{{foo-bar}}'); + + run(function () { + componentInstance.send('transitionToSister'); + }); + + assert.equal(this.routerService.get('currentRouteName'), 'parent.sister'); + assert.ok(this.routerService.isActive('parent.sister')); + } + + ['@test RouterService#recognize recognize returns routeInfo'](assert) { + let routeInfo = this.routerService.recognize('/dynamic-with-child/123/1?a=b'); + assert.ok(routeInfo); + let { name, localName, parent, child, params, queryParams, paramNames } = routeInfo; + assert.equal(name, 'dynamicWithChild.child'); + assert.equal(localName, 'child'); + assert.ok(parent); + assert.equal(parent.name, 'dynamicWithChild'); + assert.notOk(child); + assert.deepEqual(params, { child_id: '1' }); + assert.deepEqual(queryParams, { a: 'b' }); + assert.deepEqual(paramNames, ['child_id']); + } + } +); diff --git a/packages/internal-test-helpers/index.js b/packages/internal-test-helpers/index.js index 8691e514536..ca57769a58d 100644 --- a/packages/internal-test-helpers/index.js +++ b/packages/internal-test-helpers/index.js @@ -16,6 +16,7 @@ export { default as AbstractApplicationTestCase } from './lib/test-cases/abstrac export { default as ApplicationTestCase } from './lib/test-cases/application'; export { default as QueryParamTestCase } from './lib/test-cases/query-param'; export { default as RenderingTestCase } from './lib/test-cases/rendering'; +export { default as RouterNonApplicationTestCase } from './lib/test-cases/router-non-application'; export { default as RouterTestCase } from './lib/test-cases/router'; export { default as AutobootApplicationTestCase } from './lib/test-cases/autoboot-application'; export { default as DefaultResolverApplicationTestCase } from './lib/test-cases/default-resolver-application'; diff --git a/packages/internal-test-helpers/lib/test-cases/router-non-application.js b/packages/internal-test-helpers/lib/test-cases/router-non-application.js new file mode 100644 index 00000000000..f59a298f98a --- /dev/null +++ b/packages/internal-test-helpers/lib/test-cases/router-non-application.js @@ -0,0 +1,119 @@ +import { assign } from '@ember/polyfills'; +import { compile } from 'ember-template-compiler'; +import { EventDispatcher } from '@ember/-internals/views'; +import { Component, _resetRenderers } from '@ember/-internals/glimmer'; +import { ModuleBasedResolver } from '../test-resolver'; + +import AbstractTestCase from './abstract'; +import buildOwner from '../build-owner'; +import { runAppend, runDestroy } from '../run'; + +export default class RouterNonApplicationTestCase extends AbstractTestCase { + constructor() { + super(...arguments); + let bootOptions = this.getBootOptions(); + + let owner = (this.owner = buildOwner({ + ownerOptions: this.getOwnerOptions(), + resolver: this.getResolver(), + bootOptions, + })); + + owner.register('-view-registry:main', Object.create(null), { instantiate: false }); + owner.register('event_dispatcher:main', EventDispatcher); + + // TODO: why didn't buildOwner do this for us? + owner.inject('renderer', '_viewRegistry', '-view-registry:main'); + + this.renderer = this.owner.lookup('renderer:-dom'); + this.element = document.querySelector('#qunit-fixture'); + this.component = null; + } + + compile() { + return compile(...arguments); + } + + getOwnerOptions() {} + getBootOptions() {} + + get resolver() { + return this.owner.__registry__.fallback.resolver; + } + + getResolver() { + return new ModuleBasedResolver(); + } + + add(specifier, factory) { + this.resolver.add(specifier, factory); + } + + addTemplate(templateName, templateString) { + if (typeof templateName === 'string') { + this.resolver.add( + `template:${templateName}`, + this.compile(templateString, { + moduleName: templateName, + }) + ); + } else { + this.resolver.add( + templateName, + this.compile(templateString, { + moduleName: templateName.moduleName, + }) + ); + } + } + + addComponent(name, { ComponentClass = null, template = null }) { + if (ComponentClass) { + this.resolver.add(`component:${name}`, ComponentClass); + } + + if (typeof template === 'string') { + this.resolver.add( + `template:components/${name}`, + this.compile(template, { + moduleName: `components/${name}`, + }) + ); + } + } + + afterEach() { + try { + if (this.component) { + runDestroy(this.component); + } + if (this.owner) { + runDestroy(this.owner); + } + } finally { + _resetRenderers(); + } + } + + render(templateStr, context = {}) { + let { owner } = this; + + owner.register( + 'template:-top-level', + this.compile(templateStr, { + moduleName: '-top-level', + }) + ); + + let attrs = assign({}, context, { + tagName: '', + layoutName: '-top-level', + }); + + owner.register('component:-top-level', Component.extend(attrs)); + + this.component = owner.lookup('component:-top-level'); + + runAppend(this.component); + } +}