AngularJS, Jasmine, Chutzpah... Unit testing a filter? - unit-testing

I am just trying to get a better understanding of Jasmine unit tests with AngularJS. So I setup a test around an existing experimental project I started. I'll just start off with the code, my question is at the bottom.
First my app declaration:
(function () {
"use strict";
angular.module('iconic', [])
.constant('config', {
debug: true,
version: '1.0.0.1'
})
.value('globalStatus', {
currentArea: null,
progress: null,
notice: [
{ title: 'Notice 1 Title', message: 'Notice 1 Message' },
{ title: 'Notice 2 Title', message: 'Notice 1 Message' }
]
});
}());
Then a factory to get data (static now but would be an AJAX call):
(function () {
"use strict";
angular.module('iconic')
.factory('data', ['$http', function ($http) {
function areas() {
return [
{ name: 'home', text: 'Home', enabled: true, active: false },
{ name: 'gallery', text: 'Gallery', enabled: true, active: false },
{ name: 'services', text: 'Services', enabled: true, active: false },
{ name: 'pricing', text: 'Pricing', enabled: true, active: false },
{ name: 'test', text: 'Test', enabled: false, active: false }
];
}
return {
getAreas: areas
};
}]);
}());
Then my controller with the filter:
(function () {
"use strict";
angular.module('iconic')
.controller('NavController', ['$scope', 'data', function ($scope, data) {
$scope.menus = data.getAreas();
}])
.filter('EnabledFilter', ['config', function (config) {
return function (menus) {
if (config.debug)
console.log('matchEnabled', arguments);
var filtered = [];
angular.forEach(menus, function (menu) {
if (menu.enabled) {
filtered.push(menu);
}
});
return filtered;
};
}]);
}());
And then my actual Jasmine test (running this with Chutzpah):
(function () {
"use strict";
var staticData = [
{ name: 'home', text: 'Home', enabled: true, active: false },
{ name: 'gallery', text: 'Gallery', enabled: true, active: false },
{ name: 'services', text: 'Services', enabled: true, active: false },
{ name: 'pricing', text: 'Pricing', enabled: true, active: false },
{ name: 'test', text: 'Test', enabled: false, active: false }
];
describe("NavController Tests", function () {
//Mocks
//Mocks
var windowMock, httpBackend, _data;
//Controller
var ctrl;
//Scope
var ctrlScope;
//Data
var storedItems;
beforeEach(function () {
module('iconic');
});
beforeEach(inject(function ($rootScope, $httpBackend, $controller, data) {
//Mock for $window service
windowMock = { location: { href: "" } };
//Creating a new scope
ctrlScope = $rootScope.$new();
//Assigning $httpBackend mocked service to httpBackend object
httpBackend = $httpBackend;
_data = data;
storedItems = staticData;
//Creating spies for functions of data service
spyOn(data, 'getAreas').andCallThrough();
$controller('NavController', { $scope: ctrlScope, data: _data});
}));
it("should call getAreas on creation of controller", function () {
expect(_data.getAreas).toHaveBeenCalled();
});
});
}());
So this first and simple test to make sure getAreas gets called passes just fine. But I would like to add a test to basically make sure that the filter result is filtering out data from the factory where enabled is false. Any idea how I would go about doing that with Jasmine?

Related

How Angular 9 unit test must be written?

I am new to Angular and it's very hard for me to understand how to write a unit test for a piece of code. Can someone please explain to me how to write a unit test for the following code?
toggleClassActive(station: any): void {
this.isDisabled = false;
this.stationsList.map((st) => {
if (st.id === station.id) {
st.active = true;
} else {
st.active = false;
}
});
}
Given this example component
export class FooComponent {
isDisabled = true;
stationsList: Array<{ id: number, active: boolean }> = [
{id: 1, active: false}, {id: 2, active: true},
];
constructor() {
}
toggleClassActive(station: { id: number, active: boolean }): void {
this.isDisabled = false;
this.stationsList.map((st) => {
if (st.id === station.id) {
st.active = true;
} else {
st.active = false;
}
});
}
}
Here's a unit test for that very method
import {TestBed} from '#angular/core/testing';
import {FooComponent} from './foo.component';
describe('FooComponent', () => {
let component: FooComponent;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
FooComponent,
],
});
component = TestBed.inject(FooComponent);
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should toggle active class', () => {
// Optionally test the initial values // state
expect(component.stationsList).toEqual([
{id: 1, active: false}, {id: 2, active: true},
]);
expect(component.isDisabled).toEqual(true);
// Trigger the effect
component.toggleClassActive({id: 1, active: false});
// Assert the expected changes
expect(component.stationsList).toEqual([
{id: 1, active: true}, {id: 2, active: false},
]);
expect(component.isDisabled).toEqual(false);
});
});

Aurelia unit testing access component's viewModel

I am unit testing one of my components in an Aurelia project. I'd like to access my component's viewModel in my unit test but haven't had any luck so far.
I followed the example available at https://aurelia.io/docs/testing/components#manually-handling-lifecycle but I keep getting component.viewModel is undefined.
Here is the unit test:
describe.only('some basic tests', function() {
let component, user;
before(() => {
user = new User({ id: 100, first_name: "Bob", last_name: "Schmoe", email: 'joe#schmoe.com'});
user.save();
});
beforeEach( () => {
component = StageComponent
.withResources('modules/users/user')
.inView('<user></user>')
.boundTo( user );
});
it('check for ', () => {
return component.create(bootstrap)
.then(() => {
expect(2).to.equal(2);
return component.viewModel.activate({user: user});
});
});
it('can manually handle lifecycle', () => {
return component.manuallyHandleLifecycle().create(bootstrap)
.then(() => component.bind({user: user}))
.then(() => component.attached())
.then(() => component.unbind() )
.then(() => {
expect(component.viewModel.name).toBe(null);
return Promise.resolve(true);
});
});
afterEach( () => {
component.dispose();
});
});
Here is the error I get:
1) my aurelia tests
can manually handle lifecycle:
TypeError: Cannot read property 'name' of undefined
Here is the the line that defines the viewModel on the component object but only if aurelia.root.controllers.length is set. I am not sure how to set controllers in my aurelia code or if I need to do so at all.
I guess my question is:
How do I get access to a component's viewModel in my unit tests?
Edit #2:
I'd also like to point out that your own answer is essentially the same solution as the one I first proposed in the comments. It is the equivalent of directly instantiating your view model and not verifying whether the component is actually working.
Edit:
I tried this locally with a karma+webpack+mocha setup (as webpack is the popular choice nowadays) and there were a few caveats with getting this to work well. I'm not sure what the rest of your setup is, so I cannot tell you precisely where the error was (I could probably point this out if you told me more about your setup).
In any case, here's a working setup with karma+webpack+mocha that properly verifies the binding and rendering:
https://github.com/fkleuver/aurelia-karma-webpack-testing
The test code:
import './setup';
import { Greeter } from './../src/greeter';
import { bootstrap } from 'aurelia-bootstrapper';
import { StageComponent, ComponentTester } from 'aurelia-testing';
import { PLATFORM } from 'aurelia-framework';
import { assert } from 'chai';
describe('Greeter', () => {
let el: HTMLElement;
let tester: ComponentTester;
let sut: Greeter;
beforeEach(async () => {
tester = StageComponent
.withResources(PLATFORM.moduleName('greeter'))
.inView(`<greeter name.bind="name"></greeter>`)
.manuallyHandleLifecycle();
await tester.create(bootstrap);
el = <HTMLElement>tester.element;
sut = tester.viewModel;
});
it('binds correctly', async () => {
await tester.bind({ name: 'Bob' });
assert.equal(sut.name, 'Bob');
});
it('renders correctly', async () => {
await tester.bind({ name: 'Bob' });
await tester.attached();
assert.equal(el.innerText.trim(), 'Hello, Bob!');
});
});
greeter.html
<template>
Hello, ${name}!
</template>
greeter.ts
import { bindable } from 'aurelia-framework';
export class Greeter {
#bindable()
public name: string;
}
setup.ts
import 'aurelia-polyfills';
import 'aurelia-loader-webpack';
import { initialize } from 'aurelia-pal-browser';
initialize();
karma.conf.js
const { AureliaPlugin } = require('aurelia-webpack-plugin');
const { resolve } = require('path');
module.exports = function configure(config) {
const options = {
frameworks: ['source-map-support', 'mocha'],
files: ['test/**/*.ts'],
preprocessors: { ['test/**/*.ts']: ['webpack', 'sourcemap'] },
webpack: {
mode: 'development',
entry: { setup: './test/setup.ts' },
resolve: {
extensions: ['.ts', '.js'],
modules: [
resolve(__dirname, 'src'),
resolve(__dirname, 'node_modules')
]
},
devtool: 'inline-source-map',
module: {
rules: [{
test: /\.html$/i,
loader: 'html-loader'
}, {
test: /\.ts$/i,
loader: 'ts-loader',
exclude: /node_modules/
}]
},
plugins: [new AureliaPlugin()]
},
singleRun: false,
colors: true,
logLevel: config.browsers && config.browsers[0] === 'ChromeDebugging' ? config.LOG_DEBUG : config.LOG_INFO, // for troubleshooting mode
mime: { 'text/x-typescript': ['ts'] },
webpackMiddleware: { stats: 'errors-only' },
reporters: ['mocha'],
browsers: config.browsers || ['ChromeHeadless'],
customLaunchers: {
ChromeDebugging: {
base: 'Chrome',
flags: [ '--remote-debugging-port=9333' ]
}
}
};
config.set(options);
};
tsconfig.json
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"importHelpers": true,
"lib": ["es2018", "dom"],
"module": "esnext",
"moduleResolution": "node",
"sourceMap": true,
"target": "es2018"
},
"include": ["src"]
}
package.json
{
"scripts": {
"test": "karma start --browsers=ChromeHeadless"
},
"dependencies": {
"aurelia-bootstrapper": "^2.3.0",
"aurelia-loader-webpack": "^2.2.1"
},
"devDependencies": {
"#types/chai": "^4.1.6",
"#types/mocha": "^5.2.5",
"#types/node": "^10.12.0",
"aurelia-testing": "^1.0.0",
"aurelia-webpack-plugin": "^3.0.0",
"chai": "^4.2.0",
"html-loader": "^0.5.5",
"karma": "^3.1.1",
"karma-chrome-launcher": "^2.2.0",
"karma-mocha": "^1.3.0",
"karma-mocha-reporter": "^2.2.5",
"karma-source-map-support": "^1.3.0",
"karma-sourcemap-loader": "^0.3.7",
"karma-webpack": "^3.0.5",
"mocha": "^5.2.0",
"path": "^0.12.7",
"ts-loader": "^5.2.2",
"typescript": "^3.1.3",
"webpack": "^4.23.1",
"webpack-dev-server": "^3.1.10"
}
}
Original answer
If you're manually doing the lifecycle, you need to pass in a ViewModel yourself that it can bind to :)
I don't remember exactly what's strictly speaking needed so I'm quite sure there's some redundancy (e.g. one of the two bindingContexts passed in shouldn't be necessary). But this is the general idea:
const view = "<div>${msg}</div>";
const bindingContext = { msg: "foo" };
StageComponent
.withResources(resources/*optional*/)
.inView(view)
.boundTo(bindingContext)
.manuallyHandleLifecycle()
.create(bootstrap)
.then(component => {
component.bind(bindingContext);
}
.then(component => {
component.attached();
}
.then(component => {
expect(component.host.textContent).toEqual("foo");
}
.then(component => {
bindingContext.msg = "bar";
}
.then(component => {
expect(component.host.textContent).toEqual("bar");
};
Needless to say, since you create the view model yourself (the variable bindingContext in this example), you can simply access the variable you declared.
In order to get it to work, I had to use Container:
import { UserCard } from '../../src/modules/users/user-card';
import { Container } from 'aurelia-dependency-injection';
describe.only('some basic tests', function() {
let component, user;
before(() => {
user = new User({ id: 100, first_name: "Bob", last_name: "Schmoe", email: 'joe#schmoe.com'});
user.save();
});
beforeEach(() => {
container = new Container();
userCard = container.get( UserCard );
component = StageComponent
.withResources('modules/users/user-card')
.inView('<user-card></user-card>')
.boundTo( user );
});
it('check for ', () => {
return component.create(bootstrap)
.then(() => {
expect(2).to.equal(2);
return userCard.activate({user: user});
});
});
});

Unit testing a React function in component

I need a way to unit test the "edit" function in the code below. It is different form the other questions because my function returns no value.
class QslTable extends React.PureComponent {
this.state = {
data: [{
key: '0',
callSign: {
editable: true,
value: 'F1ABD',
},
QSODate: {
editable: true,
value: '10-05-31',
},
band: {
editable: true,
value: '32',
},
mode: {
editable: true,
value: 'Phone',
},
dxccEntity: {
editable: true,
value: 'France',
},
delete: {
editable: false,
value: 'Delete',
},
}],
};
edit(index) {
const { data } = this.state;
Object.keys(data[index]).forEach((item) => {
if (data[index][item] && typeof data[index][item].editable !== 'undefined') {
data[index][item].editable = true;
}
});
this.setState({ data });
}
}
This doesn't work:
it('should render the component', () => {
const renderedComponent = shallow(<QslTable />);
expect(renderedComponent.find('edit')).toBeDefined();
});
The function doesn't return any value. I have no clue how to test the function. I have looked at the documentation for Jest, but couldn't figure it out. Any help would be appreciated!

Implementing Batch Update in Kendo UI GRID not work

While trying to perform batch update, I am not able to post values to MVC WEB API controller neither I am getting Record IDs in mu PUT controller.
I have already visited some of the links egarding same problem but got no solution.
$(document).ready(function () {
debugger;
var webapiUrl = (My webapi);
dataSource = new kendo.data.DataSource({
type: "json",
transport: {
read: {
url: webapiUrl + api/GetProductsByShipID/1",
contentType: "application/json",
},
update: {
url: webapiUrl + api/OpportunityProducts/1",
contentType: "application/json",
type: "PUT"
},
destroy: {
url: webapiUrl + /api/OpportunityProducts/",
contentType: "application/json",
type: "DELETE"
},
create: {
url: webapiUrl + /api/OpportunityProducts/",
contentType: "application/json",
type: "POST"
},
parameterMap: function (options, operation) {
if (operation !== "read") {
return options;
}
}
},
batch: true,
pageSize: 10,
schema: {
model: {
id: "ID",
fields: {
ID: { editable: false, nullable: true },
ProductDesc: { type: "string" },
Quantity: {type: "number"},
UnitPrice: { type: "number"}
}
}
});
$("#grid").kendoGrid({
dataSource: dataSource,
navigatable: true,
pageable: true,
toolbar: ["create", "save", "cancel"],
columns: [
"ProductName",
{ field: "ProductDesc", title: "Product Desc"},
{ field: "Quantity", title: "Quantity" },
{ field: "UnitPrice", width: 120 },
{ command: "destroy", title: " ", width:150 }],
editable: true
});
});
</script>
Well after some workaround, late night I was able to modify parameterMap section of my kendo grid which lead me to expected output.
This is how I updated my parameterMap section...
Previous
parameterMap: function (options, operation) {
if (operation !== "read") {
return options;
}
}
Updated
parameterMap: function (options, operation) {
debugger;
if (operation !== "read" && options.models) {
var webapiUrl = (my webapi);
var i = 0;
for (i = 0; i < options.models.length; i++) {
$.ajax({
cache: false,
async: true,
type: "PUT",
url: webapiUrl + "/api/OpportunityProducts/" + options.models[i].Id,
data: {
ID: options.models[i].ID,
ProductDesc: options.models[i].ProductDesc,
Quantity: options.models[i].Quantity
},
success: function (data) {
},
error: function (jqXHR, exception) {
alert(exception);
}
});
}

Angular scope in jasmine controller tests

Trying to understand how do jasmine tests work.
I've got a module and a controller:
var app = angular.module('planApp', []);
app.controller('PlanCtrl', function($scope, plansStorage){
var plans = $scope.plans = plansStorage.get();
$scope.formHidden = true;
$scope.togglePlanForm = function() {
this.formHidden = !this.formHidden;
};
$scope.newPlan = {title: '', description: ''} ;
$scope.$watch('plans', function() {
plansStorage.put(plans);
}, true);
$scope.addPlan = function() {
var newPlan = {
title: $scope.newPlan.title.trim(),
description: $scope.newPlan.description
};
if (!newPlan.title.length || !newPlan.description.length) {
return;
}
plans.push({
title: newPlan.title,
description: newPlan.description
});
$scope.newPlan = {title: '', description: ''};
$scope.formHidden = true;
};
});
plansStorage.get() is a method of a service that gets a json string from localstorage and returns an object.
When I run this test:
var storedPlans = [
{
title: 'Good plan',
description: 'Do something right'
},
{
title: 'Bad plan',
description: 'Do something wrong'
}
];
describe('plan controller', function () {
var ctrl,
scope,
service;
beforeEach(angular.mock.module('planApp'));
beforeEach(angular.mock.inject(function($rootScope, $controller, plansStorage) {
scope = $rootScope.$new();
service = plansStorage;
spyOn(plansStorage, 'get').andReturn(storedPlans);
ctrl = $controller('PlanCtrl', {
$scope: scope,
plansStorage: service
});
spyOn(scope, 'addPlan')
}));
it('should get 2 stored plans', function(){
expect(scope.plans).toBeUndefined;
expect(service.get).toHaveBeenCalled();
expect(scope.plans).toEqual([
{
title: 'Good plan',
description: 'Do something right'
},
{
title: 'Bad plan',
description: 'Do something wrong'
}
]);
});
it('should add a plan', function() {
scope.newPlan = {title: 'new', description: 'plan'};
expect(scope.newPlan).toEqual({title: 'new', description: 'plan'});
scope.addPlan();
expect(scope.addPlan).toHaveBeenCalled();
expect(scope.plans.length).toEqual(3);
});
});
first test passes ok, but second one fails. The length of the scope.plans expected to be 3, but it is 2. scope.plans didn't change after scope.addPlan() call.
If I understand that right, the $scope inside addPlan method is not the same as scope that I trying to test in second test.
The question is why? And how do I test the addPlan method?
the solution is just to add andCallThrough() method after spy:
spyOn(scope, 'addPlan').andCallThrough()