Merge pull request #7235 from hashicorp/ui-staging

ui: UI Release Merge (ui-staging merge)
This commit is contained in:
Kenia 2020-02-06 15:34:02 -05:00 committed by GitHub
commit cabc189b50
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 405 additions and 146 deletions

View File

@ -497,7 +497,6 @@ jobs:
environment:
EMBER_TEST_PARALLEL: true #enables test parallelization with ember-exam
EMBER_TEST_REPORT: test-results/report.xml #outputs test report for CircleCI test summary
parallelism: 4
steps:
- checkout
- restore_cache:
@ -506,7 +505,7 @@ jobs:
at: ui-v2
- run:
working_directory: ui-v2
command: node_modules/ember-cli/bin/ember exam --split=$CIRCLE_NODE_TOTAL --partition=`expr $CIRCLE_NODE_INDEX + 1` --path dist --silent -r xunit
command: make test-ci
- store_test_results:
path: ui-v2/test-results

View File

@ -38,6 +38,9 @@ start-api: deps
test: deps test-node
yarn run test
test-ci: deps test-node
yarn run test:ci
test-view: deps test-node
yarn run test:view

View File

@ -53,6 +53,7 @@ export default Adapter.extend({
requestForUpdateRecord: function(request, serialized, data) {
const params = {
...this.formatDatacenter(data[DATACENTER_KEY]),
flags: data.Flags,
...this.formatNspace(data[NSPACE_KEY]),
};
return request`

View File

@ -39,6 +39,7 @@ const MENU_ITEMS = '[role^="menuitem"]';
export default Component.extend({
tagName: '',
dom: service('dom'),
router: service('router'),
guid: '',
expanded: false,
orientation: 'vertical',
@ -47,6 +48,7 @@ export default Component.extend({
this._super(...arguments);
set(this, 'guid', this.dom.guid(this));
this._listeners = this.dom.listeners();
this._routelisteners = this.dom.listeners();
},
didInsertElement: function() {
// TODO: How do you detect whether the children have changed?
@ -54,10 +56,14 @@ export default Component.extend({
this.$menu = this.dom.element(`#${COMPONENT_ID}menu-${this.guid}`);
const labelledBy = this.$menu.getAttribute('aria-labelledby');
this.$trigger = this.dom.element(`#${labelledBy}`);
this._routelisteners.add(this.router, {
routeWillChange: () => this.actions.close.apply(this, [{}]),
});
},
willDestroyElement: function() {
this._super(...arguments);
this._listeners.remove();
this._routelisteners.remove();
},
actions: {
keypressClick: function(e) {

View File

@ -31,11 +31,13 @@ export default Component.extend({
this._super(...arguments);
this._viewportlistener.add(
this.dom.isInViewport(this.element, bool => {
set(this, 'isDisplayed', bool);
if (this.isDisplayed) {
this.addPathListeners();
} else {
this.ticker.destroy(this);
if (get(this, 'isDisplayed') !== bool) {
set(this, 'isDisplayed', bool);
if (this.isDisplayed) {
this.addPathListeners();
} else {
this.ticker.destroy(this);
}
}
})
);
@ -63,24 +65,29 @@ export default Component.extend({
!routes.find(item => get(item, 'Definition.Match.HTTP.PathPrefix') === '/') &&
!routes.find(item => typeof item.Definition === 'undefined')
) {
let nextNode = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`;
const splitterID = `splitter:${this.chain.ServiceName}`;
if (typeof this.chain.Nodes[splitterID] !== 'undefined') {
let nextNode;
const resolverID = `resolver:${this.chain.ServiceName}.${this.chain.Namespace}.${this.chain.Datacenter}`;
const splitterID = `splitter:${this.chain.ServiceName}.${this.chain.Namespace}`;
if (typeof this.chain.Nodes[resolverID] !== 'undefined') {
nextNode = resolverID;
} else if (typeof this.chain.Nodes[splitterID] !== 'undefined') {
nextNode = splitterID;
}
routes.push({
Default: true,
ID: `route:${this.chain.ServiceName}`,
Name: this.chain.ServiceName,
Definition: {
Match: {
HTTP: {
PathPrefix: '/',
if (typeof nextNode !== 'undefined') {
routes.push({
Default: true,
ID: `route:${this.chain.ServiceName}`,
Name: this.chain.ServiceName,
Definition: {
Match: {
HTTP: {
PathPrefix: '/',
},
},
},
},
NextNode: nextNode,
});
NextNode: nextNode,
});
}
}
return routes;
}),
@ -92,23 +99,17 @@ export default Component.extend({
get(this, 'chain.Nodes')
);
}),
graph: computed('chain.Nodes', function() {
graph: computed('splitters', 'routes', function() {
const graph = this.dataStructs.graph();
const router = this.chain.ServiceName;
Object.entries(get(this, 'chain.Nodes')).forEach(([key, item]) => {
switch (item.Type) {
case 'splitter':
item.Splits.forEach(splitter => {
graph.addLink(item.ID, splitter.NextNode);
});
break;
case 'router':
item.Routes.forEach((route, i) => {
route = createRoute(route, router, this.dom.guid);
graph.addLink(route.ID, route.NextNode);
});
break;
}
this.splitters.forEach(item => {
item.Splits.forEach(splitter => {
graph.addLink(item.ID, splitter.NextNode);
});
});
this.routes.forEach((route, i) => {
route = createRoute(route, router, this.dom.guid);
graph.addLink(route.ID, route.NextNode);
});
return graph;
}),

View File

@ -4,12 +4,6 @@ import { get, computed } from '@ember/object';
export default Component.extend({
tagName: '',
path: computed('item', function() {
if (get(this, 'item.Default')) {
return {
type: 'Default',
value: '/',
};
}
return Object.entries(get(this, 'item.Definition.Match.HTTP') || {}).reduce(
function(prev, [key, value]) {
if (key.toLowerCase().startsWith('path')) {

View File

@ -1,6 +1,7 @@
import Route from '@ember/routing/route';
import { inject as service } from '@ember/service';
import { hash } from 'rsvp';
import { get } from '@ember/object';
export default Route.extend({
repo: service('repository/service'),
@ -17,9 +18,15 @@ export default Route.extend({
const nspace = this.modelFor('nspace').nspace.substr(1);
return hash({
item: this.repo.findBySlug(params.name, dc, nspace),
chain: this.chainRepo.findBySlug(params.name, dc, nspace),
urls: this.settings.findBySlug('urls'),
dc: dc,
}).then(model => {
return hash({
chain: ['connect-proxy', 'mesh-gateway'].includes(get(model, 'item.Service.Kind'))
? null
: this.chainRepo.findBySlug(params.name, dc, nspace),
...model,
});
});
},
setupController: function(controller, model) {

View File

@ -4,6 +4,9 @@ export default function(filterable) {
const sLower = s.toLowerCase();
return (
get(item, 'Node')
.toLowerCase()
.indexOf(sLower) !== -1 ||
get(item, 'Address')
.toLowerCase()
.indexOf(sLower) !== -1
);

View File

@ -65,8 +65,13 @@
padding: 10px;
padding-left: 36px;
}
/* here the !important is only needed for what seems to be a difference */
/* with the CSS before and after compression */
/* i.e. before compression this style is applied */
/* after compression it is in the source but doesn't seem to get */
/* applied (unless you add the !important) */
%menu-panel .is-active {
position: relative;
position: relative !important;
}
%menu-panel .is-active > *::after {
position: absolute;

View File

@ -1,5 +1,5 @@
{{!<form>}}
{{freetext-filter searchable=searchable value=search placeholder="Search by name"}}
{{freetext-filter searchable=searchable value=search placeholder="Search"}}
{{radio-group keyboardAccess=true name="status" value=status items=(array
(hash label='All (Any Status)' value='' )
(hash label='Critical Checks' value='critical')

View File

@ -12,7 +12,7 @@
{{#if dc}}
<ul>
{{#if (and (env 'CONSUL_NSPACES_ENABLED') (gt nspaces.length 0))}}
<li>
<li data-test-nspace-menu>
{{#if (and (eq nspaces.length 1) (not canManageNspaces)) }}
<span data-test-nspace-selected={{nspace.Name}}>{{nspace.Name}}</span>
{{ else }}

View File

@ -1,6 +1,6 @@
{{yield (concat 'popover-menu-' guid)}}
{{#aria-menu keyboardAccess=keyboardAccess as |change keypress ariaLabelledBy ariaControls ariaExpanded keypressClick|}}
{{#toggle-button checked=expanded onchange=(queue change (action 'change')) as |click|}}
{{#toggle-button checked=ariaExpanded onchange=(queue change (action 'change')) as |click|}}
<button type="button" aria-haspopup="menu" onkeydown={{keypress}} onclick={{click}} id={{ariaLabelledBy}} aria-controls={{ariaControls}}>
{{#yield-slot name='trigger'}}
{{yield}}

View File

@ -12,9 +12,7 @@
{{path.type}}
</dt>
<dd>
{{#if (not-eq path.type 'Default')}}
{{path.value}}
{{/if}}
</dd>
</dl>
</header>

View File

@ -1,59 +1,59 @@
{{title item.Service.Service}}
{{#app-view class="service show"}}
{{#block-slot name='notification' as |status type|}}
{{partial 'dc/services/notifications'}}
{{/block-slot}}
{{#block-slot name='breadcrumbs'}}
<ol>
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
</ol>
{{/block-slot}}
{{#block-slot name='header'}}
<h1>
{{ item.Service.Service }}
{{#with (service/external-source item.Service) as |externalSource| }}
{{#with (css-var (concat '--' externalSource '-color-svg') 'none') as |bg| }}
{{#if (not-eq bg 'none') }}
<span data-test-external-source="{{externalSource}}" style={{{ concat 'background-image:' bg }}} data-tooltip="Registered via {{externalSource}}">Registered via {{externalSource}}</span>
{{/if}}
{{/with}}
{{#block-slot name='notification' as |status type|}}
{{partial 'dc/services/notifications'}}
{{/block-slot}}
{{#block-slot name='breadcrumbs'}}
<ol>
<li><a data-test-back href={{href-to 'dc.services'}}>All Services</a></li>
</ol>
{{/block-slot}}
{{#block-slot name='header'}}
<h1>
{{item.Service.Service}}
{{#with (service/external-source item.Service) as |externalSource|}}
{{#with (css-var (concat '--' externalSource '-color-svg') 'none') as |bg|}}
{{#if (not-eq bg 'none')}}
<span data-test-external-source={{externalSource}} style={{{concat 'background-image:' bg}}} data-tooltip="Registered via {{externalSource}}">Registered via {{externalSource}}</span>
{{/if}}
{{/with}}
{{/with}}
{{#if (eq item.Service.Kind 'connect-proxy')}}
<span class="kind-proxy">Proxy</span>
<span class="kind-proxy">Proxy</span>
{{else if (eq item.Service.Kind 'mesh-gateway')}}
<span class="kind-proxy">Mesh Gateway</span>
<span class="kind-proxy">Mesh Gateway</span>
{{/if}}
</h1>
<label for="toolbar-toggle"></label>
{{tab-nav
items=(compact
(array
'Instances'
'Routing'
'Tags'
)
)
selected=selectedTab
}}
{{/block-slot}}
{{#block-slot name='actions'}}
{{#if urls.service}}
{{#templated-anchor data-test-dashboard-anchor href=urls.service vars=(hash Datacenter=dc Service=(hash Name=item.Service.Service)) rel="external"}}Open Dashboard{{/templated-anchor}}
{{/if}}
{{/block-slot}}
{{#block-slot name='content'}}
{{#each
(compact
(array
(hash id=(slugify 'Instances') partial='dc/services/instances')
(hash id=(slugify 'Routing') partial='dc/services/routing')
(hash id=(slugify 'Tags') partial='dc/services/tags')
)
) as |panel|
}}
{{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action "change")}}
{{partial panel.partial}}
{{/tab-section}}
{{/each}}
{{/block-slot}}
</h1>
<label for="toolbar-toggle"></label>
{{tab-nav
items=(compact
(array
'Instances'
(if (not-eq chain null) 'Routing' '')
'Tags'
)
)
selected=selectedTab
}}
{{/block-slot}}
{{#block-slot name='actions'}}
{{#if urls.service}}
{{#templated-anchor data-test-dashboard-anchor href=urls.service vars=(hash Datacenter=dc Service=(hash Name=item.Service.Service)) rel="external"}}Open Dashboard{{/templated-anchor}}
{{/if}}
{{/block-slot}}
{{#block-slot name='content'}}
{{#each
(compact
(array
(hash id=(slugify 'Instances') partial='dc/services/instances')
(if (not-eq chain null) (hash id=(slugify 'Routing') partial='dc/services/routing') '')
(hash id=(slugify 'Tags') partial='dc/services/tags')
)
) as |panel|
}}
{{#tab-section id=panel.id selected=(eq (if selectedTab selectedTab '') panel.id) onchange=(action 'change')}}
{{partial panel.partial}}
{{/tab-section}}
{{/each}}
{{/block-slot}}
{{/app-view}}

View File

@ -26,7 +26,7 @@
</label>
</fieldset>
{{#if (not (env 'CONSUL_UI_DISABLE_REALTIME'))}}
<fieldset>
<fieldset data-test-blocking-queries>
<h2>Blocking Queries</h2>
<p>Keep catalog info up-to-date without refreshing the page. Any changes made to services, nodes and intentions would be reflected in real time.</p>
<div class="type-toggle">

View File

@ -37,7 +37,6 @@ export const getAlternateServices = function(targets, a) {
export const getSplitters = function(nodes) {
return getNodesByType(nodes, 'splitter').map(function(item) {
// Splitters need IDs adding so we can find them in the DOM later
item.ID = `splitter:${item.Name}`;
// splitters have a service.nspace as a name
// do the reverse dance to ensure we don't mess up any
// serivice names with dots in them
@ -45,8 +44,11 @@ export const getSplitters = function(nodes) {
temp.reverse();
temp.shift();
temp.reverse();
item.Name = temp.join('.');
return item;
return {
...item,
ID: `splitter:${item.Name}`,
Name: temp.join('.'),
};
});
};
export const getRoutes = function(nodes, uid) {

View File

@ -22,6 +22,7 @@
"start:consul": "ember serve --proxy=${CONSUL_HTTP_ADDR:-http://localhost:8500} --port=${EMBER_SERVE_PORT:-4200} --live-reload-port=${EMBER_LIVE_RELOAD_PORT:-7020}",
"start:api": "api-double --dir ./node_modules/@hashicorp/consul-api-double",
"test": "ember test --test-port=${EMBER_TEST_PORT:-7357}",
"test:ci": "ember test --test-port=${EMBER_TEST_PORT:-7357} --path dist --silent --reporter xunit",
"test:parallel": "EMBER_EXAM_PARALLEL=true ember exam --split=4 --parallel",
"test:view": "ember test --server --test-port=${EMBER_TEST_PORT:-7357}",
"test:node": "tape ./node-tests/**/*.js",

View File

@ -6,6 +6,7 @@ Feature: dc / kvs / update: KV Update
And 1 kv model from yaml
---
Key: "[Name]"
Flags: 12
---
When I visit the kv page for yaml
---
@ -21,7 +22,7 @@ Feature: dc / kvs / update: KV Update
value: [Value]
---
And I submit
Then a PUT request was made to "/v1/kv/[EncodedName]?dc=datacenter&ns=@!namespace" with the body "[Value]"
Then a PUT request was made to "/v1/kv/[EncodedName]?dc=datacenter&flags=12&ns=@!namespace" with the body "[Value]"
And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "success" class
Where:
@ -37,6 +38,7 @@ Feature: dc / kvs / update: KV Update
And 1 kv model from yaml
---
Key: key
Flags: 12
---
When I visit the kv page for yaml
---
@ -51,7 +53,7 @@ Feature: dc / kvs / update: KV Update
value: ' '
---
And I submit
Then a PUT request was made to "/v1/kv/key?dc=datacenter&ns=@!namespace" with the body " "
Then a PUT request was made to "/v1/kv/key?dc=datacenter&flags=12&ns=@!namespace" with the body " "
Then the url should be /datacenter/kv
And the title should be "Key/Value - Consul"
And "[data-notification]" has the "notification-update" class
@ -60,6 +62,7 @@ Feature: dc / kvs / update: KV Update
And 1 kv model from yaml
---
Key: key
Flags: 12
---
When I visit the kv page for yaml
---
@ -74,15 +77,16 @@ Feature: dc / kvs / update: KV Update
value: ''
---
And I submit
Then a PUT request was made to "/v1/kv/key?dc=datacenter&ns=@!namespace" with no body
Then a PUT request was made to "/v1/kv/key?dc=datacenter&flags=12&ns=@!namespace" with no body
Then the url should be /datacenter/kv
And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "success" class
Scenario: Update to a key when the value is empty
And 1 kv model from yaml
---
Key: key
Value: ~
Key: key
Value: ~
Flags: 12
---
When I visit the kv page for yaml
---
@ -91,7 +95,7 @@ Feature: dc / kvs / update: KV Update
---
Then the url should be /datacenter/kv/key/edit
And I submit
Then a PUT request was made to "/v1/kv/key?dc=datacenter&ns=@!namespace" with no body
Then a PUT request was made to "/v1/kv/key?dc=datacenter&flags=12&ns=@!namespace" with no body
Then the url should be /datacenter/kv
And "[data-notification]" has the "notification-update" class
And "[data-notification]" has the "success" class

View File

@ -50,3 +50,30 @@ Feature: dc / nodes / index
Then the url should be /dc-1/nodes
Then I see 3 node models
And I see leader on the healthyNodes
Scenario: Searching the nodes with name and IP address
Given 3 node models from yaml
---
- Node: node-01
Address: 10.0.0.0
- Node: node-02
Address: 10.0.0.1
- Node: node-03
Address: 10.0.0.2
---
When I visit the nodes page for yaml
---
dc: dc-1
---
And I see 3 node models
Then I fill in with yaml
---
s: node-01
---
And I see 1 node model
And I see 1 node model with the name "node-01"
Then I fill in with yaml
---
s: 10.0.0.1
---
And I see 1 node model
And I see 1 node model with the name "node-02"

View File

@ -0,0 +1,33 @@
@setupApplicationTest
Feature: dc / nspaces / manage : Managing Namespaces
Scenario:
Given settings from yaml
---
consul:token:
SecretID: secret
AccessorID: accessor
Namespace: default
---
And 1 datacenter models from yaml
---
- dc-1
---
And 6 service models
When I visit the services page for yaml
---
dc: dc-1
---
Then the url should be /dc-1/services
Then I see 6 service models
# In order to test this properly you have to click around a few times
# between services and nspace management
When I click nspace on the navigation
And I click manageNspaces on the navigation
Then the url should be /dc-1/namespaces
And I don't see manageNspacesIsVisible on the navigation
When I click services on the navigation
Then the url should be /dc-1/services
When I click nspace on the navigation
And I click manageNspaces on the navigation
Then the url should be /dc-1/namespaces
And I don't see manageNspacesIsVisible on the navigation

View File

@ -0,0 +1,37 @@
@setupApplicationTest
Feature: dc / services / Show Routing for Serivce
Scenario: Given a service, the Routing tab should display
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Kind: consul
Name: service-0
ID: service-0-with-id
---
When I visit the service page for yaml
---
dc: dc1
service: service-0
---
And the title should be "service-0 - Consul"
And I see routing on the tabs
Scenario: Given a service proxy, the Routing tab should not display
Given 1 datacenter model with the value "dc1"
And 1 node models
And 1 service model from yaml
---
- Service:
Kind: connect-proxy
Name: service-0-proxy
ID: service-0-proxy-with-id
---
When I visit the service page for yaml
---
dc: dc1
service: service-0-proxy
---
And the title should be "service-0-proxy - Consul"
And I don't see routing on the tabs

View File

@ -2,8 +2,23 @@
@notNamespaceable
Feature: settings / show: Show Settings Page
Scenario:
Scenario: I see the Blocking queries
Given 1 datacenter model with the value "datacenter"
When I visit the settings page
Then the url should be /setting
And the title should be "Settings - Consul"
And I see blockingQueries
Scenario: Setting CONSUL_UI_DISABLE_REALTIME hides Blocking Queries
Given 1 datacenter model with the value "datacenter"
And settings from yaml
---
CONSUL_UI_DISABLE_REALTIME: 1
---
Then I have settings like yaml
---
CONSUL_UI_DISABLE_REALTIME: "1"
---
When I visit the settings page
Then the url should be /setting
And the title should be "Settings - Consul"
And I don't see blockingQueries

View File

@ -0,0 +1,10 @@
import steps from '../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -0,0 +1,10 @@
import steps from '../../steps';
// step definitions that are shared between features should be moved to the
// tests/acceptance/steps/steps.js file
export default function(assert) {
return steps(assert).then('I should find a file', function() {
assert.ok(true, this.step);
});
}

View File

@ -58,7 +58,8 @@ module('Integration | Adapter | kv', function(hooks) {
test(`requestForUpdateRecord returns the correct url/method when nspace is ${nspace}`, function(assert) {
const adapter = this.owner.lookup('adapter:kv');
const client = this.owner.lookup('service:client/http');
const expected = `PUT /v1/kv/${id}?dc=${dc}${
const flags = 12;
const expected = `PUT /v1/kv/${id}?dc=${dc}&flags=${flags}${
typeof nspace !== 'undefined' ? `&ns=${nspace}` : ``
}`;
let actual = adapter
@ -70,6 +71,7 @@ module('Integration | Adapter | kv', function(hooks) {
Key: id,
Value: '',
Namespace: nspace,
Flags: flags,
}
)
.split('\n')

View File

@ -1,4 +1,12 @@
import { create, clickable, is, attribute, collection, text } from 'ember-cli-page-object';
import {
create,
clickable,
is,
attribute,
collection,
text,
isPresent,
} from 'ember-cli-page-object';
import { alias } from 'ember-cli-page-object/macros';
import { visitable } from 'consul-ui/tests/lib/page-object/visitable';
import createDeletable from 'consul-ui/tests/lib/page-object/createDeletable';
@ -59,8 +67,10 @@ const roleSelector = roleSelectorFactory(clickable, deletable, collection, alias
export default {
index: create(index(visitable, collection)),
dcs: create(dcs(visitable, clickable, attribute, collection)),
services: create(services(visitable, clickable, attribute, collection, page, catalogFilter)),
service: create(service(visitable, attribute, collection, text, catalogFilter)),
services: create(
services(visitable, clickable, attribute, collection, page, catalogFilter, radiogroup)
),
service: create(service(visitable, attribute, collection, text, catalogFilter, radiogroup)),
instance: create(instance(visitable, attribute, collection, text, radiogroup)),
nodes: create(nodes(visitable, clickable, attribute, collection, catalogFilter)),
node: create(node(visitable, deletable, clickable, attribute, collection, radiogroup)),
@ -112,5 +122,5 @@ export default {
nspace: create(
nspace(visitable, submitable, deletable, cancelable, policySelector, roleSelector)
),
settings: create(settings(visitable, submitable)),
settings: create(settings(visitable, submitable, isPresent)),
};

View File

@ -1,4 +1,4 @@
import { clickable } from 'ember-cli-page-object';
import { clickable, is } from 'ember-cli-page-object';
const page = {
navigation: ['services', 'nodes', 'kvs', 'acls', 'intentions', 'docs', 'settings'].reduce(
function(prev, item, i, arr) {
@ -24,4 +24,10 @@ const page = {
),
};
page.navigation.dc = clickable('[data-test-datacenter-menu] button');
page.navigation.nspace = clickable('[data-test-nspace-menu] button');
page.navigation.manageNspaces = clickable('[data-test-main-nav-nspaces] a');
page.navigation.manageNspacesIsVisible = is(
':checked',
'[data-test-nspace-menu] > input[type="checkbox"]'
);
export default page;

View File

@ -1,4 +1,4 @@
export default function(visitable, attribute, collection, text, filter) {
export default function(visitable, attribute, collection, text, filter, radiogroup) {
return {
visit: visitable('/:dc/services/:service'),
externalSource: attribute('data-test-external-source', 'h1 span'),
@ -8,6 +8,7 @@ export default function(visitable, attribute, collection, text, filter) {
dashboardAnchor: {
href: attribute('href', '[data-test-dashboard-anchor]'),
},
tabs: radiogroup('tab', ['instances', 'routing', 'tags']),
filter: filter,
};
}

View File

@ -1,5 +1,6 @@
export default function(visitable, submitable) {
export default function(visitable, submitable, isPresent) {
return submitable({
visit: visitable('/setting'),
blockingQueries: isPresent('[data-test-blocking-queries]'),
});
}

View File

@ -1,4 +1,30 @@
/* eslint no-console: "off" */
const notFound = 'Element not found';
const cannotDestructure = "Cannot destructure property 'context'";
const cannotReadContext = "Cannot read property 'context' of undefined";
// checking for existence of pageObjects is pretty difficult
// errors are thrown but we should check to make sure its the error that we
// want and not another real error
// to make things more difficult depending on how you reference the pageObject
// an error with a different message is thrown for example:
// pageObject[thing]() will give you a Element not found error
// whereas:
// const obj = pageObject[thing]; obj() will give you a 'cannot destructure error'
// and in CI it will give you a 'cannot read property' error
// the difference in CI could be a difference due to headless vs headed browser
// or difference in Chrome/browser versions
// ideally we wouldn't be checking on error messages at all, but we want to make sure
// that real errors are picked up by the tests, so if this gets unmanageable at any point
// look at checking for the instance of e being TypeError or similar
const isExpectedError = function(e) {
return [notFound, cannotDestructure, cannotReadContext].some(item => e.message.startsWith(item));
};
export default function(scenario, assert, find, currentPage) {
scenario
.then('I see $property on the $component like yaml\n$yaml', function(
@ -64,34 +90,63 @@ export default function(scenario, assert, find, currentPage) {
);
})
.then(["I don't see $property on the $component"], function(property, component) {
// Collection
var obj;
const message = `Expected to not see ${property} on ${component}`;
// Cope with collections
let obj;
if (typeof currentPage()[component].objectAt === 'function') {
obj = currentPage()[component].objectAt(0);
} else {
obj = currentPage()[component];
}
assert.throws(
function() {
const func = obj[property].bind(obj);
func();
},
function(e) {
return e.message.startsWith('Element not found');
},
`Expected to not see ${property} on ${component}`
);
let prop;
try {
prop = obj[property];
} catch (e) {
if (isExpectedError(e)) {
assert.ok(true, message);
} else {
throw e;
}
}
if (typeof prop === 'function') {
assert.throws(
function() {
prop();
},
function(e) {
return isExpectedError(e);
},
message
);
} else {
assert.notOk(prop);
}
})
.then(["I don't see $property"], function(property) {
assert.throws(
function() {
return currentPage()[property]();
},
function(e) {
return e.message.startsWith('Element not found');
},
`Expected to not see ${property}`
);
const message = `Expected to not see ${property}`;
let prop;
try {
prop = currentPage()[property];
} catch (e) {
if (isExpectedError(e)) {
assert.ok(true, message);
} else {
throw e;
}
}
if (typeof prop === 'function') {
assert.throws(
function() {
prop();
},
function(e) {
return isExpectedError(e);
},
message
);
} else {
assert.notOk(prop);
}
})
.then(['I see $property'], function(property) {
assert.ok(currentPage()[property], `Expected to see ${property}`);

View File

@ -3,10 +3,11 @@ import { module, test } from 'qunit';
module('Unit | Search | Filter | node', function() {
const filter = getFilter(cb => cb);
test('items are found by properties', function(assert) {
test('items are found by name', function(assert) {
[
{
Node: 'node-HIT',
Address: '10.0.0.0',
},
].forEach(function(item) {
const actual = filter(item, {
@ -15,10 +16,24 @@ module('Unit | Search | Filter | node', function() {
assert.ok(actual);
});
});
test('items are not found', function(assert) {
test('items are found by IP address', function(assert) {
[
{
Node: 'node-HIT',
Address: '10.0.0.0',
},
].forEach(function(item) {
const actual = filter(item, {
s: '10',
});
assert.ok(actual);
});
});
test('items are not found by name', function(assert) {
[
{
Node: 'name',
Address: '10.0.0.0',
},
].forEach(function(item) {
const actual = filter(item, {
@ -27,4 +42,17 @@ module('Unit | Search | Filter | node', function() {
assert.notOk(actual);
});
});
test('items are not found by IP address', function(assert) {
[
{
Node: 'name',
Address: '10.0.0.0',
},
].forEach(function(item) {
const actual = filter(item, {
s: '9',
});
assert.notOk(actual);
});
});
});

View File

@ -992,9 +992,9 @@
js-yaml "^3.13.1"
"@hashicorp/consul-api-double@^2.6.2":
version "2.11.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.11.0.tgz#0b833893ccc5cfb9546b1513127d5e92d30f2262"
integrity sha512-2MO1jiwuJyPlSGQ4AeFtLKJWmLSj0msoiaRHPtj6YPjm69ZkY/t4U4SU3cfpVn2Dx7wHzXe//9GvNHI1gRxAzg==
version "2.12.0"
resolved "https://registry.yarnpkg.com/@hashicorp/consul-api-double/-/consul-api-double-2.12.0.tgz#725078f770bbd0ef75a5f2498968c5c8891f90a2"
integrity sha512-8OcgesUjWQ8AjaXzbz3tGJQn1kM0sN6pLidGM7isNPUyYmIjIEXQzaeUQYzsfv0N2Ko9ZuOXYUsaBl8IK1KGow==
"@hashicorp/ember-cli-api-double@^2.0.0":
version "2.0.0"