Unit testing user interaction in an angular2 component - unit-testing

I've recently started learning angular2, and I thought I'd try and write a combobox component similar to select2. Here's a simplified version of the component:
#Component({
selector: 'combobox',
template: `
<div class="combobox-wrapper">
<input type="text"></input>
<button class="toggle-button"></button>
</div>
<div *ngIf="isDropdownOpen" class="dropdown">
<div *ngFor="let option of options; let index = index;"
class="option"
[class.focused]="focusedIndex==index">
{{option.label}}
</div>
</div>
`
})
export class ComboboxComponent {
#Input() options: any[] = [];
isDropdownOpen: boolean = false;
focusedIndex: number = 0;
}
I'm struggling when it comes to writing unit tests involving user interaction. For example, I want the user to be able to navigate the list of options by using the up and down keys on the keyboard. The way I see it, I have two options.
Option 1:
describe('when the dropdown is open and the user presses the "UP" key', () => {
it('should focus the first available preceeding option', () => {
const button = fixture.debugElement.query(By.css('.toggle-button'));
const input = fixture.debugElement.query(By.css('input'));
button.triggerEventHandler('mousedown', {});
input.triggerEventHandler('keydown', { key: 'ArrowDown' });
input.triggerEventHandler('keydown', { key: 'ArrowDown' });
input.triggerEventHandler('keydown', { key: 'ArrowUp' });
fixture.detectChanges();
const options = fixture.debugElement.queryAll(By.css('.option'));
expect(options[1].nativeElement.className).toContain('focused');
});
});
Option 2:
describe('when the dropdown is open and the user presses the "UP" key', () => {
it('should focus the first available preceeding option', () => {
const combobox = fixture.componentInstance;
combobox.isDropdownOpen = true;
combobox.focusedIndex = 2;
input.triggerEventHandler('keydown', { key: 'ArrowUp' });
fixture.detectChanges();
expect(combobox.focusedIndex).toBe(1);
});
});
Neither option feels right. In the first case I'm making assumptions about behaviour that is not part of the test itself - namely that clicking the "toggle" button will open the dropdown, and that pressing the "ArrowDown" key will focus the next option on the list.
In the second case I'm accessing properties that are not part of the component's public interface (#Inputs and #Outputs), and the test itself requires detailed knowledge about the actual implementation.
How should I approach this?

Related

Write a unit test for a data binding property of Mat-button [color]

I am trying to write a unit test for a data-binding property of HTML element Mat-button.
Code is below:
<div fxLayout="row">
<button data-testid="group-button" mat-raised-button *ngFor="let role of user.groups"
[color]="role == 'ADMIN' ? 'warn' : 'primary'">{{role}}</button>
</div>
I need to write a unit test for [color] property to check color is primary when role is 'ADMIN'
I tried this so far using debug element:
I am mocking user.groups as ["ADMIN", "FOO", "BAR"]
it('should change button color based on user groups', () => {
const groups = fixture.debugElement.queryAll(By.css('[data-testid="group-button"]'));
expect(groups[0].nativeElement.getProperty('color')).toBe('Primary'); // Fail
expect(groups[0].nativeElement.getAttribute('color')).toBe('Primary'); // Fail
//expect(groups[0].nativeElement.textContent).toBe('ADMIN')
}
I finally found the solution for my problem..
The color attribute is converted to ng-reflect-color in browser so test was not finding the attribute. The below test worked:
it('should change button color based on user groups', () => {
const groups = fixture.debugElement.queryAll(By.css('[data-testid="group-button"]'));
expect(groups[0].nativeElement.getAttribute('ng-reflect-color')).toBe('warn'); // Pass
expect(groups[1].nativeElement.getAttribute('ng-reflect-color')).toBe('primary'); // Pass
}

emberjs child component not re-rendering after property update

I have a page component (five-whys) with a number of inputs that the user can choose to finalize the input. When the user clicks finalize, all questions are made to be disabled.
Page component
five-whys.hbs:
{{#each this.whys as |why i|}}
<Generic::RichTextInput
#value={{why.content}}
#onChange={{action this.whyChanged i}}
#disabled={{this.isFinalized}} />
{{/each}}
<button {{on "click" this.finalizeWhy}}>Finalize</button>
five-whys.ts
interface AnalyzeFiveWhysArgs {
dataStory: DataStory;
}
export default class AnalyzeFiveWhys extends Component<AnalyzeFiveWhysArgs> {
#alias("args.dataStory.fiveWhysAnalysis") fiveWhysAnalysis
#tracked
isFinalized: boolean = this.fiveWhysAnalysis.isFinalized ?? false;
#tracked
whys: LocalWhy[] = this.fiveWhysAnalysis.whys;
#tracked
isFinalized: boolean = this.fiveWhysAnalysis.isFinalized ?? false;
#action
async finalizeWhy() {
this.isFinalized = true;
}
This works fine when my rich text component is just a regular text area. However, I am trying to implement tinymce which requires me to do stuff outside of Embers little safe space of magic.
My rich text component:
Template:
<textarea id={{this.id}} disabled={{this.templatePieceIsDisabled}}>{{#value}}</textarea>
Typescript:
interface GenericRichTextInputArgs {
value?: string;
onChange: (value: string) => void;
name: string;
disabled?: boolean;
}
export default class GenericRichTextInput extends Component<GenericRichTextInputArgs> {
constructor(owner: unknown, args: GenericRichTextInputArgs) {
super(owner, args);
this.initializeTinymce();
}
id = this.args.name;
get editor() {
return tinymce.get(this.id);
}
get settings() {
console.log(this.args.disabled);
const settings: TinyMCESettings = {
selector: `#${this.id}`,
setup: (editor: Editor) => this.setupEditor(this, editor),
readonly: this.args.disabled ? this.args.disabled : false
};
return settings;
}
initializeTinymce() {
Ember.run.schedule('afterRender', () => {
console.log("re-initializing"); // I expect to see this log every time the isFinalized property in the five-whys component changes. But I only see it on page load.
tinymce.init(this.settings);
});
}
setupEditor(self: GenericRichTextInput, editor: Editor) {
... // details of tinymce API
}
}
When I click the finalize button, The effect of the disabled flag in the rich text component does not change.
Note:
The tinymce library I'm using sets the text area display to none and the aria-hidden to true. This is because it wraps the textarea in a widget. So I have to use the library's api to set disabled.
I figured it out. Ember doesn't run the constructor for the update life-cycle event. So I need to tell Ember to re-run the initializer when the template gets re-rendered. I had to use https://github.com/emberjs/ember-render-modifiers to do this.
So my rich text editor template looks like:
<textarea
id={{this.id}}
{{did-update this.updateDisabled #disabled}}>
{{#value}}
</textarea>
And I added this action in the code behind of the rich text editor:
#action
updateDisabled(element: HTMLTextAreaElement, [disabled]: any[]) {
this.disabled = disabled;
this.editor.destroy();
this.initializeTinymce();
}

Can I do all my unit-tests with only shallow rendering and without mounting the full component tree?

Please let me illustrate. Let's say we have this to do app:
// TodoContainer.jsx
<TodoContainer>
<AddTodo />
<TodoList />
</TodoContainer>
My question is how should I test its add todo functionality?
Is it enough to..
Approach #1
shallow render TodoContainer and assert that..
a. it has a working addTodoMethod which updates the state accordingly
b. it passes the addTodoMethod its child composite component, AddTodo
shallow render AddTodo and assert that..
a. it has received the addTodoMethod as prop
b. it fires the addTodoMethod on button click
This is how approach #1 would look like:
// TodoContainer.spec.jsx
describe('<TodoContainer />', function() {
describe('addTodoMethod', function() {
beforeEach(function() {
this.todoContainer = shallow(<TodoContainer />)
})
it('adds todo to todos state when called', function() {
const todos = this.todoContainer.instance().state('todos')
this.todoContainer.instance().addToDoMethod('sample todo item')
expect(todos).length.to.be(1)
expect(todos[0]).to.be('sample to do item')
})
it('is passed do AddTodo component', function() {
const addTodo = shallow(<AddTodo />)
expect(addTodo.instance().prop('onClick')).to.eql.(this.todoContainer.addTodoMethod)
})
})
})
// TodoContainer.spec.jsx
describe('<AddTodo />', function() {
beforeEach(function() {
this.addTodo = shallow(<AddTodo />)
})
it('has onClick prop', function() {
expect(this.addTodo).to.be.a('function')
})
it('calls onClick when clicked', function() {
const onClickSpy = spy();
const addTodo = shallow(<AddTodo onClick={onClickSpy} />)
addTodo.setState({ newTodoText: 'sample new todo' })
addButton.find('button').simulate('click');
expect(onClickSpy.calledOnce()).to.be.true
expect(onClickSpy.calledWith('sample new todo')).to.be.true
})
})
or should I also do this next approach?
Approach #2
in addition to assertions above, also mount the TodoContainer to assert that..
a. it can add a todo item thru inputting new to do text and clicking button (both in AddTodo component), the new todo item is also asserted thru transversing to TodoList component and check if it has the correct number of li with their correct content.
Code for approach #2:
// TodoContainer.spec.jsx
describe('<TodoContainer />', function() {
...
it('can add a todo item', function() {
this.todoContainer.find('input').simulate('change', {target: {value: 'sample new todo 01'}})
this.todoContainer.find('button').simulate('click')
expect(this.todoContainer).to.have.exactly(1).descendants('li')
expect(this.todoContainer.find('li')[0]).to.have.text('sample new todo')
this.todoContainer.find('input').simulate('change', {target: {value: 'sample new todo 02'}})
this.todoContainer.find('button').simulate('click')
expect(this.todoContainer).to.have.exactly(2).descendants('li')
expect(this.todoContainer.find('li')[1]).to.have.text('sample new todo 02')
})
})
I'm just thinking if the second approach is kinda redundant, and if it is a better practice to focus only on testing the component at hand and letting the child components rely on their own tests.
In short, yes - you can write all of your unit tests with shallow. And in most cases, you probably want exactly that. There are some times however when there's an exception where you'd want to use mount instead:
If a certain group of components are clustered together, and you're more interested in testing the cluster as a whole instead of each part individually.
If your component is tightly coupled to an external library, and testing your component individually would feel more like you're testing that external library.
As a general rule of thumb, though, I tend to reach for shallow for unit tests, mount for integration tests.

Ionic 2 - Get data back from modal

I have a component which is my main interface. Inside this component, clicking a button opens ionic 2 modal which allows to choose items.
My modal page (itemsPage):
..list of items here
<button ion-button [disabled]="!MY_TURN || !selectedItem || !selectedItem.quantity"
(click)="useItem(selectedItem)">
<span>Choose item {{selectedItem?.name}}</span>
</button>
useItem() should:
Send item data to my main interface component
Close the modal
Execute a method in my main interface
How I can perform such actions? Couldn't find any documentation about communicating between modal and component in Ionic 2.
It is simply a matter of using parameters in viewController.
In your main interface component,
let chooseModal = this.modalCtrl.create(itemsPage);
chooseModal.onDidDismiss(data => {
console.log(data);
});
chooseModal.present();
In your modal page,
useItem(item) {
this.viewCtrl.dismiss(item);
}
Modal Controller link here
This is a clear example of getting data from modals in ionic.
You need to add a handler for modal’s onDismiss() and then return the data from the modal itself by passing the data to the ViewController’s dismiss() method:
// myPage.ts
// Passing data to the modal:
let modal = Modal.create(myModal, { data: [...] });
// Getting data from the modal:
modal.onDismiss(data => {
console.log('MODAL DATA', data);
});
this.nav.present(modal);
on the modal page
// myModal.ts
constructor(private navParams: NavParams, private viewCtrl: ViewController) {
// Getting data from the page:
var dataFromPage = navParams.get('data');
}
dismiss() {
// Returning data from the modal:
this.viewCtrl.dismiss(
// Whatever should be returned, e.g. a variable name:
// { name : this.name }
);
}

Angular2 Component: Testing form input value change

I have a text input and i'm listening for the changes.
mycomponent.ts
ngOnInit() {
this.searchInput = new Control();
this.searchInput.valueChanges
.distinctUntilChanged()
.subscribe(newValue => this.search(newValue))
}
search(query) {
// do something to search
}
mycomponent.html
<search-box>
<input type="text" [ngFormControl]="searchInput" >
</search-box>
Running the application everything works fine, but i want to unit-test it.
So here's what i tried
mycomponent.spec.ts
beforeEach(done => {
createComponent().then(fix => {
cmpFixture = fix
mockResponse()
instance = cmpFixture.componentInstance
cmpFixture.detectChanges();
done();
})
})
describe('on searching on the list', () => {
let compiled, input
beforeEach(() => {
cmpFixture.detectChanges();
compiled = cmpFixture.debugElement.nativeElement;
spyOn(instance, 'search').and.callThrough()
input = compiled.querySelector('search-box > input')
input.value = 'fake-search-query'
cmpFixture.detectChanges();
})
it('should call the .search() method', () => {
expect(instance.search).toHaveBeenCalled()
})
})
Test fails as the .search() method is not called.
I guess i have to set the value in another way to have the test realize of the change but i really don't know how.
Anyone has ideas?
It might be a little bit late, but it seems that your code is not dispatching input event after setting input element value:
// ...
input.value = 'fake-search-query';
input.dispatchEvent(new Event('input'));
cmpFixture.detectChanges();
// ...
Updating input html field from within an Angular 2 test
Triggering the value change of FormControl is as simple as:
cmpFixture.debugElement.componentInstance.searchInput.setValue(newValue);
Custom component with #input, subscriptions, two way data binding
If you got a custom component you would need further changes in your application to be able to successfully unit test your application
have a look at the gist here this will give you some idea
https://gist.github.com/AikoPath/050ad0ffb91d628d4b10ef81736af386/raw/846c7bcfc54be8cce78eba8d12015bf749b91eee/#ViewChild(ComponentUnderTestComponent).js
More over complete reading over here carefully otherwise you can easily get confused again -
https://betterprogramming.pub/testing-angular-components-with-input-3bd6c07cfaf6