From a14a37a078b55f17173937eb56c05d93d81adcf6 Mon Sep 17 00:00:00 2001 From: John Cowen Date: Fri, 22 Mar 2019 17:24:40 +0000 Subject: [PATCH] UI: Add support for blocking queries on the service instance detail page (#5487) This commit includes several pieces of functionality to enable services to be removed and the page to present information that this has happened but also keep the deleted information on the page. Along with the more usual blocking query based listing. To enable this: 1. Implements `meta` on the model (only available on collections in ember) 2. Adds new `catchable` ComputedProperty alongside a `listen` helper for working with specific errors that can be thrown from EventSources in an ember-like way. Briefly, normal computed properties update when a property changes, EventSources can additionally throw errors so we can catch them and show different visuals based on that. Also: Add support for blocking queries on the service instance detail page 1. Previous we could return undefined when a service instance has no proxy, but this means we have nothing to attach `meta` to. We've changed this to return an almost empty object, so with only a meta property. At first glance there doesn't seem to be any way to provide a proxy object to templates and be able to detect whether it is actually null or not so we instead change some conditional logic in the templates to detect the property we are using to generate the anchor. 2. Made a `pauseUntil` test helper function for steps where we wait for things. This helps for DRYness but also means if we can move away from setInterval to something else later, we can do it in one place 3. Whilst running into point 1 here, we managed to make the blocking queries eternally loop. Whilst this is due to an error in the code and shouldn't ever happen whilst in actual use, we've added an extra check so that we only recur/loop the blocking query if the previous response has a `meta.cursor` Adds support for blocking queries on the node detail page (#5489) 1. Moves data re-shaping for the templates variables into a repository so they are easily covered by blocking queries (into coordinatesRepo) 2. The node API returns a 404 as signal for deregistration, we also close the sessions and coordinates blocking queries when this happens --- ui-v2/app/controllers/dc/nodes/show.js | 24 ++++++++++++-- ui-v2/app/controllers/dc/services/instance.js | 20 ++++++++++-- .../app/instance-initializers/event-source.js | 19 +++++++++-- ui-v2/app/models/node.js | 1 + ui-v2/app/routes/dc/nodes/show.js | 32 ++++++------------- ui-v2/app/routes/dc/services/instance.js | 3 ++ ui-v2/app/services/repository/coordinate.js | 15 +++++++++ ui-v2/app/services/repository/node.js | 11 ------- ui-v2/app/services/repository/proxy.js | 17 ++++++---- ui-v2/app/services/repository/service.js | 1 + .../app/templates/dc/nodes/-notifications.hbs | 6 +++- ui-v2/app/templates/dc/nodes/show.hbs | 10 +++--- ui-v2/app/templates/dc/services/instance.hbs | 5 ++- .../tests/acceptance/dc/list-blocking.feature | 10 +++--- ui-v2/tests/acceptance/dc/nodes/show.feature | 32 +++++++++++++++---- .../dc/services/instances/show.feature | 21 ++++++++++-- .../services/repository/node-test.js | 4 +++ ui-v2/tests/steps.js | 20 ++++++++++-- ui-v2/tests/steps/assertions/dom.js | 14 +++++++- ui-v2/tests/steps/assertions/model.js | 27 ++++++---------- .../unit/controllers/dc/nodes/show-test.js | 2 +- .../controllers/dc/services/instance-test.js | 2 +- ui-v2/tests/unit/routes/dc/nodes/show-test.js | 1 + 23 files changed, 206 insertions(+), 91 deletions(-) diff --git a/ui-v2/app/controllers/dc/nodes/show.js b/ui-v2/app/controllers/dc/nodes/show.js index f90de09113..3dc82775e6 100644 --- a/ui-v2/app/controllers/dc/nodes/show.js +++ b/ui-v2/app/controllers/dc/nodes/show.js @@ -1,9 +1,14 @@ import Controller from '@ember/controller'; import { inject as service } from '@ember/service'; import { get, set, computed } from '@ember/object'; +import { alias } from '@ember/object/computed'; import WithSearching from 'consul-ui/mixins/with-searching'; -export default Controller.extend(WithSearching, { +import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source'; + +export default Controller.extend(WithEventSource, WithSearching, { dom: service('dom'), + notify: service('flashMessages'), + items: alias('item.Services'), queryParams: { s: { as: 'filter', @@ -16,6 +21,21 @@ export default Controller.extend(WithSearching, { }; this._super(...arguments); }, + item: listen('item').catch(function(e) { + if (e.target.readyState === 1) { + // OPEN + if (get(e, 'error.errors.firstObject.status') === '404') { + get(this, 'notify').add({ + destroyOnClick: false, + sticky: true, + type: 'warning', + action: 'update', + }); + get(this, 'tomography').close(); + get(this, 'sessions').close(); + } + } + }), searchable: computed('items', function() { return get(this, 'searchables.nodeservice') .add(get(this, 'items')) @@ -28,7 +48,7 @@ export default Controller.extend(WithSearching, { // This method is called immediately after `Route::setupController`, and done here rather than there // as this is a variable used purely for view level things, if the view was different we might not // need this variable - set(this, 'selectedTab', get(this.item, 'Checks.length') > 0 ? 'health-checks' : 'services'); + set(this, 'selectedTab', get(this, 'item.Checks.length') > 0 ? 'health-checks' : 'services'); }, actions: { change: function(e) { diff --git a/ui-v2/app/controllers/dc/services/instance.js b/ui-v2/app/controllers/dc/services/instance.js index a8934de52d..37847bb519 100644 --- a/ui-v2/app/controllers/dc/services/instance.js +++ b/ui-v2/app/controllers/dc/services/instance.js @@ -1,7 +1,10 @@ import Controller from '@ember/controller'; -import { set } from '@ember/object'; +import { get, set } from '@ember/object'; +import { inject as service } from '@ember/service'; +import WithEventSource, { listen } from 'consul-ui/mixins/with-event-source'; -export default Controller.extend({ +export default Controller.extend(WithEventSource, { + notify: service('flashMessages'), setProperties: function() { this._super(...arguments); // This method is called immediately after `Route::setupController`, and done here rather than there @@ -9,6 +12,19 @@ export default Controller.extend({ // need this variable set(this, 'selectedTab', 'service-checks'); }, + item: listen('item').catch(function(e) { + if (e.target.readyState === 1) { + // OPEN + if (get(e, 'error.errors.firstObject.status') === '404') { + get(this, 'notify').add({ + destroyOnClick: false, + sticky: true, + type: 'warning', + action: 'update', + }); + } + } + }), actions: { change: function(e) { set(this, 'selectedTab', e.target.value); diff --git a/ui-v2/app/instance-initializers/event-source.js b/ui-v2/app/instance-initializers/event-source.js index 01265dfec5..d2a707ee10 100644 --- a/ui-v2/app/instance-initializers/event-source.js +++ b/ui-v2/app/instance-initializers/event-source.js @@ -5,7 +5,7 @@ export function initialize(container) { if (config[enabled] || window.localStorage.getItem(enabled) !== null) { return; } - ['node', 'service'] + ['node', 'coordinate', 'session', 'service', 'proxy'] .map(function(item) { // create repositories that return a promise resolving to an EventSource return { @@ -20,7 +20,7 @@ export function initialize(container) { }) .concat([ // These are the routes where we overwrite the 'default' - // repo service. Default repos are repos that return a promise resovlving to + // repo service. Default repos are repos that return a promise resolving to // an ember-data record or recordset { route: 'dc/nodes/index', @@ -28,6 +28,14 @@ export function initialize(container) { repo: 'repository/node/event-source', }, }, + { + route: 'dc/nodes/show', + services: { + repo: 'repository/node/event-source', + coordinateRepo: 'repository/coordinate/event-source', + sessionRepo: 'repository/session/event-source', + }, + }, { route: 'dc/services/index', services: { @@ -40,6 +48,13 @@ export function initialize(container) { repo: 'repository/service/event-source', }, }, + { + route: 'dc/services/instance', + services: { + repo: 'repository/service/event-source', + proxyRepo: 'repository/proxy/event-source', + }, + }, ]) .forEach(function(definition) { if (typeof definition.extend !== 'undefined') { diff --git a/ui-v2/app/models/node.js b/ui-v2/app/models/node.js index cbe4272a58..be33ca7cf6 100644 --- a/ui-v2/app/models/node.js +++ b/ui-v2/app/models/node.js @@ -21,6 +21,7 @@ export default Model.extend({ Datacenter: attr('string'), Segment: attr(), Coord: attr(), + meta: attr(), hasStatus: function(status) { return hasStatus(get(this, 'Checks'), status); }, diff --git a/ui-v2/app/routes/dc/nodes/show.js b/ui-v2/app/routes/dc/nodes/show.js index 0449afc2df..ad7bf3b029 100644 --- a/ui-v2/app/routes/dc/nodes/show.js +++ b/ui-v2/app/routes/dc/nodes/show.js @@ -1,17 +1,14 @@ import Route from '@ember/routing/route'; import { inject as service } from '@ember/service'; import { hash } from 'rsvp'; -import { get, set } from '@ember/object'; +import { get } from '@ember/object'; -import distance from 'consul-ui/utils/distance'; -import tomographyFactory from 'consul-ui/utils/tomography'; import WithBlockingActions from 'consul-ui/mixins/with-blocking-actions'; -const tomography = tomographyFactory(distance); - export default Route.extend(WithBlockingActions, { repo: service('repository/node'), sessionRepo: service('repository/session'), + coordinateRepo: service('repository/coordinate'), queryParams: { s: { as: 'filter', @@ -20,24 +17,11 @@ export default Route.extend(WithBlockingActions, { }, model: function(params) { const dc = this.modelFor('dc').dc.Name; - const repo = get(this, 'repo'); - const sessionRepo = get(this, 'sessionRepo'); + const name = params.name; return hash({ - item: repo.findBySlug(params.name, dc), - }).then(function(model) { - // TODO: Consider loading this after initial page load - const coordinates = get(model.item, 'Coordinates'); - return hash({ - ...model, - ...{ - tomography: - get(coordinates, 'length') > 1 - ? tomography(params.name, coordinates.map(item => get(item, 'data'))) - : null, - items: get(model.item, 'Services'), - sessions: sessionRepo.findByNode(get(model.item, 'Node'), dc), - }, - }); + item: get(this, 'repo').findBySlug(name, dc), + tomography: get(this, 'coordinateRepo').findAllByNode(name, dc), + sessions: get(this, 'sessionRepo').findByNode(name, dc), }); }, setupController: function(controller, model) { @@ -52,7 +36,9 @@ export default Route.extend(WithBlockingActions, { const node = get(item, 'Node'); return repo.remove(item).then(() => { return repo.findByNode(node, dc).then(function(sessions) { - set(controller, 'sessions', sessions); + controller.setProperties({ + sessions: sessions, + }); }); }); }, 'delete'); diff --git a/ui-v2/app/routes/dc/services/instance.js b/ui-v2/app/routes/dc/services/instance.js index b4d0f1c5b9..2fb9eb30fd 100644 --- a/ui-v2/app/routes/dc/services/instance.js +++ b/ui-v2/app/routes/dc/services/instance.js @@ -13,6 +13,9 @@ export default Route.extend({ return hash({ item: repo.findInstanceBySlug(params.id, params.name, dc), }).then(function(model) { + // this will not be run in a blocking loop, but this is ok as + // its highly unlikely that a service will suddenly change to being a + // connect-proxy or vice versa so leave as is for now return hash({ proxy: get(model.item, 'Kind') === 'connect-proxy' diff --git a/ui-v2/app/services/repository/coordinate.js b/ui-v2/app/services/repository/coordinate.js index f6f5c1d8e6..75b3f2f1f7 100644 --- a/ui-v2/app/services/repository/coordinate.js +++ b/ui-v2/app/services/repository/coordinate.js @@ -1,8 +1,23 @@ +import { get } from '@ember/object'; import RepositoryService from 'consul-ui/services/repository'; +import tomographyFactory from 'consul-ui/utils/tomography'; +import distance from 'consul-ui/utils/distance'; +const tomography = tomographyFactory(distance); + const modelName = 'coordinate'; export default RepositoryService.extend({ getModelName: function() { return modelName; }, + findAllByNode: function(node, dc, configuration) { + return this.findAllByDatacenter(dc, configuration).then(function(coordinates) { + let results = {}; + if (get(coordinates, 'length') > 1) { + results = tomography(node, coordinates.map(item => get(item, 'data'))); + } + results.meta = get(coordinates, 'meta'); + return results; + }); + }, }); diff --git a/ui-v2/app/services/repository/node.js b/ui-v2/app/services/repository/node.js index 0d7eb57344..eec4f211d3 100644 --- a/ui-v2/app/services/repository/node.js +++ b/ui-v2/app/services/repository/node.js @@ -1,20 +1,9 @@ import RepositoryService from 'consul-ui/services/repository'; import { inject as service } from '@ember/service'; -import { get } from '@ember/object'; const modelName = 'node'; export default RepositoryService.extend({ coordinates: service('repository/coordinate'), getModelName: function() { return modelName; }, - findBySlug: function(slug, dc) { - return this._super(...arguments).then(node => { - return get(this, 'coordinates') - .findAllByDatacenter(dc) - .then(function(res) { - node.Coordinates = res; - return node; - }); - }); - }, }); diff --git a/ui-v2/app/services/repository/proxy.js b/ui-v2/app/services/repository/proxy.js index 41f1207b4e..6daeee8c54 100644 --- a/ui-v2/app/services/repository/proxy.js +++ b/ui-v2/app/services/repository/proxy.js @@ -1,6 +1,6 @@ import RepositoryService from 'consul-ui/services/repository'; import { PRIMARY_KEY } from 'consul-ui/models/proxy'; -import { get } from '@ember/object'; +import { get, set } from '@ember/object'; const modelName = 'proxy'; export default RepositoryService.extend({ getModelName: function() { @@ -21,17 +21,20 @@ export default RepositoryService.extend({ }, findInstanceBySlug: function(id, slug, dc, configuration) { return this.findAllBySlug(slug, dc, configuration).then(function(items) { + let res = {}; if (get(items, 'length') > 0) { let instance = items.findBy('ServiceProxy.DestinationServiceID', id); if (instance) { - return instance; - } - instance = items.findBy('ServiceProxy.DestinationServiceName', slug); - if (instance) { - return instance; + res = instance; + } else { + instance = items.findBy('ServiceProxy.DestinationServiceName', slug); + if (instance) { + res = instance; + } } } - return; + set(res, 'meta', get(items, 'meta')); + return res; }); }, }); diff --git a/ui-v2/app/services/repository/service.js b/ui-v2/app/services/repository/service.js index 3e42c84f56..472c6ae670 100644 --- a/ui-v2/app/services/repository/service.js +++ b/ui-v2/app/services/repository/service.js @@ -46,6 +46,7 @@ export default RepositoryService.extend({ service.NodeChecks = item.Nodes[i].Checks.filter(function(item) { return item.ServiceID == ''; }); + set(service, 'meta', get(item, 'meta')); return service; } // TODO: Add an store.error("404", "message") or similar diff --git a/ui-v2/app/templates/dc/nodes/-notifications.hbs b/ui-v2/app/templates/dc/nodes/-notifications.hbs index 70f90530b4..cbc36249f0 100644 --- a/ui-v2/app/templates/dc/nodes/-notifications.hbs +++ b/ui-v2/app/templates/dc/nodes/-notifications.hbs @@ -4,5 +4,9 @@ {{else}} There was an error invalidating the session. {{/if}} +{{else if (eq type 'update')}} + {{#if (eq status 'warning') }} + This node no longer exists in the catalog. + {{else}} + {{/if}} {{/if}} - diff --git a/ui-v2/app/templates/dc/nodes/show.hbs b/ui-v2/app/templates/dc/nodes/show.hbs index 10a12fc576..3a4d135ffe 100644 --- a/ui-v2/app/templates/dc/nodes/show.hbs +++ b/ui-v2/app/templates/dc/nodes/show.hbs @@ -18,7 +18,7 @@ (array 'Health Checks' 'Services' - (if tomography 'Round Trip Time' '') + (if tomography.distances 'Round Trip Time' '') 'Lock Sessions' ) ) @@ -48,10 +48,10 @@ {{#each (compact (array - (hash id=(slugify 'Health Checks') partial='dc/nodes/healthchecks') - (hash id=(slugify 'Services') partial='dc/nodes/services') - (if tomography (hash id=(slugify 'Round Trip Time') partial='dc/nodes/rtt') '') - (hash id=(slugify 'Lock Sessions') partial='dc/nodes/sessions') + (hash id=(slugify 'Health Checks') partial='dc/nodes/healthchecks') + (hash id=(slugify 'Services') partial='dc/nodes/services') + (if tomography.distances (hash id=(slugify 'Round Trip Time') partial='dc/nodes/rtt') '') + (hash id=(slugify 'Lock Sessions') partial='dc/nodes/sessions') ) ) as |panel| }} diff --git a/ui-v2/app/templates/dc/services/instance.hbs b/ui-v2/app/templates/dc/services/instance.hbs index 76a0b95d26..b5aae2b9ea 100644 --- a/ui-v2/app/templates/dc/services/instance.hbs +++ b/ui-v2/app/templates/dc/services/instance.hbs @@ -1,4 +1,7 @@ {{#app-view class="instance show"}} + {{#block-slot 'notification' as |status type|}} + {{partial 'dc/services/notifications'}} + {{/block-slot}} {{#block-slot 'breadcrumbs'}}
  1. All Services
  2. @@ -28,7 +31,7 @@
    Node Name
    {{item.Node.Node}}
    -{{#if proxy}} +{{#if proxy.ServiceName}}
    {{if proxy.ServiceProxy.DestinationServiceID "Sidecar " ""}}Proxy
    {{proxy.ServiceID}}
    diff --git a/ui-v2/tests/acceptance/dc/list-blocking.feature b/ui-v2/tests/acceptance/dc/list-blocking.feature index 21bb5ec990..7413ae8a7b 100644 --- a/ui-v2/tests/acceptance/dc/list-blocking.feature +++ b/ui-v2/tests/acceptance/dc/list-blocking.feature @@ -46,11 +46,9 @@ Feature: dc / list-blocking And an external edit results in 1 [Model] model And pause until I see 1 [Model] model And an external edit results in 0 [Model] models - And pause for 300 - And I see the text "deregistered" in "[data-notification]" + And pause until I see the text "deregistered" in "[data-notification]" Where: - ------------------------------------------------ - | Page | Model | Url | + ------------------------------------------------- + | Page | Model | Url | | service | instance | services/service-0 | - ------------------------------------------------ - + ------------------------------------------------- \ No newline at end of file diff --git a/ui-v2/tests/acceptance/dc/nodes/show.feature b/ui-v2/tests/acceptance/dc/nodes/show.feature index 2ea28bb3e2..21ca13a9ad 100644 --- a/ui-v2/tests/acceptance/dc/nodes/show.feature +++ b/ui-v2/tests/acceptance/dc/nodes/show.feature @@ -1,8 +1,9 @@ @setupApplicationTest Feature: dc / nodes / show: Show node - Scenario: Given 2 nodes all the tabs are visible and clickable + Background: Given 1 datacenter model with the value "dc1" - And 2 node models from yaml + Scenario: Given 2 nodes all the tabs are visible and clickable + Given 2 node models from yaml When I visit the node page for yaml --- dc: dc1 @@ -19,8 +20,7 @@ Feature: dc / nodes / show: Show node When I click lockSessions on the tabs And I see lockSessionsIsSelected on the tabs Scenario: Given 1 node all the tabs are visible and clickable and the RTT one isn't there - Given 1 datacenter model with the value "dc1" - And 1 node models from yaml + Given 1 node models from yaml --- ID: node-0 --- @@ -39,8 +39,7 @@ Feature: dc / nodes / show: Show node When I click lockSessions on the tabs And I see lockSessionsIsSelected on the tabs Scenario: Given 1 node with no checks all the tabs are visible but the Services tab is selected - Given 1 datacenter model with the value "dc1" - And 1 node models from yaml + Given 1 node models from yaml --- ID: node-0 Checks: [] @@ -55,3 +54,24 @@ Feature: dc / nodes / show: Show node And I see roundTripTime on the tabs And I see lockSessions on the tabs And I see servicesIsSelected on the tabs + Scenario: A node warns when deregistered whilst blocking + Given 1 node model from yaml + --- + ID: node-0 + --- + And settings from yaml + --- + consul:client: + blocking: 1 + throttle: 200 + --- + And a network latency of 100 + When I visit the node page for yaml + --- + dc: dc1 + node: node-0 + --- + Then the url should be /dc1/nodes/node-0 + And the url "/v1/internal/ui/node/node-0" responds with a 404 status + And pause until I see the text "no longer exists" in "[data-notification]" + diff --git a/ui-v2/tests/acceptance/dc/services/instances/show.feature b/ui-v2/tests/acceptance/dc/services/instances/show.feature index bf3f64971c..8547bf6615 100644 --- a/ui-v2/tests/acceptance/dc/services/instances/show.feature +++ b/ui-v2/tests/acceptance/dc/services/instances/show.feature @@ -1,6 +1,6 @@ @setupApplicationTest Feature: dc / services / instances / show: Show Service Instance - Scenario: A Service instance has no Proxy + Background: Given 1 datacenter model with the value "dc1" And 1 service model from yaml --- @@ -41,6 +41,7 @@ Feature: dc / services / instances / show: Show Service Instance DestinationServiceName: service-1 DestinationServiceID: ~ --- + Scenario: A Service instance has no Proxy When I visit the instance page for yaml --- dc: dc1 @@ -49,7 +50,6 @@ Feature: dc / services / instances / show: Show Service Instance --- Then the url should be /dc1/services/service-0/service-0-with-id Then I don't see type on the proxy - Then I see externalSource like "nomad" And I don't see upstreams on the tabs @@ -65,4 +65,21 @@ Feature: dc / services / instances / show: Show Service Instance Then I see the text "Tag1" in "[data-test-tags] span:nth-child(1)" Then I see the text "Tag2" in "[data-test-tags] span:nth-child(2)" + Scenario: A Service instance warns when deregistered whilst blocking + Given settings from yaml + --- + consul:client: + blocking: 1 + throttle: 200 + --- + And a network latency of 100 + When I visit the instance page for yaml + --- + dc: dc1 + service: service-0 + id: service-0-with-id + --- + Then the url should be /dc1/services/service-0/service-0-with-id + And an external edit results in 0 instance models + And pause until I see the text "deregistered" in "[data-notification]" diff --git a/ui-v2/tests/integration/services/repository/node-test.js b/ui-v2/tests/integration/services/repository/node-test.js index 8a6b4816da..d3086c04fc 100644 --- a/ui-v2/tests/integration/services/repository/node-test.js +++ b/ui-v2/tests/integration/services/repository/node-test.js @@ -55,6 +55,10 @@ test('findBySlug returns the correct data for item endpoint', function(assert) { return Object.assign({}, item, { Datacenter: dc, uid: `["${dc}","${item.ID}"]`, + meta: { + date: undefined, + cursor: undefined, + }, }); }) ); diff --git a/ui-v2/tests/steps.js b/ui-v2/tests/steps.js index cad75cceef..b115e0c0dd 100644 --- a/ui-v2/tests/steps.js +++ b/ui-v2/tests/steps.js @@ -21,6 +21,22 @@ export default function(assert, library, pages, utils) { return page; }; + const pauseUntil = function(cb) { + return new Promise(function(resolve, reject) { + let count = 0; + const interval = setInterval(function() { + if (++count >= 50) { + clearInterval(interval); + assert.ok(false); + reject(); + } + cb(function() { + clearInterval(interval); + resolve(); + }); + }, 100); + }); + }; models(library, utils.create); http(library, utils.respondWith, utils.set); visit(library, pages, setCurrentPage); @@ -28,9 +44,9 @@ export default function(assert, library, pages, utils) { form(library, utils.fillIn, utils.triggerKeyEvent, getCurrentPage); debug(library, assert, utils.currentURL); assertHttp(library, assert, utils.lastNthRequest); - assertModel(library, assert, getCurrentPage, utils.pluralize); + assertModel(library, assert, getCurrentPage, pauseUntil, utils.pluralize); assertPage(library, assert, getCurrentPage); - assertDom(library, assert, utils.find, utils.currentURL); + assertDom(library, assert, pauseUntil, utils.find, utils.currentURL); return library.given(["I'm using a legacy token"], function(number, model, data) { window.localStorage['consul:token'] = JSON.stringify({ AccessorID: null, SecretID: 'id' }); diff --git a/ui-v2/tests/steps/assertions/dom.js b/ui-v2/tests/steps/assertions/dom.js index 519ed3ab7a..12699d04bf 100644 --- a/ui-v2/tests/steps/assertions/dom.js +++ b/ui-v2/tests/steps/assertions/dom.js @@ -1,5 +1,17 @@ -export default function(scenario, assert, find, currentURL) { +export default function(scenario, assert, pauseUntil, find, currentURL) { scenario + .then('pause until I see the text "$text" in "$selector"', function(text, selector) { + return pauseUntil(function(resolve) { + const $el = find(selector); + if ($el) { + const hasText = $el.textContent.indexOf(text) !== -1; + if (hasText) { + assert.ok(hasText, `Expected to see "${text}" in "${selector}"`); + resolve(); + } + } + }); + }) .then(['I see the text "$text" in "$selector"'], function(text, selector) { assert.ok( find(selector).textContent.indexOf(text) !== -1, diff --git a/ui-v2/tests/steps/assertions/model.js b/ui-v2/tests/steps/assertions/model.js index 7019c086ea..c7d68bd33a 100644 --- a/ui-v2/tests/steps/assertions/model.js +++ b/ui-v2/tests/steps/assertions/model.js @@ -1,23 +1,14 @@ -export default function(scenario, assert, currentPage, pluralize) { +export default function(scenario, assert, currentPage, pauseUntil, pluralize) { scenario .then('pause until I see $number $model model[s]?', function(num, model) { - return new Promise(function(resolve) { - let count = 0; - const interval = setInterval(function() { - if (++count >= 50) { - clearInterval(interval); - assert.ok(false); - resolve(); - } - const len = currentPage()[pluralize(model)].filter(function(item) { - return item.isVisible; - }).length; - if (len === num) { - clearInterval(interval); - assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`); - resolve(); - } - }, 100); + return pauseUntil(function(resolve) { + const len = currentPage()[pluralize(model)].filter(function(item) { + return item.isVisible; + }).length; + if (len === num) { + assert.equal(len, num, `Expected ${num} ${model}s, saw ${len}`); + resolve(); + } }); }) .then(['I see $num $model model[s]?'], function(num, model) { diff --git a/ui-v2/tests/unit/controllers/dc/nodes/show-test.js b/ui-v2/tests/unit/controllers/dc/nodes/show-test.js index a1dd0c5618..d6f93489b6 100644 --- a/ui-v2/tests/unit/controllers/dc/nodes/show-test.js +++ b/ui-v2/tests/unit/controllers/dc/nodes/show-test.js @@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit'; moduleFor('controller:dc/nodes/show', 'Unit | Controller | dc/nodes/show', { // Specify the other units that are required for this test. - needs: ['service:search', 'service:dom'], + needs: ['service:search', 'service:dom', 'service:flashMessages'], }); // Replace this with your real tests. diff --git a/ui-v2/tests/unit/controllers/dc/services/instance-test.js b/ui-v2/tests/unit/controllers/dc/services/instance-test.js index 2b0693934f..7e4fc2ca75 100644 --- a/ui-v2/tests/unit/controllers/dc/services/instance-test.js +++ b/ui-v2/tests/unit/controllers/dc/services/instance-test.js @@ -2,7 +2,7 @@ import { moduleFor, test } from 'ember-qunit'; moduleFor('controller:dc/services/instance', 'Unit | Controller | dc/services/instance', { // Specify the other units that are required for this test. - // needs: ['controller:foo'] + needs: ['service:dom', 'service:flashMessages'], }); // Replace this with your real tests. diff --git a/ui-v2/tests/unit/routes/dc/nodes/show-test.js b/ui-v2/tests/unit/routes/dc/nodes/show-test.js index e6f486e6bc..4f512ba9aa 100644 --- a/ui-v2/tests/unit/routes/dc/nodes/show-test.js +++ b/ui-v2/tests/unit/routes/dc/nodes/show-test.js @@ -4,6 +4,7 @@ moduleFor('route:dc/nodes/show', 'Unit | Route | dc/nodes/show', { // Specify the other units that are required for this test. needs: [ 'service:repository/node', + 'service:repository/coordinate', 'service:repository/session', 'service:feedback', 'service:logger',