fix(aio): header click should always toggle expand/collapse (#16439)
Also adds class tests for this NavItemComponent. Not (yet) testing effects on the templated HTML
This commit is contained in:
parent
f74dafcb07
commit
8d300ffbfc
@ -1,6 +1,6 @@
|
|||||||
<div *ngIf="!node.children">
|
<div *ngIf="!node.children">
|
||||||
<a href="{{node.url}}" [ngClass]="classes" title="{{node.tooltip}}"
|
<a href="{{node.url}}" [ngClass]="classes" title="{{node.tooltip}}"
|
||||||
(click)="itemClicked()" class="vertical-menu-item">
|
class="vertical-menu-item">
|
||||||
{{node.title}}
|
{{node.title}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
159
aio/src/app/layout/nav-item/nav-item.component.spec.ts
Normal file
159
aio/src/app/layout/nav-item/nav-item.component.spec.ts
Normal file
@ -0,0 +1,159 @@
|
|||||||
|
|
||||||
|
import { SimpleChange, SimpleChanges } from '@angular/core';
|
||||||
|
|
||||||
|
import { NavItemComponent } from './nav-item.component';
|
||||||
|
import { NavigationNode } from 'app/navigation/navigation.model';
|
||||||
|
|
||||||
|
// Testing the component class behaviors, independent of its template
|
||||||
|
// No dependencies. Just new it and test :)
|
||||||
|
// Let e2e tests verify how it displays.
|
||||||
|
describe('NavItemComponent (class-only)', () => {
|
||||||
|
|
||||||
|
let component: NavItemComponent;
|
||||||
|
|
||||||
|
let selectedNodes: NavigationNode[];
|
||||||
|
let setClassesSpy: jasmine.Spy;
|
||||||
|
|
||||||
|
function initialize(nd: NavigationNode) {
|
||||||
|
component.node = nd;
|
||||||
|
onChanges(); // Angular calls when initializing the component
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enough to triggers component's ngOnChange method
|
||||||
|
function onChanges() {
|
||||||
|
component.ngOnChanges({node: <SimpleChange><any> 'anything' });
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
component = new NavItemComponent();
|
||||||
|
setClassesSpy = spyOn(component, 'setClasses').and.callThrough();
|
||||||
|
|
||||||
|
// Selected nodes is the selected node and its header ancestors
|
||||||
|
selectedNodes = [
|
||||||
|
{ title: 'a' }, // selected node: an item or a header
|
||||||
|
{ title: 'parent' }, // selected node's header parent
|
||||||
|
{ title: 'grandparent' }, // selected node's header grandparent
|
||||||
|
];
|
||||||
|
component.selectedNodes = selectedNodes;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should have expected classes when initialized', () => {
|
||||||
|
it('with selected node', () => {
|
||||||
|
initialize(selectedNodes[0]);
|
||||||
|
expect(component.classes).toEqual(
|
||||||
|
// selecting the current node has no effect on expanded state,
|
||||||
|
// even if current node is a header.
|
||||||
|
{ 'level-1': true, collapsed: true, expanded: false, selected: true}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with selected node ancestor', () => {
|
||||||
|
initialize(selectedNodes[1]);
|
||||||
|
expect(component.classes).toEqual(
|
||||||
|
// ancestor is a header and should be expanded
|
||||||
|
{ 'level-1': true, collapsed: false, expanded: true, selected: true}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with other than a selected node or ancestor', () => {
|
||||||
|
initialize({ title: 'x' });
|
||||||
|
expect(component.classes).toEqual(
|
||||||
|
{ 'level-1': true, collapsed: true, expanded: false, selected: false}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when becomes a non-selected node', () => {
|
||||||
|
|
||||||
|
// this node won't be the selected node when ngOnChanges() called
|
||||||
|
beforeEach(() => component.node = { title: 'x' });
|
||||||
|
|
||||||
|
it('should collapse if previously expanded', () => {
|
||||||
|
component.isExpanded = true;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isExpanded).toBe(false, 'becomes collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should de-select if previously selected', () => {
|
||||||
|
component.isSelected = true;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isSelected).toBe(false, 'becomes de-selected');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when becomes a selected node', () => {
|
||||||
|
|
||||||
|
// this node will be the selected node when ngOnChanges() called
|
||||||
|
beforeEach(() => component.node = selectedNodes[0]);
|
||||||
|
|
||||||
|
it('should select when previously not selected', () => {
|
||||||
|
component.isSelected = false;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isSelected).toBe(true, 'becomes selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should leave the expanded/collapsed state untouched', () => {
|
||||||
|
component.isExpanded = false;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isExpanded).toBe(false, 'remains false');
|
||||||
|
|
||||||
|
component.isExpanded = true;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isExpanded).toBe(true, 'remains true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when becomes a selected ancestor node', () => {
|
||||||
|
|
||||||
|
// this node will be a selected node ancestor header when ngOnChanges() called
|
||||||
|
beforeEach(() => component.node = selectedNodes[2]);
|
||||||
|
|
||||||
|
it('should select when previously not selected', () => {
|
||||||
|
component.isSelected = false;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isSelected).toBe(true, 'becomes selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should always expand this header', () => {
|
||||||
|
component.isExpanded = false;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isExpanded).toBe(true, 'becomes expanded');
|
||||||
|
|
||||||
|
component.isExpanded = false;
|
||||||
|
onChanges();
|
||||||
|
expect(component.isExpanded).toBe(true, 'stays expanded');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when headerClicked()', () => {
|
||||||
|
// current node doesn't matter in these tests.
|
||||||
|
|
||||||
|
it('should expand when headerClicked() and previously collapsed', () => {
|
||||||
|
component.isExpanded = false;
|
||||||
|
component.headerClicked();
|
||||||
|
expect(component.isExpanded).toBe(true, 'should be expanded');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collapse when headerClicked() and previously expanded', () => {
|
||||||
|
component.isExpanded = true;
|
||||||
|
component.headerClicked();
|
||||||
|
expect(component.isExpanded).toBe(false, 'should be collapsed');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change isSelected when headerClicked()', () => {
|
||||||
|
component.isSelected = true;
|
||||||
|
component.headerClicked();
|
||||||
|
expect(component.isSelected).toBe(true, 'remains selected');
|
||||||
|
|
||||||
|
component.isSelected = false;
|
||||||
|
component.headerClicked();
|
||||||
|
expect(component.isSelected).toBe(false, 'remains not selected');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set classes', () => {
|
||||||
|
component.headerClicked();
|
||||||
|
expect(setClassesSpy).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,5 @@
|
|||||||
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
import { Component, Input, OnChanges, SimpleChanges } from '@angular/core';
|
||||||
import { NavigationNode } from 'app/navigation/navigation.service';
|
import { NavigationNode } from 'app/navigation/navigation.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'aio-nav-item',
|
selector: 'aio-nav-item',
|
||||||
@ -16,8 +16,9 @@ export class NavItemComponent implements OnChanges {
|
|||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges) {
|
ngOnChanges(changes: SimpleChanges) {
|
||||||
if (changes['selectedNodes'] || changes['node']) {
|
if (changes['selectedNodes'] || changes['node']) {
|
||||||
this.isSelected = this.selectedNodes.indexOf(this.node) !== -1;
|
const ix = this.selectedNodes.indexOf(this.node);
|
||||||
this.isExpanded = this.isSelected;
|
this.isSelected = ix !== -1;
|
||||||
|
if (ix !== 0) { this.isExpanded = this.isSelected; }
|
||||||
}
|
}
|
||||||
this.setClasses();
|
this.setClasses();
|
||||||
}
|
}
|
||||||
@ -31,11 +32,6 @@ export class NavItemComponent implements OnChanges {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
itemClicked() {
|
|
||||||
this.isExpanded = true;
|
|
||||||
this.isSelected = !!this.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
headerClicked() {
|
headerClicked() {
|
||||||
this.isExpanded = !this.isExpanded;
|
this.isExpanded = !this.isExpanded;
|
||||||
this.setClasses();
|
this.setClasses();
|
||||||
|
@ -4,7 +4,7 @@ import { NavigationNode } from 'app/navigation/navigation.service';
|
|||||||
// Testing the component class behaviors, independent of its template
|
// Testing the component class behaviors, independent of its template
|
||||||
// No dependencies, no life-cycle hooks. Just new it and test :)
|
// No dependencies, no life-cycle hooks. Just new it and test :)
|
||||||
// Let e2e tests verify how it displays.
|
// Let e2e tests verify how it displays.
|
||||||
describe('NavMenuComponent', () => {
|
describe('NavMenuComponent (class-only)', () => {
|
||||||
it('should filter out hidden nodes', () => {
|
it('should filter out hidden nodes', () => {
|
||||||
const component = new NavMenuComponent();
|
const component = new NavMenuComponent();
|
||||||
const nodes: NavigationNode[] =
|
const nodes: NavigationNode[] =
|
||||||
|
Loading…
x
Reference in New Issue
Block a user