ui: Fix sticky nspace menu (#7164)

* ui: Fix typo expanded > ariaExpanded

* ui: Add the things we need to test this

* ui: Add tests for testing the menu closes when clicked

* ui: Ensure the aria-menu closes on route change
pull/7190/head
John Cowen 2020-01-31 14:11:46 +00:00 committed by GitHub
parent 1b74a68780
commit f45880bffe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 110 additions and 15 deletions

View File

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

View File

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

View File

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

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,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

@ -1,4 +1,4 @@
import { clickable } from 'ember-cli-page-object'; import { clickable, is } from 'ember-cli-page-object';
const page = { const page = {
navigation: ['services', 'nodes', 'kvs', 'acls', 'intentions', 'docs', 'settings'].reduce( navigation: ['services', 'nodes', 'kvs', 'acls', 'intentions', 'docs', 'settings'].reduce(
function(prev, item, i, arr) { function(prev, item, i, arr) {
@ -24,4 +24,10 @@ const page = {
), ),
}; };
page.navigation.dc = clickable('[data-test-datacenter-menu] button'); 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; export default page;

View File

@ -1,4 +1,30 @@
/* eslint no-console: "off" */ /* 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) { export default function(scenario, assert, find, currentPage) {
scenario scenario
.then('I see $property on the $component like yaml\n$yaml', function( .then('I see $property on the $component like yaml\n$yaml', function(
@ -64,23 +90,37 @@ export default function(scenario, assert, find, currentPage) {
); );
}) })
.then(["I don't see $property on the $component"], function(property, component) { .then(["I don't see $property on the $component"], function(property, component) {
// Collection const message = `Expected to not see ${property} on ${component}`;
var obj; // Cope with collections
let obj;
if (typeof currentPage()[component].objectAt === 'function') { if (typeof currentPage()[component].objectAt === 'function') {
obj = currentPage()[component].objectAt(0); obj = currentPage()[component].objectAt(0);
} else { } else {
obj = currentPage()[component]; obj = currentPage()[component];
} }
assert.throws( let prop;
function() { try {
const func = obj[property].bind(obj); prop = obj[property];
func(); } catch (e) {
}, if (isExpectedError(e)) {
function(e) { assert.ok(true, message);
return e.message.startsWith('Element not found'); } else {
}, throw e;
`Expected to not see ${property} on ${component}` }
); }
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) { .then(["I don't see $property"], function(property) {
assert.throws( assert.throws(