-
Notifications
You must be signed in to change notification settings - Fork 6.8k
feat(cdk-experimental/menu): Implement grouping logic for menuitems #19628
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 5 commits
671341f
c230e82
bb7b960
56f724e
39a3d88
0438e6c
463f5ba
561040c
f1949de
25baedd
120139c
95f0747
a412360
8c9edc8
9714c83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
import {ComponentFixture, TestBed, async} from '@angular/core/testing'; | ||
import {Component} from '@angular/core'; | ||
import {CdkMenuBar} from './menu-bar'; | ||
import {CdkMenuModule} from './menu-module'; | ||
import {CdkMenuItem} from './menu-item'; | ||
import {By} from '@angular/platform-browser'; | ||
|
||
describe('MenuBar', () => { | ||
describe('as radio group', () => { | ||
let fixture: ComponentFixture<MenuBarRadioGroup>; | ||
let menuBar: CdkMenuBar; | ||
let menuItems: Array<CdkMenuItem>; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [CdkMenuModule], | ||
declarations: [MenuBarRadioGroup], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(MenuBarRadioGroup); | ||
fixture.detectChanges(); | ||
|
||
menuBar = fixture.debugElement.query(By.directive(CdkMenuBar)).injector.get(CdkMenuBar); | ||
|
||
menuItems = menuBar._allItems.toArray(); | ||
})); | ||
|
||
it('should toggle menuitemradio items', () => { | ||
expect(menuItems[0].checked).toBeTrue(); | ||
expect(menuItems[1].checked).toBeFalse(); | ||
|
||
menuItems[1].trigger(); | ||
|
||
expect(menuItems[0].checked).toBeFalse(); | ||
expect(menuItems[1].checked).toBeTrue(); | ||
}); | ||
}); | ||
|
||
describe('as checkbox group', () => { | ||
let fixture: ComponentFixture<MenuBarCheckboxGroup>; | ||
let menuBar: CdkMenuBar; | ||
let menuItems: Array<CdkMenuItem>; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [CdkMenuModule], | ||
declarations: [MenuBarCheckboxGroup], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(MenuBarCheckboxGroup); | ||
|
||
menuBar = fixture.debugElement.query(By.directive(CdkMenuBar)).injector.get(CdkMenuBar); | ||
fixture.detectChanges(); | ||
|
||
menuItems = menuBar._allItems.toArray(); | ||
})); | ||
|
||
it('should toggle menuitemcheckbox', () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you remove words like 'should' from your test descriptions, you can make them more concise. (We can assume that all tests describe the intended behavior for the components under tests.) Also, this description could be more specific. You're testing that menuitemcheckboxes are toggled in isolation, so if the description mentions this, you can avoid the comment further down. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a convention for Angular projects; we want test description to read like a sentence ("it should...") |
||
expect(menuItems[0].checked).toBeTrue(); | ||
expect(menuItems[1].checked).toBeFalse(); | ||
|
||
menuItems[1].trigger(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with the public Angular testing conventions. Would this be the appropriate point to bring in test harnesses? (Would they apply for CDK directives, as opposed to actual components?) Usually, it would be better to simulate real clicks using harness actions or mouse events, instead of directly calling methods on the directive class. But I don't know if that applies here, or if it's possible at this point in development. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We actually don't use harnesses for the component's unit test; we create separate tests for the harnesses once we make them |
||
expect(menuItems[0].checked).toBeTrue(); // checkbox should not change | ||
|
||
menuItems[0].trigger(); | ||
|
||
expect(menuItems[0].checked).toBeFalse(); | ||
expect(menuItems[1].checked).toBeTrue(); | ||
}); | ||
}); | ||
|
||
describe('checkbox change events', () => { | ||
let fixture: ComponentFixture<MenuBarCheckboxGroup>; | ||
let menu: CdkMenuBar; | ||
let menuItems: Array<CdkMenuItem>; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [CdkMenuModule], | ||
declarations: [MenuBarCheckboxGroup], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(MenuBarCheckboxGroup); | ||
|
||
menu = fixture.debugElement.query(By.directive(CdkMenuBar)).injector.get(CdkMenuBar); | ||
fixture.detectChanges(); | ||
|
||
menuItems = menu._allItems.toArray(); | ||
})); | ||
|
||
it('should emit on click', () => { | ||
const spy = jasmine.createSpy('cdkMenu change spy'); | ||
fixture.debugElement | ||
.query(By.directive(CdkMenuBar)) | ||
.injector.get(CdkMenuBar) | ||
.change.subscribe(spy); | ||
|
||
menuItems[0].trigger(); | ||
|
||
expect(spy).toHaveBeenCalledTimes(1); | ||
expect(spy).toHaveBeenCalledWith(menuItems[0]); | ||
}); | ||
}); | ||
|
||
describe('radiogroup change events', () => { | ||
let fixture: ComponentFixture<MenuBarRadioGroup>; | ||
let menu: CdkMenuBar; | ||
let menuItems: Array<CdkMenuItem>; | ||
|
||
beforeEach(async(() => { | ||
TestBed.configureTestingModule({ | ||
imports: [CdkMenuModule], | ||
declarations: [MenuBarRadioGroup], | ||
}).compileComponents(); | ||
|
||
fixture = TestBed.createComponent(MenuBarRadioGroup); | ||
|
||
menu = fixture.debugElement.query(By.directive(CdkMenuBar)).injector.get(CdkMenuBar); | ||
fixture.detectChanges(); | ||
|
||
menuItems = menu._allItems.toArray(); | ||
})); | ||
|
||
it('should emit on click', () => { | ||
const spy = jasmine.createSpy('cdkMenu change spy'); | ||
fixture.debugElement | ||
.query(By.directive(CdkMenuBar)) | ||
.injector.get(CdkMenuBar) | ||
.change.subscribe(spy); | ||
|
||
menuItems[0].trigger(); | ||
|
||
expect(spy).toHaveBeenCalledTimes(1); | ||
expect(spy).toHaveBeenCalledWith(menuItems[0]); | ||
}); | ||
}); | ||
}); | ||
|
||
@Component({ | ||
selector: 'menubar-as-radio-group', | ||
andy9775 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
template: ` | ||
<ul cdkMenuBar> | ||
<li role="none"> | ||
<button checked="true" role="menuitemradio" cdkMenuItem> | ||
first | ||
</button> | ||
</li> | ||
<li role="none"> | ||
<button role="menuitemradio" cdkMenuItem> | ||
second | ||
</button> | ||
</li> | ||
</ul> | ||
`, | ||
}) | ||
class MenuBarRadioGroup {} | ||
|
||
@Component({ | ||
selector: 'menubar-as-checkbox-group', | ||
template: ` | ||
<ul cdkMenuBar> | ||
<li role="none"> | ||
<button checked="true" role="menuitemcheckbox" cdkMenuItem> | ||
first | ||
</button> | ||
</li> | ||
<li role="none"> | ||
<button role="menuitemcheckbox" cdkMenuItem> | ||
second | ||
</button> | ||
</li> | ||
</ul> | ||
`, | ||
}) | ||
class MenuBarCheckboxGroup {} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,9 @@ | |
* found in the LICENSE file at https://angular.io/license | ||
*/ | ||
|
||
import {Directive, Input} from '@angular/core'; | ||
import {Directive, Input, ContentChildren, QueryList} from '@angular/core'; | ||
import {CdkMenuGroup} from './menu-group'; | ||
import {CdkMenuItem} from './menu-item'; | ||
|
||
/** | ||
* Directive applied to an element which configures it as a MenuBar by setting the appropriate | ||
|
@@ -21,11 +23,16 @@ import {Directive, Input} from '@angular/core'; | |
'role': 'menubar', | ||
'[attr.aria-orientation]': 'orientation', | ||
}, | ||
providers: [{provide: CdkMenuGroup, useExisting: CdkMenuBar}], | ||
}) | ||
export class CdkMenuBar { | ||
export class CdkMenuBar extends CdkMenuGroup { | ||
/** | ||
* Sets the aria-orientation attribute and determines where sub-menus will be opened. | ||
* Does not affect styling/layout. | ||
*/ | ||
@Input('cdkMenuBarOrientation') orientation: 'horizontal' | 'vertical' = 'horizontal'; | ||
|
||
/** All the child MenuItem components which this directive wraps including descendants */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are you only using this to access items in the unit test? If so, it may be better to leave it out and find another way to grab the menu items for the test (using selectors, for instance). Although it's good to design things for testability, it's not a good idea to have test-only APIs. In general, they encourage testing in ways that don't match the real-world use of the components. (At the very least, you also want to reduce the size of the Angular payload by not including test-only code.) If you do plan to use this within this component itself, then never mind (although I still wouldn't use it in tests, since it's been marked private with an underscore). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It'll be used in future functionality. I added it at this point since I can also use it in the test. But ya, I could have also added the ContentChildren into the test fixture and gone about it that way. As for the package private - the Angular recommendation is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We do use the underscore prefix as "library-internal". For testing, though, we we typically grab content child items directly (via As a rule of thumb, I would only add stuff to the code as it's needed for some behavior/feature. |
||
@ContentChildren(CdkMenuItem, {descendants: true}) | ||
readonly _allItems: QueryList<CdkMenuItem>; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I just realized you test a lot of the same functionality in the tests for menu-group. Since menu-bar extends menu-group, I don't think you need to repeat the same tests here -- this suite can focus on tests specific to menu-bar's extra functionality.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The menu and menu-bar tests are more of a sanity check to ensure that it's wired up correctly The meat of the logic is in the menu-group which contains most of the tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In that case, just one or two sanity-check tests should be enough -- I don't think you need to go over checkboxes and radio buttons separately. (That said, I initially thought there were more tests in this file, for some reason. Now that I look again, there doesn't seem to be as much overlap. I'm still not used to reviewing on Github...)