AMBARI-20316. Move breadcrumbs to the separated view (onechiporenko)
Project: http://git-wip-us.apache.org/repos/asf/ambari/repo Commit: http://git-wip-us.apache.org/repos/asf/ambari/commit/f7e7c8bb Tree: http://git-wip-us.apache.org/repos/asf/ambari/tree/f7e7c8bb Diff: http://git-wip-us.apache.org/repos/asf/ambari/diff/f7e7c8bb Branch: refs/heads/branch-feature-AMBARI-12556 Commit: f7e7c8bb331d92997d547e6717ee1f3fdbefc6a8 Parents: 3d95dd6 Author: Oleg Nechiporenko <onechipore...@apache.org> Authored: Mon Mar 6 14:58:54 2017 +0200 Committer: Oleg Nechiporenko <onechipore...@apache.org> Committed: Mon Mar 6 16:30:14 2017 +0200 ---------------------------------------------------------------------- ambari-web/app/assets/test/tests.js | 1 + ambari-web/app/routes/main.js | 78 ++++++++ ambari-web/app/routes/view.js | 5 + ambari-web/app/routes/views.js | 7 + ambari-web/app/templates/application.hbs | 8 +- ambari-web/app/templates/common/breadcrumbs.hbs | 24 +++ ambari-web/app/views.js | 1 + ambari-web/app/views/application.js | 106 +---------- ambari-web/app/views/common/breadcrumbs_view.js | 190 +++++++++++++++++++ .../test/controllers/main/service/item_test.js | 19 +- .../test/views/common/breadcrumbs_view_test.js | 37 ++++ 11 files changed, 359 insertions(+), 117 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/assets/test/tests.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/assets/test/tests.js b/ambari-web/app/assets/test/tests.js index 308edcd..ab007f0 100644 --- a/ambari-web/app/assets/test/tests.js +++ b/ambari-web/app/assets/test/tests.js @@ -236,6 +236,7 @@ var files = [ 'test/utils/configs/modification_handlers/misc_test', 'test/utils/load_timer_test', 'test/utils/configs/theme/theme_test', + 'test/views/common/breadcrumbs_view_test', 'test/views/common/chart/linear_time_test', 'test/views/common/configs/widgets/combo_config_widget_view_test', 'test/views/common/configs/widgets/config_widget_view_test', http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/routes/main.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/routes/main.js b/ambari-web/app/routes/main.js index 4460442..4607033 100644 --- a/ambari-web/app/routes/main.js +++ b/ambari-web/app/routes/main.js @@ -18,7 +18,19 @@ var App = require('app'); +function getPostFormatLabel(parent) { + return function (label) { + return `${parent} - ${label}`; + } +} + module.exports = Em.Route.extend(App.RouterRedirections, { + + breadcrumbs: { + label: '<span class="glyphicon glyphicon-home"></span>', + route: 'dashboard' + }, + route: '/main', enter: function (router) { App.db.updateStorage(); @@ -111,6 +123,12 @@ module.exports = Em.Route.extend(App.RouterRedirections, { }), dashboard: Em.Route.extend({ + + breadcrumbs: { + label: Em.I18n.t('menu.item.dashboard'), + route: 'dashboard' + }, + route: '/dashboard', connectOutlets: function (router, context) { router.get('mainController').connectOutlet('mainDashboard'); @@ -191,6 +209,15 @@ module.exports = Em.Route.extend(App.RouterRedirections, { hosts: Em.Route.extend({ + + breadcrumbs: { + label: Em.I18n.t('menu.item.hosts'), + route: 'hosts', + beforeTransition() { + App.router.set('mainHostController.showFilterConditionsFirstLoad', false); + } + }, + route: '/hosts', index: Ember.Route.extend({ route: '/', @@ -202,6 +229,12 @@ module.exports = Em.Route.extend(App.RouterRedirections, { }), hostDetails: Em.Route.extend({ + + breadcrumbs: { + labelBindingPath: 'App.router.mainHostDetailsController.content.hostName', + disabled: true + }, + route: '/:host_id', connectOutlets: function (router, host) { router.get('mainHostController').set('showFilterConditionsFirstLoad', true); @@ -325,6 +358,15 @@ module.exports = Em.Route.extend(App.RouterRedirections, { hostAdd: require('routes/add_host_routes'), alerts: Em.Route.extend({ + + breadcrumbs: { + label: Em.I18n.t('menu.item.alerts'), + route: 'alerts', + beforeTransition() { + App.router.set('mainAlertDefinitionsController.showFilterConditionsFirstLoad', false); + } + }, + route: '/alerts', index: Em.Route.extend({ route: '/', @@ -335,6 +377,11 @@ module.exports = Em.Route.extend(App.RouterRedirections, { alertDetails: Em.Route.extend({ + breadcrumbs: { + labelBindingPath: 'App.router.mainAlertDefinitionDetailsController.content.label', + disabled: true + }, + route: '/:alert_definition_id', connectOutlets: function (router, alertDefinition) { @@ -401,6 +448,12 @@ module.exports = Em.Route.extend(App.RouterRedirections, { }), adminKerberos: Em.Route.extend({ + + breadcrumbs: { + label: Em.I18n.t('common.kerberos'), + labelPostFormat: getPostFormatLabel('Admin') + }, + route: '/kerberos', enter: function (router, transition) { if (router.get('loggedIn') && !App.isAuthorized('CLUSTER.TOGGLE_KERBEROS')) { @@ -519,6 +572,12 @@ module.exports = Em.Route.extend(App.RouterRedirections, { }), services: Em.Route.extend({ + + breadcrumbs: { + label: Em.I18n.t('admin.stackUpgrade.title'), + labelPostFormat: getPostFormatLabel('Admin') + }, + route: '/services', connectOutlets: function (router, context) { router.get('mainAdminStackAndUpgradeController').connectOutlet('mainAdminStackServices'); @@ -556,6 +615,12 @@ module.exports = Em.Route.extend(App.RouterRedirections, { } }), adminServiceAccounts: Em.Route.extend({ + + breadcrumbs: { + label: Em.I18n.t('common.serviceAccounts'), + labelPostFormat: getPostFormatLabel('Admin') + }, + route: '/serviceAccounts', enter: function (router, transition) { if (router.get('loggedIn') && !App.isAuthorized('SERVICE.SET_SERVICE_USERS_GROUPS')) { @@ -570,6 +635,12 @@ module.exports = Em.Route.extend(App.RouterRedirections, { }), adminServiceAutoStart: Em.Route.extend({ + + breadcrumbs: { + label: Em.I18n.t('admin.serviceAutoStart.title'), + labelPostFormat: getPostFormatLabel('Admin') + }, + route: '/serviceAutoStart', enter: function(router, transition) { if (router.get('loggedIn') && !App.isAuthorized('CLUSTER.MANAGE_AUTO_START') && !App.isAuthorized('CLUSTER.MANAGE_AUTO_START')) { @@ -653,6 +724,13 @@ module.exports = Em.Route.extend(App.RouterRedirections, { editWidget: require('routes/edit_widget'), services: Em.Route.extend({ + + breadcrumbs: { + labelBindingPath: 'App.router.mainServiceItemController.content.displayName', + labelPostFormat: getPostFormatLabel('Service'), + disabled: true + }, + route: '/services', index: Em.Route.extend({ route: '/', http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/routes/view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/routes/view.js b/ambari-web/app/routes/view.js index 50d8f4b..22a4057 100644 --- a/ambari-web/app/routes/view.js +++ b/ambari-web/app/routes/view.js @@ -19,6 +19,11 @@ var App = require('app'); module.exports = Em.Route.extend({ + + breadcrumbs: { + labelBindingPath: 'App.router.mainViewsDetailsController.content.label' + }, + route: '/view', enter: function (router) { Em.$('body').addClass('contribview'); http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/routes/views.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/routes/views.js b/ambari-web/app/routes/views.js index 88b1251..073507c 100644 --- a/ambari-web/app/routes/views.js +++ b/ambari-web/app/routes/views.js @@ -19,6 +19,13 @@ var App = require('app'); module.exports = Em.Route.extend({ + + breadcrumbs: { + transition() { + App.router.route('views'); + } + }, + route: '/views', enter: function (router) { router.get('mainViewsController').loadAmbariViews(); http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/templates/application.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/application.hbs b/ambari-web/app/templates/application.hbs index 9d6db78..9eab287 100644 --- a/ambari-web/app/templates/application.hbs +++ b/ambari-web/app/templates/application.hbs @@ -68,13 +68,7 @@ <nav class="navbar navbar-default navbar-static-top"> <div class="container main-container"> <div class="navbar-header navbar-nav"> - {{#if view.breadcrumbs.length}} - {{#each item in view.breadcrumbs}} - <span><a {{bindAttr class="item.disabled:disabled"}} {{action goToSection item.route target="view"}}> - {{{item.label}}}</a></span> - {{#unless item.lastItem}} / {{/unless}} - {{/each}} - {{/if}} + {{view App.BreadcrumbsView}} </div> {{! right offset. don't delete me! }} http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/templates/common/breadcrumbs.hbs ---------------------------------------------------------------------- diff --git a/ambari-web/app/templates/common/breadcrumbs.hbs b/ambari-web/app/templates/common/breadcrumbs.hbs new file mode 100644 index 0000000..89353d9 --- /dev/null +++ b/ambari-web/app/templates/common/breadcrumbs.hbs @@ -0,0 +1,24 @@ +{{! +* Licensed to the Apache Software Foundation (ASF) under one +* or more contributor license agreements. See the NOTICE file +* distributed with this work for additional information +* regarding copyright ownership. The ASF licenses this file +* to you under the Apache License, Version 2.0 (the +* "License"); you may not use this file except in compliance +* with the License. You may obtain a copy of the License at +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software +* distributed under the License is distributed on an "AS IS" BASIS, +* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +* See the License for the specific language governing permissions and +* limitations under the License. +}} + +{{#each item in view.items}} + <a {{bindAttr class="item.disabled:disabled"}} {{action moveTo item target="view"}}> + {{{item.formattedLabel}}} + </a> + {{#unless item.isLast}} / {{/unless}} +{{/each}} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/views.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views.js b/ambari-web/app/views.js index 7ec59f7..3ed1a37 100644 --- a/ambari-web/app/views.js +++ b/ambari-web/app/views.js @@ -20,6 +20,7 @@ // load all views here require('views/application'); +require('views/common/breadcrumbs_view'); require('views/common/clock_view'); require('views/common/checkbox_view'); require('views/common/log_search_ui_link_view'); http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/views/application.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/application.js b/ambari-web/app/views/application.js index ef9df6a..7a64584 100644 --- a/ambari-web/app/views/application.js +++ b/ambari-web/app/views/application.js @@ -22,111 +22,9 @@ var App = require('app'); App.ApplicationView = Em.View.extend({ templateName: require('templates/application'), - views: function () {⨠+ views: function () { return App.router.get('loggedIn') ? App.router.get('mainViewsController.visibleAmbariViews') : []; - â¨}.property('App.router.mainViewsController.visibleAmbariViews.[]', 'App.router.loggedIn'), - - /** - * Create the breadcrums showing on ambari top bar - * Eg, Home / Alerts / Metrics Monitor Status - * @returns {array} - */ - breadcrumbs: function () { - var breadcrumbs = []; - if (App.router.get('loggedIn')) { - var home = { - label: '<span class="glyphicon glyphicon-home"></span>', - route: 'dashboard', - disabled: false - }; - if (App.router.get('currentState.parentState.name') == 'dashboard') { - breadcrumbs.pushObject({ - label: '<span class="glyphicon glyphicon-home"></span> ' + Em.I18n.t('menu.item.dashboard'), - disabled: true, - lastItem: true - }); - } else if (App.router.get('currentState.parentState.name') == 'hosts') { - breadcrumbs.pushObject(home); - breadcrumbs.pushObject({ - label: Em.I18n.t('menu.item.hosts'), - disabled: true, - lastItem: true - }); - } else if (App.router.get('currentState.parentState.name') == 'hostDetails') { - var hostName = App.router.get('mainHostDetailsController.content.hostName'); - breadcrumbs.pushObject(home); - breadcrumbs.pushObject({ - label: Em.I18n.t('menu.item.hosts'), - route: 'hosts', - disabled: false - }); - breadcrumbs.pushObject({ - label: hostName, - disabled: true, - lastItem: true - }); - } else if (App.router.get('currentState.parentState.name') == 'alerts') { - breadcrumbs.pushObject(home); - if (App.router.get('currentState.name') == 'alertDetails') { - breadcrumbs.pushObject({ - label: Em.I18n.t('menu.item.alerts'), - route: 'alerts', - disabled: false - }); - breadcrumbs.pushObject({ - label: App.router.get('mainAlertDefinitionDetailsController.content.label'), - disabled: true, - lastItem: true - }); - } else { - breadcrumbs.pushObject({ - label: Em.I18n.t('menu.item.alerts'), - disabled: true, - lastItem: true - }); - } - } else if (App.router.get('currentState.parentState.name') == 'service') { - breadcrumbs.pushObject(home); - var serviceName = App.router.get('mainServiceItemController.content.displayName'); - breadcrumbs.pushObject({ - label: 'Service - ' + serviceName, - disabled: true, - lastItem: true - }); - } else if (App.router.get('currentState.parentState.name') == 'admin'|| App.router.get('currentState.parentState.parentState.name') == 'admin') { - breadcrumbs.pushObject(home); - breadcrumbs.pushObject({ - label: 'Admin - ' + App.router.get('mainAdminController.categoryLabel'), - disabled: true, - lastItem: true - }); - } else if (App.router.get('currentState.parentState.name') == 'views') { - breadcrumbs.pushObject(home); - breadcrumbs.pushObject({ - label: App.router.get('mainViewsDetailsController.content.label'), - disabled: true, - lastItem: true - }); - } - } - return breadcrumbs; - - }.property('App.router.loggedIn', 'App.router.currentState.parentState.name', - 'App.router.mainHostDetailsController.content.hostName', 'App.router.mainAlertDefinitionDetailsController.content.label', - 'App.router.mainServiceItemController.content.displayName', 'App.router.mainAdminController.categoryLabel', 'App.router.mainViewsDetailsController.content.label'), - - goToSection: function (event) { - if (!event.context) return; - if (event.context === 'hosts') { - App.router.set('mainHostController.showFilterConditionsFirstLoad', false); - } else if (event.context === 'views') { - App.router.route('views'); - return; - } else if (event.context === 'alerts') { - App.router.set('mainAlertDefinitionsController.showFilterConditionsFirstLoad', false); - } - App.router.route('main/' + event.context); - }, + }.property('App.router.mainViewsController.visibleAmbariViews.[]', 'App.router.loggedIn'), didInsertElement: function () { // on 'Enter' pressed, trigger modal window primary button if primary button is enabled(green) http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/app/views/common/breadcrumbs_view.js ---------------------------------------------------------------------- diff --git a/ambari-web/app/views/common/breadcrumbs_view.js b/ambari-web/app/views/common/breadcrumbs_view.js new file mode 100644 index 0000000..f9073ba --- /dev/null +++ b/ambari-web/app/views/common/breadcrumbs_view.js @@ -0,0 +1,190 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +var App = require('app'); + +function _getLabelPathWithoutApp(labelBindingPath) { + return labelBindingPath.startsWith('App.') ? labelBindingPath.replace('App.', '') : labelBindingPath; +} + +/** + * Don't create instances directly + * Only <code>App.BreadcrumbsView</code>-instance will create them + * + * @type {Em.Object} + */ +App.BreadcrumbItem = Em.Object.extend({ + + /** + * String shown as breadcrumb + * + * @type {string} + */ + label: '', + + /** + * Path to variable that will be used as breadcrumb + * If <code>labelBindingPath</code> is <code>'App.router.somePath'</code>, its value will be used + * + * @type {string} + */ + labelBindingPath: '', + + /** + * Determines if breadcrumb is disabled + * + * @type {boolean} + */ + disabled: false, + + /** + * Check if current breadcrumb is last + * + * @type {boolean} + */ + isLast: false, + + /** + * Move user to this route when click on breadcrumb item (don't add prefix <code>main</code>) + * + * @type {string} + */ + route: '', + + /** + * Hook executed before transition + * may be overridden when needed + * + * @type {Function} + */ + beforeTransition: Em.K, + + /** + * Hook executed after transition + * may be overridden when needed + * + * @type {Function} + */ + afterTransition: Em.K, + + /** + * Label shown on the page + * Result of <code>createLabel</code> execution + * + * @type {string} + */ + formattedLabel: '', + + /** + * Hook for label formatting + * It's executed after <code>label</code> or <code>labelBindingPath</code> is processed + * + * @param {string} label + * @returns {string} + */ + labelPostFormat: function (label) { + return label; + }, + + transition: function () { + return App.router.route('main/' + this.get('route')); + }, + + /** + * Generate <code>formattedLabel</code> shown on the page + * + * @method createLabel + */ + createLabel() { + let label = this.get('label'); + let labelBindingPath = this.get('labelBindingPath'); + + let formattedLabel = labelBindingPath ? App.get(_getLabelPathWithoutApp(labelBindingPath)) : label; + this.set('formattedLabel', this.labelPostFormat(formattedLabel)); + }, + + /** + * If <code>labelBindingPath</code> is provided, <code>createLabel</code> should observe value in path <code>${labelBindingPath}</code> + * + * @returns {*} + */ + init() { + let labelBindingPath = this.get('labelBindingPath'); + if (labelBindingPath) { + labelBindingPath = `App.${_getLabelPathWithoutApp(labelBindingPath)}`; + this.addObserver(labelBindingPath, this, 'createLabel'); + } + this.createLabel(); + return this._super(...arguments); + } + +}); + +/** + * Usage: + * <code>{{view App.BreadcrumbsView}}</code> + * + * @type {Em.View} + */ +App.BreadcrumbsView = Em.View.extend({ + + templateName: require('templates/common/breadcrumbs'), + + /** + * List of the breadcrumbs + * It's updated if <code>App.router.currentState</code> is changed. This happens when user is moved from one page to another + * + * @type {BreadcrumbItem[]} + */ + items: function () { + let currentState = App.get('router.currentState'); + let items = []; + while (currentState) { + if (currentState.breadcrumbs) { + items.pushObject(currentState.breadcrumbs); + } + currentState = currentState.get('parentState'); + } + items = items.reverse().map(item => App.BreadcrumbItem.extend(item).create()); + if (items.length) { + items.get('lastObject').setProperties({ + disabled: true, + isLast: true + }); + } + return items; + }.property('App.router.currentState'), + + /** + * Move user to the route described in the breadcrumb item + * <code>beforeTransition</code> hook is executed + * + * @param {{context: App.BreadcrumbItem}} event + * @returns {*} + */ + moveTo(event) { + let item = event.context; + if (!item || item.get('disabled')) { + return; + } + Em.tryInvoke(item, 'beforeTransition'); + Em.tryInvoke(item, 'transition'); + Em.tryInvoke(item, 'afterTransition'); + } + +}); \ No newline at end of file http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/test/controllers/main/service/item_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/controllers/main/service/item_test.js b/ambari-web/test/controllers/main/service/item_test.js index e33ecaa..c4a50ea 100644 --- a/ambari-web/test/controllers/main/service/item_test.js +++ b/ambari-web/test/controllers/main/service/item_test.js @@ -1926,8 +1926,10 @@ describe('App.MainServiceItemController', function () { describe('#applyRecommendedValues', function () { var controller; + var configsS1; + var configsS2; - beforeEach(function () { + beforeEach(function () { controller = App.MainServiceItemController.create({ stepConfigs: [ Em.Object.create({ @@ -1983,12 +1985,17 @@ describe('App.MainServiceItemController', function () { }); }); - it('should update properties with saveRecommended flag set to true', function () { + beforeEach(function () { controller.applyRecommendedValues(controller.get('stepConfigs')); - expect(controller.get('stepConfigs').findProperty('serviceName', 's1').get('configs').findProperty('name', 'p1').get('value')).to.equal('i1'); - expect(controller.get('stepConfigs').findProperty('serviceName', 's1').get('configs').findProperty('name', 'p2').get('value')).to.equal('r2'); - expect(controller.get('stepConfigs').findProperty('serviceName', 's2').get('configs').findProperty('name', 'p3').get('value')).to.equal('r3'); - expect(controller.get('stepConfigs').findProperty('serviceName', 's2').get('configs').findProperty('name', 'p4').get('value')).to.equal('v4'); + configsS1 = controller.get('stepConfigs').findProperty('serviceName', 's1').get('configs'); + configsS2 = controller.get('stepConfigs').findProperty('serviceName', 's2').get('configs'); + }); + + it('should update properties with saveRecommended flag set to true', function () { + expect(configsS1.findProperty('name', 'p1').get('value')).to.equal('i1'); + expect(configsS1.findProperty('name', 'p2').get('value')).to.equal('r2'); + expect(configsS2.findProperty('name', 'p3').get('value')).to.equal('r3'); + expect(configsS2.findProperty('name', 'p4').get('value')).to.equal('v4'); }); }); http://git-wip-us.apache.org/repos/asf/ambari/blob/f7e7c8bb/ambari-web/test/views/common/breadcrumbs_view_test.js ---------------------------------------------------------------------- diff --git a/ambari-web/test/views/common/breadcrumbs_view_test.js b/ambari-web/test/views/common/breadcrumbs_view_test.js new file mode 100644 index 0000000..751811c --- /dev/null +++ b/ambari-web/test/views/common/breadcrumbs_view_test.js @@ -0,0 +1,37 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +describe('App.BreadcrumbItem', function () { + + describe('#createLabel', function () { + + describe('#labelBindingPath', function () { + + beforeEach(function () { + this.breadcrumb = App.BreadcrumbItem.create({labelBindingPath: 'App.router.somePath'}); + }); + + it('Observer is added', function () { + expect(Em.meta(this.breadcrumb).listeners).to.have.property('App.router.somePath:change'); + }); + + }); + + }); + +}); \ No newline at end of file