import { moduleFor, ApplicationTestCase } from 'internal-test-helpers'; import Controller, { inject as injectController } from '@ember/controller'; import { A as emberA } from '@ember/-internals/runtime'; import { alias } from '@ember/-internals/metal'; import { subscribe, reset } from '@ember/instrumentation'; import { Route, NoneLocation } from '@ember/-internals/routing'; import { EMBER_IMPROVED_INSTRUMENTATION } from '@ember/canary-features'; // IE includes the host name function normalizeUrl(url) { return url.replace(/https?:\/\/[^\/]+/, ''); } function shouldNotBeActive(assert, element) { checkActive(assert, element, false); } function shouldBeActive(assert, element) { checkActive(assert, element, true); } function checkActive(assert, element, active) { let classList = element.attr('class'); assert.equal(classList.indexOf('active') > -1, active, `${element} active should be ${active}`); } moduleFor( 'The {{link-to}} helper - basic tests', class extends ApplicationTestCase { constructor() { super(); this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'index' id='self-link'}}Self{{/link-to}} ` ); this.addTemplate( 'about', ` <h3 class="about">About</h3> {{#link-to 'index' id='home-link'}}Home{{/link-to}} {{#link-to 'about' id='self-link'}}Self{{/link-to}} ` ); } ['@test The {{link-to}} helper moves into the named route'](assert) { return this.visit('/') .then(() => { assert.equal(this.$('h3.home').length, 1, 'The home template was rendered'); assert.equal( this.$('#self-link.active').length, 1, 'The self-link was rendered with active class' ); assert.equal( this.$('#about-link:not(.active)').length, 1, 'The other link was rendered without active class' ); return this.click('#about-link'); }) .then(() => { assert.equal(this.$('h3.about').length, 1, 'The about template was rendered'); assert.equal( this.$('#self-link.active').length, 1, 'The self-link was rendered with active class' ); assert.equal( this.$('#home-link:not(.active)').length, 1, 'The other link was rendered without active class' ); }); } [`@test the {{link-to}} helper doesn't add an href when the tagName isn't 'a'`](assert) { this.addTemplate( 'index', ` {{#link-to 'about' id='about-link' tagName='div'}}About{{/link-to}} ` ); return this.visit('/').then(() => { assert.equal(this.$('#about-link').attr('href'), undefined, 'there is no href attribute'); }); } [`@test the {{link-to}} applies a 'disabled' class when disabled`](assert) { this.addTemplate( 'index', ` {{#link-to "about" id="about-link-static" disabledWhen="shouldDisable"}}About{{/link-to}} {{#link-to "about" id="about-link-dynamic" disabledWhen=dynamicDisabledWhen}}About{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ shouldDisable: true, dynamicDisabledWhen: 'shouldDisable', }) ); return this.visit('/').then(() => { assert.equal( this.$('#about-link-static.disabled').length, 1, 'The static link is disabled when its disabledWhen is true' ); assert.equal( this.$('#about-link-dynamic.disabled').length, 1, 'The dynamic link is disabled when its disabledWhen is true' ); let controller = this.applicationInstance.lookup('controller:index'); this.runTask(() => controller.set('dynamicDisabledWhen', false)); assert.equal( this.$('#about-link-dynamic.disabled').length, 0, 'The dynamic link is re-enabled when its disabledWhen becomes false' ); }); } [`@test the {{link-to}} doesn't apply a 'disabled' class if disabledWhen is not provided`]( assert ) { this.addTemplate('index', `{{#link-to "about" id="about-link"}}About{{/link-to}}`); return this.visit('/').then(() => { assert.ok( !this.$('#about-link').hasClass('disabled'), 'The link is not disabled if disabledWhen not provided' ); }); } [`@test the {{link-to}} helper supports a custom disabledClass`](assert) { this.addTemplate( 'index', ` {{#link-to "about" id="about-link" disabledWhen=true disabledClass="do-not-want"}}About{{/link-to}} ` ); return this.visit('/').then(() => { assert.equal( this.$('#about-link.do-not-want').length, 1, 'The link can apply a custom disabled class' ); }); } [`@test the {{link-to}} helper supports a custom disabledClass set via bound param`](assert) { this.addTemplate( 'index', ` {{#link-to "about" id="about-link" disabledWhen=true disabledClass=disabledClass}}About{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ disabledClass: 'do-not-want', }) ); return this.visit('/').then(() => { assert.equal( this.$('#about-link.do-not-want').length, 1, 'The link can apply a custom disabled class via bound param' ); }); } [`@test the {{link-to}} helper does not respond to clicks when disabledWhen`](assert) { this.addTemplate( 'index', ` {{#link-to "about" id="about-link" disabledWhen=true}}About{{/link-to}} ` ); return this.visit('/') .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal(this.$('h3.about').length, 0, 'Transitioning did not occur'); }); } [`@test the {{link-to}} helper does not respond to clicks when disabled`](assert) { this.addTemplate( 'index', ` {{#link-to "about" id="about-link" disabled=true}}About{{/link-to}} ` ); return this.visit('/') .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal(this.$('h3.about').length, 0, 'Transitioning did not occur'); }); } [`@test the {{link-to}} helper responds to clicks according to its disabledWhen bound param`]( assert ) { this.addTemplate( 'index', ` {{#link-to "about" id="about-link" disabledWhen=disabledWhen}}About{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ disabledWhen: true, }) ); return this.visit('/') .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal(this.$('h3.about').length, 0, 'Transitioning did not occur'); let controller = this.applicationInstance.lookup('controller:index'); controller.set('disabledWhen', false); return this.runLoopSettled(); }) .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal( this.$('h3.about').length, 1, 'Transitioning did occur when disabledWhen became false' ); }); } [`@test The {{link-to}} helper supports a custom activeClass`](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'index' id='self-link' activeClass='zomg-active'}}Self{{/link-to}} ` ); return this.visit('/').then(() => { assert.equal(this.$('h3.home').length, 1, 'The home template was rendered'); assert.equal( this.$('#self-link.zomg-active').length, 1, 'The self-link was rendered with active class' ); assert.equal( this.$('#about-link:not(.active)').length, 1, 'The other link was rendered without active class' ); }); } [`@test The {{link-to}} helper supports a custom activeClass from a bound param`](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'index' id='self-link' activeClass=activeClass}}Self{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ activeClass: 'zomg-active', }) ); return this.visit('/').then(() => { assert.equal(this.$('h3.home').length, 1, 'The home template was rendered'); assert.equal( this.$('#self-link.zomg-active').length, 1, 'The self-link was rendered with active class' ); assert.equal( this.$('#about-link:not(.active)').length, 1, 'The other link was rendered without active class' ); }); } [`@test The {{link-to}} helper supports 'classNameBindings' with custom values [GH #11699]`]( assert ) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link' classNameBindings='foo:foo-is-true:foo-is-false'}}About{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ foo: false, }) ); return this.visit('/').then(() => { assert.equal( this.$('#about-link.foo-is-false').length, 1, 'The about-link was rendered with the falsy class' ); let controller = this.applicationInstance.lookup('controller:index'); this.runTask(() => controller.set('foo', true)); assert.equal( this.$('#about-link.foo-is-true').length, 1, 'The about-link was rendered with the truthy class after toggling the property' ); }); } } ); moduleFor( 'The {{link-to}} helper - location hooks', class extends ApplicationTestCase { constructor() { super(); this.updateCount = 0; this.replaceCount = 0; let testContext = this; this.add( 'location:none', NoneLocation.extend({ setURL() { testContext.updateCount++; return this._super(...arguments); }, replaceURL() { testContext.replaceCount++; return this._super(...arguments); }, }) ); this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'index' id='self-link'}}Self{{/link-to}} ` ); this.addTemplate( 'about', ` <h3 class="about">About</h3> {{#link-to 'index' id='home-link'}}Home{{/link-to}} {{#link-to 'about' id='self-link'}}Self{{/link-to}} ` ); } visit() { return super.visit(...arguments).then(() => { this.updateCountAfterVisit = this.updateCount; this.replaceCountAfterVisit = this.replaceCount; }); } ['@test The {{link-to}} helper supports URL replacement'](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link' replace=true}}About{{/link-to}} ` ); return this.visit('/') .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal(this.updateCount, this.updateCountAfterVisit, 'setURL should not be called'); assert.equal( this.replaceCount, this.replaceCountAfterVisit + 1, 'replaceURL should be called once' ); }); } ['@test The {{link-to}} helper supports URL replacement via replace=boundTruthyThing'](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link' replace=boundTruthyThing}}About{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ boundTruthyThing: true, }) ); return this.visit('/') .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal(this.updateCount, this.updateCountAfterVisit, 'setURL should not be called'); assert.equal( this.replaceCount, this.replaceCountAfterVisit + 1, 'replaceURL should be called once' ); }); } ['@test The {{link-to}} helper supports setting replace=boundFalseyThing'](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link' replace=boundFalseyThing}}About{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ boundFalseyThing: false, }) ); return this.visit('/') .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal(this.updateCount, this.updateCountAfterVisit + 1, 'setURL should be called'); assert.equal( this.replaceCount, this.replaceCountAfterVisit, 'replaceURL should not be called' ); }); } } ); if (EMBER_IMPROVED_INSTRUMENTATION) { moduleFor( 'The {{link-to}} helper with EMBER_IMPROVED_INSTRUMENTATION', class extends ApplicationTestCase { constructor() { super(); this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'index' id='self-link'}}Self{{/link-to}} ` ); this.addTemplate( 'about', ` <h3 class="about">About</h3> {{#link-to 'index' id='home-link'}}Home{{/link-to}} {{#link-to 'about' id='self-link'}}Self{{/link-to}} ` ); } beforeEach() { return this.visit('/'); } afterEach() { reset(); return super.afterEach(); } ['@test The {{link-to}} helper fires an interaction event'](assert) { assert.expect(2); subscribe('interaction.link-to', { before() { assert.ok(true, 'instrumentation subscriber was called'); }, after() { assert.ok(true, 'instrumentation subscriber was called'); }, }); return this.click('#about-link'); } ['@test The {{link-to}} helper interaction event includes the route name'](assert) { assert.expect(2); subscribe('interaction.link-to', { before(name, timestamp, { routeName }) { assert.equal(routeName, 'about', 'instrumentation subscriber was passed route name'); }, after(name, timestamp, { routeName }) { assert.equal(routeName, 'about', 'instrumentation subscriber was passed route name'); }, }); return this.click('#about-link'); } ['@test The {{link-to}} helper interaction event includes the transition in the after hook']( assert ) { assert.expect(1); subscribe('interaction.link-to', { before() {}, after(name, timestamp, { transition }) { assert.equal( transition.targetName, 'about', 'instrumentation subscriber was passed route name' ); }, }); return this.click('#about-link'); } } ); } moduleFor( 'The {{link-to}} helper - nested routes and link-to arguments', class extends ApplicationTestCase { ['@test The {{link-to}} helper supports leaving off .index for nested routes'](assert) { this.router.map(function() { this.route('about', function() { this.route('item'); }); }); this.addTemplate('about', `<h1>About</h1>{{outlet}}`); this.addTemplate('about.index', `<div id='index'>Index</div>`); this.addTemplate('about.item', `<div id='item'>{{#link-to 'about'}}About{{/link-to}}</div>`); return this.visit('/about/item').then(() => { assert.equal(normalizeUrl(this.$('#item a').attr('href')), '/about'); }); } [`@test The {{link-to}} helper supports custom, nested, current-when`](assert) { this.router.map(function() { this.route('index', { path: '/' }, function() { this.route('about'); }); this.route('item'); }); this.addTemplate('index', `<h3 class="home">Home</h3>{{outlet}}`); this.addTemplate( 'index.about', ` {{#link-to 'item' id='other-link' current-when='index'}}ITEM{{/link-to}} ` ); return this.visit('/about').then(() => { assert.equal( this.$('#other-link.active').length, 1, 'The link is active since current-when is a parent route' ); }); } [`@test The {{link-to}} helper does not disregard current-when when it is given explicitly for a route`]( assert ) { this.router.map(function() { this.route('index', { path: '/' }, function() { this.route('about'); }); this.route('items', function() { this.route('item'); }); }); this.addTemplate('index', `<h3 class="home">Home</h3>{{outlet}}`); this.addTemplate( 'index.about', ` {{#link-to 'items' id='other-link' current-when='index'}}ITEM{{/link-to}} ` ); return this.visit('/about').then(() => { assert.equal( this.$('#other-link.active').length, 1, 'The link is active when current-when is given for explicitly for a route' ); }); } ['@test The {{link-to}} helper does not disregard current-when when it is set via a bound param']( assert ) { this.router.map(function() { this.route('index', { path: '/' }, function() { this.route('about'); }); this.route('items', function() { this.route('item'); }); }); this.add( 'controller:index.about', Controller.extend({ currentWhen: 'index', }) ); this.addTemplate('index', `<h3 class="home">Home</h3>{{outlet}}`); this.addTemplate( 'index.about', `{{#link-to 'items' id='other-link' current-when=currentWhen}}ITEM{{/link-to}}` ); return this.visit('/about').then(() => { assert.equal( this.$('#other-link.active').length, 1, 'The link is active when current-when is given for explicitly for a route' ); }); } ['@test The {{link-to}} helper supports multiple current-when routes'](assert) { this.router.map(function() { this.route('index', { path: '/' }, function() { this.route('about'); }); this.route('item'); this.route('foo'); }); this.addTemplate('index', `<h3 class="home">Home</h3>{{outlet}}`); this.addTemplate( 'index.about', `{{#link-to 'item' id='link1' current-when='item index'}}ITEM{{/link-to}}` ); this.addTemplate( 'item', `{{#link-to 'item' id='link2' current-when='item index'}}ITEM{{/link-to}}` ); this.addTemplate( 'foo', `{{#link-to 'item' id='link3' current-when='item index'}}ITEM{{/link-to}}` ); return this.visit('/about') .then(() => { assert.equal( this.$('#link1.active').length, 1, 'The link is active since current-when contains the parent route' ); return this.visit('/item'); }) .then(() => { assert.equal( this.$('#link2.active').length, 1, 'The link is active since you are on the active route' ); return this.visit('/foo'); }) .then(() => { assert.equal( this.$('#link3.active').length, 0, 'The link is not active since current-when does not contain the active route' ); }); } ['@test The {{link-to}} helper supports boolean values for current-when'](assert) { this.router.map(function() { this.route('index', { path: '/' }, function() { this.route('about'); }); this.route('item'); }); this.addTemplate( 'index.about', ` {{#link-to 'index' id='index-link' current-when=isCurrent}}index{{/link-to}} {{#link-to 'item' id='about-link' current-when=true}}ITEM{{/link-to}} ` ); this.add('controller:index.about', Controller.extend({ isCurrent: false })); return this.visit('/about').then(() => { assert.ok( this.$('#about-link').hasClass('active'), 'The link is active since current-when is true' ); assert.notOk( this.$('#index-link').hasClass('active'), 'The link is not active since current-when is false' ); let controller = this.applicationInstance.lookup('controller:index.about'); this.runTask(() => controller.set('isCurrent', true)); assert.ok( this.$('#index-link').hasClass('active'), 'The link is active since current-when is true' ); }); } ['@test The {{link-to}} helper defaults to bubbling'](assert) { this.addTemplate( 'about', ` <div {{action 'hide'}}> {{#link-to 'about.contact' id='about-contact'}}About{{/link-to}} </div> {{outlet}} ` ); this.addTemplate( 'about.contact', ` <h1 id='contact'>Contact</h1> ` ); this.router.map(function() { this.route('about', function() { this.route('contact'); }); }); let hidden = 0; this.add( 'route:about', Route.extend({ actions: { hide() { hidden++; }, }, }) ); return this.visit('/about') .then(() => { return this.click('#about-contact'); }) .then(() => { assert.equal(this.$('#contact').text(), 'Contact', 'precond - the link worked'); assert.equal(hidden, 1, 'The link bubbles'); }); } [`@test The {{link-to}} helper supports bubbles=false`](assert) { this.addTemplate( 'about', ` <div {{action 'hide'}}> {{#link-to 'about.contact' id='about-contact' bubbles=false}} About {{/link-to}} </div> {{outlet}} ` ); this.addTemplate('about.contact', `<h1 id='contact'>Contact</h1>`); this.router.map(function() { this.route('about', function() { this.route('contact'); }); }); let hidden = 0; this.add( 'route:about', Route.extend({ actions: { hide() { hidden++; }, }, }) ); return this.visit('/about') .then(() => { return this.click('#about-contact'); }) .then(() => { assert.equal(this.$('#contact').text(), 'Contact', 'precond - the link worked'); assert.equal(hidden, 0, "The link didn't bubble"); }); } [`@test The {{link-to}} helper supports bubbles=boundFalseyThing`](assert) { this.addTemplate( 'about', ` <div {{action 'hide'}}> {{#link-to 'about.contact' id='about-contact' bubbles=boundFalseyThing}} About {{/link-to}} </div> {{outlet}} ` ); this.addTemplate('about.contact', `<h1 id='contact'>Contact</h1>`); this.add( 'controller:about', Controller.extend({ boundFalseyThing: false, }) ); this.router.map(function() { this.route('about', function() { this.route('contact'); }); }); let hidden = 0; this.add( 'route:about', Route.extend({ actions: { hide() { hidden++; }, }, }) ); return this.visit('/about') .then(() => { return this.click('#about-contact'); }) .then(() => { assert.equal(this.$('#contact').text(), 'Contact', 'precond - the link worked'); assert.equal(hidden, 0, "The link didn't bubble"); }); } [`@test The {{link-to}} helper moves into the named route with context`](assert) { this.router.map(function() { this.route('about'); this.route('item', { path: '/item/:id' }); }); this.addTemplate( 'about', ` <h3 class="list">List</h3> <ul> {{#each model as |person|}} <li> {{#link-to 'item' person id=person.id}} {{person.name}} {{/link-to}} </li> {{/each}} </ul> {{#link-to 'index' id='home-link'}}Home{{/link-to}} ` ); this.addTemplate( 'item', ` <h3 class="item">Item</h3> <p>{{model.name}}</p> {{#link-to 'index' id='home-link'}}Home{{/link-to}} ` ); this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'about' id='about-link'}}About{{/link-to}} ` ); this.add( 'route:about', Route.extend({ model() { return [ { id: 'yehuda', name: 'Yehuda Katz' }, { id: 'tom', name: 'Tom Dale' }, { id: 'erik', name: 'Erik Brynroflsson' }, ]; }, }) ); return this.visit('/about') .then(() => { assert.equal(this.$('h3.list').length, 1, 'The home template was rendered'); assert.equal( normalizeUrl(this.$('#home-link').attr('href')), '/', 'The home link points back at /' ); return this.click('#yehuda'); }) .then(() => { assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); assert.equal(this.$('p').text(), 'Yehuda Katz', 'The name is correct'); return this.click('#home-link'); }) .then(() => { return this.click('#about-link'); }) .then(() => { assert.equal(normalizeUrl(this.$('li a#yehuda').attr('href')), '/item/yehuda'); assert.equal(normalizeUrl(this.$('li a#tom').attr('href')), '/item/tom'); assert.equal(normalizeUrl(this.$('li a#erik').attr('href')), '/item/erik'); return this.click('#erik'); }) .then(() => { assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); assert.equal(this.$('p').text(), 'Erik Brynroflsson', 'The name is correct'); }); } [`@test The {{link-to}} helper binds some anchor html tag common attributes`](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'index' id='self-link' title='title-attr' rel='rel-attr' tabindex='-1'}} Self {{/link-to}} ` ); return this.visit('/').then(() => { let link = this.$('#self-link'); assert.equal(link.attr('title'), 'title-attr', 'The self-link contains title attribute'); assert.equal(link.attr('rel'), 'rel-attr', 'The self-link contains rel attribute'); assert.equal(link.attr('tabindex'), '-1', 'The self-link contains tabindex attribute'); }); } [`@test The {{link-to}} helper supports 'target' attribute`](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'index' id='self-link' target='_blank'}}Self{{/link-to}} ` ); return this.visit('/').then(() => { let link = this.$('#self-link'); assert.equal(link.attr('target'), '_blank', 'The self-link contains `target` attribute'); }); } [`@test The {{link-to}} helper supports 'target' attribute specified as a bound param`]( assert ) { this.addTemplate( 'index', `<h3 class="home">Home</h3>{{#link-to 'index' id='self-link' target=boundLinkTarget}}Self{{/link-to}}` ); this.add( 'controller:index', Controller.extend({ boundLinkTarget: '_blank', }) ); return this.visit('/').then(() => { let link = this.$('#self-link'); assert.equal(link.attr('target'), '_blank', 'The self-link contains `target` attribute'); }); } [`@test the {{link-to}} helper calls preventDefault`](assert) { this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` {{#link-to 'about' id='about-link'}}About{{/link-to}} ` ); return this.visit('/').then(() => { assertNav({ prevented: true }, () => this.$('#about-link').click(), assert); }); } [`@test the {{link-to}} helper does not call preventDefault if 'preventDefault=false' is passed as an option`]( assert ) { this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` {{#link-to 'about' id='about-link' preventDefault=false}}About{{/link-to}} ` ); return this.visit('/').then(() => { assertNav({ prevented: false }, () => this.$('#about-link').trigger('click'), assert); }); } [`@test the {{link-to}} helper does not call preventDefault if 'preventDefault=boundFalseyThing' is passed as an option`]( assert ) { this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` {{#link-to 'about' id='about-link' preventDefault=boundFalseyThing}}About{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ boundFalseyThing: false, }) ); return this.visit('/').then(() => { assertNav({ prevented: false }, () => this.$('#about-link').trigger('click'), assert); }); } [`@test The {{link-to}} helper does not call preventDefault if 'target' attribute is provided`]( assert ) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'index' id='self-link' target='_blank'}}Self{{/link-to}} ` ); return this.visit('/').then(() => { assertNav({ prevented: false }, () => this.$('#self-link').click(), assert); }); } [`@test The {{link-to}} helper should preventDefault when 'target = _self'`](assert) { this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to 'index' id='self-link' target='_self'}}Self{{/link-to}} ` ); return this.visit('/').then(() => { assertNav({ prevented: true }, () => this.$('#self-link').click(), assert); }); } [`@test The {{link-to}} helper should not transition if target is not equal to _self or empty`]( assert ) { this.addTemplate( 'index', ` {{#link-to 'about' id='about-link' replace=true target='_blank'}} About {{/link-to}} ` ); this.router.map(function() { this.route('about'); }); return this.visit('/') .then(() => this.click('#about-link')) .then(() => { let currentRouteName = this.applicationInstance .lookup('controller:application') .get('currentRouteName'); assert.notEqual( currentRouteName, 'about', 'link-to should not transition if target is not equal to _self or empty' ); }); } [`@test The {{link-to}} helper accepts string/numeric arguments`](assert) { this.router.map(function() { this.route('filter', { path: '/filters/:filter' }); this.route('post', { path: '/post/:post_id' }); this.route('repo', { path: '/repo/:owner/:name' }); }); this.add( 'controller:filter', Controller.extend({ filter: 'unpopular', repo: { owner: 'ember', name: 'ember.js' }, post_id: 123, }) ); this.addTemplate( 'filter', ` <p>{{filter}}</p> {{#link-to "filter" "unpopular" id="link"}}Unpopular{{/link-to}} {{#link-to "filter" filter id="path-link"}}Unpopular{{/link-to}} {{#link-to "post" post_id id="post-path-link"}}Post{{/link-to}} {{#link-to "post" 123 id="post-number-link"}}Post{{/link-to}} {{#link-to "repo" repo id="repo-object-link"}}Repo{{/link-to}} ` ); return this.visit('/filters/popular').then(() => { assert.equal(normalizeUrl(this.$('#link').attr('href')), '/filters/unpopular'); assert.equal(normalizeUrl(this.$('#path-link').attr('href')), '/filters/unpopular'); assert.equal(normalizeUrl(this.$('#post-path-link').attr('href')), '/post/123'); assert.equal(normalizeUrl(this.$('#post-number-link').attr('href')), '/post/123'); assert.equal( normalizeUrl(this.$('#repo-object-link').attr('href')), '/repo/ember/ember.js' ); }); } [`@test Issue 4201 - Shorthand for route.index shouldn't throw errors about context arguments`]( assert ) { assert.expect(2); this.router.map(function() { this.route('lobby', function() { this.route('index', { path: ':lobby_id' }); this.route('list'); }); }); this.add( 'route:lobby.index', Route.extend({ model(params) { assert.equal(params.lobby_id, 'foobar'); return params.lobby_id; }, }) ); this.addTemplate( 'lobby.index', ` {{#link-to 'lobby' 'foobar' id='lobby-link'}}Lobby{{/link-to}} ` ); this.addTemplate( 'lobby.list', ` {{#link-to 'lobby' 'foobar' id='lobby-link'}}Lobby{{/link-to}} ` ); return this.visit('/lobby/list') .then(() => this.click('#lobby-link')) .then(() => shouldBeActive(assert, this.$('#lobby-link'))); } [`@test Quoteless route param performs property lookup`](assert) { this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` {{#link-to 'index' id='string-link'}}string{{/link-to}} {{#link-to foo id='path-link'}}path{{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ foo: 'index', }) ); let assertEquality = href => { assert.equal(normalizeUrl(this.$('#string-link').attr('href')), '/'); assert.equal(normalizeUrl(this.$('#path-link').attr('href')), href); }; return this.visit('/').then(() => { assertEquality('/'); let controller = this.applicationInstance.lookup('controller:index'); this.runTask(() => controller.set('foo', 'about')); assertEquality('/about'); }); } [`@test The {{link-to}} helper refreshes href element when one of params changes`](assert) { this.router.map(function() { this.route('post', { path: '/posts/:post_id' }); }); let post = { id: '1' }; let secondPost = { id: '2' }; this.addTemplate( 'index', ` {{#link-to "post" post id="post"}}post{{/link-to}} ` ); this.add('controller:index', Controller.extend()); return this.visit('/').then(() => { let indexController = this.applicationInstance.lookup('controller:index'); this.runTask(() => indexController.set('post', post)); assert.equal( normalizeUrl(this.$('#post').attr('href')), '/posts/1', 'precond - Link has rendered href attr properly' ); this.runTask(() => indexController.set('post', secondPost)); assert.equal( this.$('#post').attr('href'), '/posts/2', 'href attr was updated after one of the params had been changed' ); this.runTask(() => indexController.set('post', null)); assert.equal( this.$('#post').attr('href'), '#', 'href attr becomes # when one of the arguments in nullified' ); }); } [`@test The {{link-to}} helper is active when a route is active`](assert) { this.router.map(function() { this.route('about', function() { this.route('item'); }); }); this.addTemplate( 'about', ` <div id='about'> {{#link-to 'about' id='about-link'}}About{{/link-to}} {{#link-to 'about.item' id='item-link'}}Item{{/link-to}} {{outlet}} </div> ` ); return this.visit('/about') .then(() => { assert.equal(this.$('#about-link.active').length, 1, 'The about route link is active'); assert.equal(this.$('#item-link.active').length, 0, 'The item route link is inactive'); return this.visit('/about/item'); }) .then(() => { assert.equal(this.$('#about-link.active').length, 1, 'The about route link is active'); assert.equal(this.$('#item-link.active').length, 1, 'The item route link is active'); }); } [`@test The {{link-to}} helper works in an #each'd array of string route names`](assert) { this.router.map(function() { this.route('foo'); this.route('bar'); this.route('rar'); }); this.add( 'controller:index', Controller.extend({ routeNames: emberA(['foo', 'bar', 'rar']), route1: 'bar', route2: 'foo', }) ); this.addTemplate( 'index', ` {{#each routeNames as |routeName|}} {{#link-to routeName}}{{routeName}}{{/link-to}} {{/each}} {{#each routeNames as |r|}} {{#link-to r}}{{r}}{{/link-to}} {{/each}} {{#link-to route1}}a{{/link-to}} {{#link-to route2}}b{{/link-to}} ` ); let linksEqual = (links, expected) => { assert.equal(links.length, expected.length, 'Has correct number of links'); let idx; for (idx = 0; idx < links.length; idx++) { let href = this.$(links[idx]).attr('href'); // Old IE includes the whole hostname as well assert.equal( href.slice(-expected[idx].length), expected[idx], `Expected link to be '${expected[idx]}', but was '${href}'` ); } }; return this.visit('/').then(() => { linksEqual(this.$('a'), ['/foo', '/bar', '/rar', '/foo', '/bar', '/rar', '/bar', '/foo']); let indexController = this.applicationInstance.lookup('controller:index'); this.runTask(() => indexController.set('route1', 'rar')); linksEqual(this.$('a'), ['/foo', '/bar', '/rar', '/foo', '/bar', '/rar', '/rar', '/foo']); this.runTask(() => indexController.routeNames.shiftObject()); linksEqual(this.$('a'), ['/bar', '/rar', '/bar', '/rar', '/rar', '/foo']); }); } [`@test The non-block form {{link-to}} helper moves into the named route`](assert) { assert.expect(3); this.router.map(function() { this.route('contact'); }); this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{link-to 'Contact us' 'contact' id='contact-link'}} {{#link-to 'index' id='self-link'}}Self{{/link-to}} ` ); this.addTemplate( 'contact', ` <h3 class="contact">Contact</h3> {{link-to 'Home' 'index' id='home-link'}} {{link-to 'Self' 'contact' id='self-link'}} ` ); return this.visit('/') .then(() => { return this.click('#contact-link'); }) .then(() => { assert.equal(this.$('h3.contact').length, 1, 'The contact template was rendered'); assert.equal( this.$('#self-link.active').length, 1, 'The self-link was rendered with active class' ); assert.equal( this.$('#home-link:not(.active)').length, 1, 'The other link was rendered without active class' ); }); } [`@test The non-block form {{link-to}} helper updates the link text when it is a binding`]( assert ) { assert.expect(8); this.router.map(function() { this.route('contact'); }); this.add( 'controller:index', Controller.extend({ contactName: 'Jane', }) ); this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{link-to contactName 'contact' id='contact-link'}} {{#link-to 'index' id='self-link'}}Self{{/link-to}} ` ); this.addTemplate( 'contact', ` <h3 class="contact">Contact</h3> {{link-to 'Home' 'index' id='home-link'}} {{link-to 'Self' 'contact' id='self-link'}} ` ); return this.visit('/') .then(() => { assert.equal( this.$('#contact-link').text(), 'Jane', 'The link title is correctly resolved' ); let controller = this.applicationInstance.lookup('controller:index'); this.runTask(() => controller.set('contactName', 'Joe')); assert.equal( this.$('#contact-link').text(), 'Joe', 'The link title is correctly updated when the bound property changes' ); this.runTask(() => controller.set('contactName', 'Robert')); assert.equal( this.$('#contact-link').text(), 'Robert', 'The link title is correctly updated when the bound property changes a second time' ); return this.click('#contact-link'); }) .then(() => { assert.equal(this.$('h3.contact').length, 1, 'The contact template was rendered'); assert.equal( this.$('#self-link.active').length, 1, 'The self-link was rendered with active class' ); assert.equal( this.$('#home-link:not(.active)').length, 1, 'The other link was rendered without active class' ); return this.click('#home-link'); }) .then(() => { assert.equal(this.$('h3.home').length, 1, 'The index template was rendered'); assert.equal( this.$('#contact-link').text(), 'Robert', 'The link title is correctly updated when the route changes' ); }); } [`@test The non-block form {{link-to}} helper moves into the named route with context`]( assert ) { assert.expect(5); this.router.map(function() { this.route('item', { path: '/item/:id' }); }); this.add( 'route:index', Route.extend({ model() { return [ { id: 'yehuda', name: 'Yehuda Katz' }, { id: 'tom', name: 'Tom Dale' }, { id: 'erik', name: 'Erik Brynroflsson' }, ]; }, }) ); this.addTemplate( 'index', ` <h3 class="home">Home</h3> <ul> {{#each model as |person|}} <li> {{link-to person.name 'item' person id=person.id}} </li> {{/each}} </ul> ` ); this.addTemplate( 'item', ` <h3 class="item">Item</h3> <p>{{model.name}}</p> {{#link-to 'index' id='home-link'}}Home{{/link-to}} ` ); return this.visit('/') .then(() => { return this.click('#yehuda'); }) .then(() => { assert.equal(this.$('h3.item').length, 1, 'The item template was rendered'); assert.equal(this.$('p').text(), 'Yehuda Katz', 'The name is correct'); return this.click('#home-link'); }) .then(() => { assert.equal(normalizeUrl(this.$('li a#yehuda').attr('href')), '/item/yehuda'); assert.equal(normalizeUrl(this.$('li a#tom').attr('href')), '/item/tom'); assert.equal(normalizeUrl(this.$('li a#erik').attr('href')), '/item/erik'); }); } [`@test The non-block form {{link-to}} performs property lookup`](assert) { this.router.map(function() { this.route('about'); }); this.addTemplate( 'index', ` {{link-to 'string' 'index' id='string-link'}} {{link-to path foo id='path-link'}} ` ); this.add( 'controller:index', Controller.extend({ foo: 'index', }) ); return this.visit('/').then(() => { let assertEquality = href => { assert.equal(normalizeUrl(this.$('#string-link').attr('href')), '/'); assert.equal(normalizeUrl(this.$('#path-link').attr('href')), href); }; assertEquality('/'); let controller = this.applicationInstance.lookup('controller:index'); this.runTask(() => controller.set('foo', 'about')); assertEquality('/about'); }); } [`@test The non-block form {{link-to}} protects against XSS`](assert) { this.addTemplate('application', `{{link-to display 'index' id='link'}}`); this.add( 'controller:application', Controller.extend({ display: 'blahzorz', }) ); return this.visit('/').then(() => { assert.equal(this.$('#link').text(), 'blahzorz'); let controller = this.applicationInstance.lookup('controller:application'); this.runTask(() => controller.set('display', '<b>BLAMMO</b>')); assert.equal(this.$('#link').text(), '<b>BLAMMO</b>'); assert.equal(this.$('b').length, 0); }); } [`@test the {{link-to}} helper throws a useful error if you invoke it wrong`](assert) { assert.expect(1); this.router.map(function() { this.route('post', { path: 'post/:post_id' }); }); this.addTemplate('application', `{{#link-to 'post'}}Post{{/link-to}}`); assert.throws(() => { this.visit('/'); }, /(You attempted to define a `\{\{link-to "post"\}\}` but did not pass the parameters required for generating its dynamic segments.|You must provide param `post_id` to `generate`)/); return this.runLoopSettled(); } [`@test the {{link-to}} helper does not throw an error if its route has exited`](assert) { assert.expect(0); this.router.map(function() { this.route('post', { path: 'post/:post_id' }); }); this.addTemplate( 'application', ` {{#link-to 'index' id='home-link'}}Home{{/link-to}} {{#link-to 'post' defaultPost id='default-post-link'}}Default Post{{/link-to}} {{#if currentPost}} {{#link-to 'post' currentPost id='current-post-link'}}Current Post{{/link-to}} {{/if}} ` ); this.add( 'controller:application', Controller.extend({ defaultPost: { id: 1 }, postController: injectController('post'), currentPost: alias('postController.model'), }) ); this.add('controller:post', Controller.extend()); this.add( 'route:post', Route.extend({ model() { return { id: 2 }; }, serialize(model) { return { post_id: model.id }; }, }) ); return this.visit('/') .then(() => this.click('#default-post-link')) .then(() => this.click('#home-link')) .then(() => this.click('#current-post-link')) .then(() => this.click('#home-link')); } [`@test {{link-to}} active property respects changing parent route context`](assert) { this.router.map(function() { this.route('things', { path: '/things/:name' }, function() { this.route('other'); }); }); this.addTemplate( 'application', ` {{link-to 'OMG' 'things' 'omg' id='omg-link'}} {{link-to 'LOL' 'things' 'lol' id='lol-link'}} ` ); return this.visit('/things/omg') .then(() => { shouldBeActive(assert, this.$('#omg-link')); shouldNotBeActive(assert, this.$('#lol-link')); return this.visit('/things/omg/other'); }) .then(() => { shouldBeActive(assert, this.$('#omg-link')); shouldNotBeActive(assert, this.$('#lol-link')); }); } [`@test {{link-to}} populates href with default query param values even without query-params object`]( assert ) { this.add( 'controller:index', Controller.extend({ queryParams: ['foo'], foo: '123', }) ); this.addTemplate('index', `{{#link-to 'index' id='the-link'}}Index{{/link-to}}`); return this.visit('/').then(() => { assert.equal(this.$('#the-link').attr('href'), '/', 'link has right href'); }); } [`@test {{link-to}} populates href with default query param values with empty query-params object`]( assert ) { this.add( 'controller:index', Controller.extend({ queryParams: ['foo'], foo: '123', }) ); this.addTemplate( 'index', ` {{#link-to 'index' (query-params) id='the-link'}}Index{{/link-to}} ` ); return this.visit('/').then(() => { assert.equal(this.$('#the-link').attr('href'), '/', 'link has right href'); }); } [`@test {{link-to}} with only query-params and a block updates when route changes`](assert) { this.router.map(function() { this.route('about'); }); this.add( 'controller:application', Controller.extend({ queryParams: ['foo', 'bar'], foo: '123', bar: 'yes', }) ); this.addTemplate( 'application', ` {{#link-to (query-params foo='456' bar='NAW') id='the-link'}}Index{{/link-to}} ` ); return this.visit('/') .then(() => { assert.equal( this.$('#the-link').attr('href'), '/?bar=NAW&foo=456', 'link has right href' ); return this.visit('/about'); }) .then(() => { assert.equal( this.$('#the-link').attr('href'), '/about?bar=NAW&foo=456', 'link has right href' ); }); } [`@test Block-less {{link-to}} with only query-params updates when route changes`](assert) { this.router.map(function() { this.route('about'); }); this.add( 'controller:application', Controller.extend({ queryParams: ['foo', 'bar'], foo: '123', bar: 'yes', }) ); this.addTemplate( 'application', ` {{link-to "Index" (query-params foo='456' bar='NAW') id='the-link'}} ` ); return this.visit('/') .then(() => { assert.equal( this.$('#the-link').attr('href'), '/?bar=NAW&foo=456', 'link has right href' ); return this.visit('/about'); }) .then(() => { assert.equal( this.$('#the-link').attr('href'), '/about?bar=NAW&foo=456', 'link has right href' ); }); } [`@test The {{link-to}} helper can use dynamic params`](assert) { this.router.map(function() { this.route('foo', { path: 'foo/:some/:thing' }); this.route('bar', { path: 'bar/:some/:thing/:else' }); }); this.add( 'controller:index', Controller.extend({ init() { this._super(...arguments); this.dynamicLinkParams = ['foo', 'one', 'two']; }, }) ); this.addTemplate( 'index', ` <h3 class="home">Home</h3> {{#link-to params=dynamicLinkParams id="dynamic-link"}}Dynamic{{/link-to}} ` ); return this.visit('/').then(() => { let link = this.$('#dynamic-link'); assert.equal(link.attr('href'), '/foo/one/two'); let controller = this.applicationInstance.lookup('controller:index'); this.runTask(() => { controller.set('dynamicLinkParams', ['bar', 'one', 'two', 'three']); }); assert.equal(link.attr('href'), '/bar/one/two/three'); }); } [`@test GJ: {{link-to}} to a parent root model hook which performs a 'transitionTo' has correct active class #13256`]( assert ) { assert.expect(1); this.router.map(function() { this.route('parent', function() { this.route('child'); }); }); this.add( 'route:parent', Route.extend({ afterModel() { this.transitionTo('parent.child'); }, }) ); this.addTemplate( 'application', ` {{link-to 'Parent' 'parent' id='parent-link'}} ` ); return this.visit('/') .then(() => { return this.click('#parent-link'); }) .then(() => { shouldBeActive(assert, this.$('#parent-link')); }); } } ); moduleFor( 'The {{link-to}} helper - loading states and warnings', class extends ApplicationTestCase { [`@test link-to with null/undefined dynamic parameters are put in a loading state`](assert) { assert.expect(19); let warningMessage = 'This link-to is in an inactive loading state because at least one of its parameters presently has a null/undefined value, or the provided route name is invalid.'; this.router.map(function() { this.route('thing', { path: '/thing/:thing_id' }); this.route('about'); }); this.addTemplate( 'index', ` {{#link-to destinationRoute routeContext loadingClass='i-am-loading' id='context-link'}} string {{/link-to}} {{#link-to secondRoute loadingClass=loadingClass id='static-link'}} string {{/link-to}} ` ); this.add( 'controller:index', Controller.extend({ destinationRoute: null, routeContext: null, loadingClass: 'i-am-loading', }) ); this.add( 'route:about', Route.extend({ activate() { assert.ok(true, 'About was entered'); }, }) ); function assertLinkStatus(link, url) { if (url) { assert.equal(normalizeUrl(link.attr('href')), url, 'loaded link-to has expected href'); assert.ok(!link.hasClass('i-am-loading'), 'loaded linkComponent has no loadingClass'); } else { assert.equal(normalizeUrl(link.attr('href')), '#', "unloaded link-to has href='#'"); assert.ok(link.hasClass('i-am-loading'), 'loading linkComponent has loadingClass'); } } let contextLink, staticLink, controller; return this.visit('/') .then(() => { contextLink = this.$('#context-link'); staticLink = this.$('#static-link'); controller = this.applicationInstance.lookup('controller:index'); assertLinkStatus(contextLink); assertLinkStatus(staticLink); return expectWarning(() => { return this.click(contextLink[0]); }, warningMessage); }) .then(() => { // Set the destinationRoute (context is still null). this.runTask(() => controller.set('destinationRoute', 'thing')); assertLinkStatus(contextLink); // Set the routeContext to an id this.runTask(() => controller.set('routeContext', '456')); assertLinkStatus(contextLink, '/thing/456'); // Test that 0 isn't interpreted as falsy. this.runTask(() => controller.set('routeContext', 0)); assertLinkStatus(contextLink, '/thing/0'); // Set the routeContext to an object this.runTask(() => { controller.set('routeContext', { id: 123 }); }); assertLinkStatus(contextLink, '/thing/123'); // Set the destinationRoute back to null. this.runTask(() => controller.set('destinationRoute', null)); assertLinkStatus(contextLink); return expectWarning(() => { return this.click(staticLink[0]); }, warningMessage); }) .then(() => { this.runTask(() => controller.set('secondRoute', 'about')); assertLinkStatus(staticLink, '/about'); // Click the now-active link return this.click(staticLink[0]); }); } } ); function assertNav(options, callback, assert) { let nav = false; function check(event) { assert.equal( event.defaultPrevented, options.prevented, `expected defaultPrevented=${options.prevented}` ); nav = true; event.preventDefault(); } try { document.addEventListener('click', check); callback(); } finally { document.removeEventListener('click', check); assert.ok(nav, 'Expected a link to be clicked'); } }