Related
I am trying to test AsyncTypeahead from react-bootstrap-typeahead.
I have a very simple test component :
class AsyncTypeahead2 extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
};
}
render() {
return ( <AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/> )
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id:1, stateName:"Alaska"},
{id:2, stateName:"Alabama"}
]
},
);
(Note that the URL is mocked to return two elements)
When I run this in my storybook it looks fine :
But if I want to test it (with Enzyme) it does not recognise the < li > items that pop up.
let Compoment =
<div>Basic AsyncTypeahead Example
<AsyncTypeahead2/>
</div>
const wrapper = mount(Compoment);
let json = wrapper.html();
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
Instead of finding two "dropdown-item" items there is just one item with the text "Type to Search...".
Is the AynchTypeahead not updating the DOM correctly with respect to Enzyme?
<AsyncTypeahead> is asynchronous. On the other hand simulate() is synchronous. So at the time you get to expect() AsyncTypeahead not even started to populate the dropdown with <li> elements. You need to wait for it.
It's not specified, but it looks like you are using fetch-mock package.
There is the flush function which
Returns a Promise that resolves once all fetches handled by fetch-mock have resolved
So this:
...
sel.simulate('click');
sel.simulate('change', { target: { value: "al" } });
await fetchMock.flush() // !!!
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
expect(wrapper.find(".dropdown-item").length).toBe(2)
should work.
...But probably it won't. Because
fetchMock.mock(...)
fetch(...)
await fetchMock.flush()
does work, but
fetchMock.mock(...)
setTimeout(() => fetch(...), 0)
await fetchMock.flush()
does not. await fetchMock.flush() returns right away if there was no call of fetch. And probably there won't be. Because <AsyncTypeahead> debounces.
(By the way, you can also try to mock fetch on a per-test basis. Just in case.)
So I see two options:
Use something else instead of fetch-mock package. Where you can resolve your own Promises on mocked requests completion.
https://tech.travelaudience.com/how-to-test-asynchronous-data-fetching-on-a-react-component-ff2ee7433d71
import waitUntil from 'async-wait-until';
...
test("test name", async () => {
let Compoment = <AsyncTypeahead2/>
...
await waitUntil(() => wrapper.state().isLoading === false);
// or even
// await waitUntil(() => wrapper.find(".dropdown-item").length === 2, timeout);
expect(...)
})
This options if not pretty. But maybe it's your only option - there is not only the fetch-mock you should worry about. setState also asynchronous... and it looks like there is no pretty way to check when it's done updating the state and the DOM without changing the real code (which is quite undesirable).
The exact solution to my problem is in the following code (copy and paste into a JS file to see it work).
Things to note :
I needed to use the waitUntil function from the async-wait-until library. fetch-mock on its own does not provide the functionality to test async code.
I needed to add an ugly hack at global.document.createRange because of some tooltip issue with react-bootstrap-typeahead and jest.
use waitUntil to wait on changes on the internal state of the component
It is very important to call wrapper.update() to update the DOM afterwards.
..
import React, {Component} from 'react';
import waitUntil from 'async-wait-until';
import {mount} from "enzyme";
import fetchMock from "fetch-mock";
import {AsyncTypeahead} from "react-bootstrap-typeahead";
describe('Autocomplete Tests ', () => {
test(' Asynch AutocompleteInput ', async () => {
class AsyncTypeaheadExample extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
isLoading: false,
finished: false
};
}
render() {
return (<AsyncTypeahead
isLoading={this.state.isLoading}
onSearch={query => {
this.setState({isLoading: true});
fetch("http://www.myHTTPenpoint.com")
.then(resp => resp.json())
.then(json => this.setState({
isLoading: false,
options: json.items,
finished: true
}));
}}
options={this.state.options}
labelKey={option => `${option.stateName}`}
/>)
}
}
const url = "http://www.myHTTPenpoint.com"
fetchMock
.reset()
.get(
url,
{
items: [
{id: 1, stateName: "Alaska"},
{id: 2, stateName: "Alabama"}
]
},
);
let Compoment =
<AsyncTypeaheadExample/>
// ugly hacky patch to fix some tooltip bug
// https://github.com/mui-org/material-ui/issues/15726
global.document.createRange = () => ({
setStart: () => {
},
setEnd: () => {
},
commonAncestorContainer: {
nodeName: 'BODY',
ownerDocument: document,
},
});
let wrapper = mount(Compoment);
let sel = wrapper.find(".rbt-input-main").at(0)
sel.simulate('click');
sel.simulate('change', {target: {value: "al"}});
expect(wrapper.find(".rbt-input-main").at(0).getElement().props.value).toBe("al")
//now the async stuff is happening ...
await waitUntil(() => {
return wrapper.state().finished === true;
}, 3000); //wait about 3 seconds
wrapper.update() //need to update the DOM!
expect(wrapper.find(".dropdown-item").length).toBe(2) //but get just 1 element "Type to Search..."
})
});
UPDATE
I can also compare on wrapper items rather than doing a direct comparison on the state :
//now the async stuff is happening ...
await waitUntil(() => {
wrapper.update() //need to update the DOM!
return wrapper.find(".dropdown-item").length > 1
}, 3000); //wait about 3 seconds
This is probably better because it means i dont need to know about the component internals.
I'm trying to write tests for a react component I've built that utilizes navigator.geolocation.getCurrentPosition() within a method like so (rough example of my component):
class App extends Component {
constructor() {
...
}
method() {
navigator.geolocation.getCurrentPosition((position) => {
...code...
}
}
render() {
return(...)
}
}
I'm using create-react-app, which includes a test:
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<App />, div);
});
This test fails, printing out this in the console:
TypeError: Cannot read property 'getCurrentPosition' of undefined
I'm new to React, but have quite a bit of experience with angular 1.x. In angular it is common to mock out (within the tests in a beforeEach) functions, "services", and global object methods like navigator.geolocation.etc. I spent time researching this issue and this bit of code is the closest I could get to a mock:
global.navigator = {
geolocation: {
getCurrentPosition: jest.fn()
}
}
I put this in my test file for App, but it had no effect.
How can I "mock" out this navigator method and get the test to pass?
EDIT: I looked into using a library called geolocation which supposedly wraps navigator.getCurrentPosition for use in a node environment. If I understand correctly, jest runs tests in a node environment and uses JSDOM to mock out things like window. I haven't been able to find much information on JSDOM's support of navigator. The above mentioned library did not work in my react app. Using the specific method getCurrentPosition would only return undefined even though the library itself was imported correctly and available within the context of the App class.
It appears that there is already a global.navigator object and, like you, I wasn't able to reassign it.
I found that mocking the geolocation part and adding it to the existing global.navigator worked for me.
const mockGeolocation = {
getCurrentPosition: jest.fn(),
watchPosition: jest.fn()
};
global.navigator.geolocation = mockGeolocation;
I added this to a src/setupTests.js file as described here - https://create-react-app.dev/docs/running-tests#initializing-test-environment
I know this issue might have been solved, but seems that all the solutions above are all wrong, at least for me.
When you do this mock: getCurrentPosition: jest.fn()
it returns undefined, if you want to return something, this is the correct implementation:
const mockGeolocation = {
getCurrentPosition: jest.fn()
.mockImplementationOnce((success) => Promise.resolve(success({
coords: {
latitude: 51.1,
longitude: 45.3
}
})))
};
global.navigator.geolocation = mockGeolocation;
I am using create-react-app
A TypeScript version for anyone that was getting
Cannot assign to 'geolocation' because it is a read-only property.
In the mockNavigatorGeolocation.ts file (this can live in a test-utils folder or similar)
export const mockNavigatorGeolocation = () => {
const clearWatchMock = jest.fn();
const getCurrentPositionMock = jest.fn();
const watchPositionMock = jest.fn();
const geolocation = {
clearWatch: clearWatchMock,
getCurrentPosition: getCurrentPositionMock,
watchPosition: watchPositionMock,
};
Object.defineProperty(global.navigator, 'geolocation', {
value: geolocation,
});
return { clearWatchMock, getCurrentPositionMock, watchPositionMock };
};
I then import this in my test at the top of the file:
import { mockNavigatorGeolocation } from '../../test-utils';
And then use the function like so:
const { getCurrentPositionMock } = mockNavigatorGeolocation();
getCurrentPositionMock.mockImplementation((success, rejected) =>
rejected({
code: '',
message: '',
PERMISSION_DENIED: '',
POSITION_UNAVAILABLE: '',
TIMEOUT: '',
})
);
Mocking with setupFiles
// __mocks__/setup.js
jest.mock('Geolocation', () => {
return {
getCurrentPosition: jest.fn(),
watchPosition: jest.fn(),
}
});
and then in your package.json
"jest": {
"preset": "react-native",
"setupFiles": [
"./__mocks__/setup.js"
]
}
I followed #madeo's comment above to mock global.navigator.geolocation. It worked!
Additionally I did the following to mock global.navigator.permissions:
global.navigator.permissions = {
query: jest
.fn()
.mockImplementationOnce(() => Promise.resolve({ state: 'granted' })),
};
Set state to any of granted, denied, prompt as per requirement.
For whatever reason, I did not have the global.navigator object defined, so I had to specify it in my setupTests.js file
const mockGeolocation = {
getCurrentPosition: jest.fn(),
watchPosition: jest.fn(),
}
global.navigator = { geolocation: mockGeolocation }
Added to the above answers, if you want to update navigator.permissions, this will work.The key here is to mark writable as true before mocking
Object.defineProperty(global.navigator, "permissions", {
writable: true,
value: {
query : jest.fn()
.mockImplementation(() => Promise.resolve({ state: 'granted' }))
},
});
I am using a directive to get the data from input used as a filter text.
here is my hostlistener in the directive:
#HostListener('input', ['$event.target.value'])
public onChangeFilter(event: any): void {
console.log('input event fired, value: ' + event);
this.columnFiltering.filterString = event;
this.filterChanged.emit({filtering: this.columnFiltering});
}
this code is working perfectly, I am unable to unit test the same.
I have subscribed to the filterChanged EventEmitter, in my unit test to check the value.
I tried simulating keypress event to change value and also tried settings value attribute. None of these is working for me.
here is my spec file:
describe('Table View', () => {
let fixture: ComponentFixture<any>;
let context: TableComponent;
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
TableComponent,
],
imports: [TableModule],
});
fixture = TestBed.createComponent(TableComponent);
context = fixture.componentInstance;
});
it('should allow filter', () => {
const element = fixture.nativeElement;
context.config = config;
fixture.detectChanges();
let tableChangeCount = 0;
let tableEvent: any;
context.tableChanged.subscribe((event: any) => {
tableChangeCount++;
tableEvent = event;
});
// Check if table exists
let inputElement = element.querySelectorAll('tr')[1].querySelector('input');
let e = new KeyboardEvent("keypress", {
key: "a",
bubbles: true,
cancelable: true,
});
inputElement.dispatchEvent(e);
});
});
I tried setting value:
let attrs = inputElement.attributes;
inputElement.setAttribute('value', 'abc');
for (let i = attrs.length - 1; i >= 0; i--) {
// Attribute value is set correctly
if (attrs[i].name === 'value') {
console.log(attrs[i].name + "->" + attrs[i].value);
}
}
Can anyone please help me, how can I unit test the same?
I've had some trouble simulating a keypress in a unit test also. But came across an answer by Seyed Jalal Hosseini. It might be what you're after.
If you're attempting to simulate a keypress you can trigger an event by calling dispatchEvent(new Event('keypress')); on the element.
Here is the answer I'm referring to which gives more detail : https://stackoverflow.com/a/37956877/4081730
If you want to set the key that was pressed, this can be done also.
const event = new KeyboardEvent("keypress",{
"key": "Enter"
});
el.dispatchEvent(event);
Further information I've just come across: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
If you wish to use a key code (or "which"), you can do this:
// #HostListener('document:keypress')
const escapeEvent: any = document.createEvent('CustomEvent');
escapeEvent.which = 27;
escapeEvent.initEvent('keypress', true, true);
document.dispatchEvent(escapeEvent);
it('should trigger a TAB keypress event on an element', () => {
const tabKeypress = new KeyboardEvent('keypress', {
// #ts-ignore
keyCode: 9, // Tab Key
cancelable: true
});
const myTableEle = debugEle.nativeElement.querySelector('.your-element');
myTableEle.dispatchEvent(tabKeypress);
fixture.detectChanges();
});
// #ts-ignore :- is to remove TS warning because keyCode is deprecated. Its not needed in case you want to set "key" property of KeyboardEvent.
So I've run into another snag, which I'm fighting with... I have a method that is a sync call, and within this method it calls a promise, async, method.
in my app I have the following:
export class App {
constructor(menuService) {
_menuService = menuService;
this.message = "init";
}
configureRouter(config, router) {
console.log('calling configureRouter');
_menuService.getById(1).then(menuItem => {
console.log('within then');
console.log(`configureRouter ${JSON.stringify(menuItem, null, 2)}`);
const collection = menuItem.links.map(convertToRouteCollection);
console.log(`collection ${JSON.stringify(collection, null, 2)}`);
//I think there is an issue with asyn to synch for the test
config.map(collection);
}).catch(err => {
console.error(err);
});
console.log('calling configureRouter assign router');
this.router = router;
}
}
The test I've tried the following within mocha
...
it('should update router config', function () {
const expectedData = {
name: "main menu",
links: [{
url: '/one/two',
name: 'link name',
title: 'link title'
}]
};
const configMapStub = sinon.stub();
const config = {
map: configMapStub
};
const routerMock = sinon.stub();
let app = null;
const actualRouter = null;
let menuService = null;
setTimeout(() => {
menuService = {
getById: sinon.stub().returns(Promise.resolve(expectedData).delay(1))
};
app = new App(menuService);
app.configureRouter(config, routerMock);
}, 10);
clock.tick(30);
expect(app.router).to.equal(routerMock);
expect(menuService.getById.calledWith(1)).to.equal(true);
//console.log(configMapStub.args);
expect(configMapStub.called).to.equal(true);
const linkItem = expectedData.links[0];
const actual = [{
route: ['', 'welcome'],
name: linkItem.name,
moduleId: linkItem.name,
nav: true,
title: linkItem.title
}];
console.log(`actual ${JSON.stringify(actual, null, 2)}`);
expect(config.map.calledWith(actual)).to.equal(true);
});
...
No matter what, I get configMockStub to always get false, while I am getting the menuService.getById.calledWith(1).to.equal(true) to equal true.
The test above was an attempt to try and get 'time' to pass. I've tried it without and have equally failed.
I'm really striking out on ideas on how to test this. Maybe I have the code wrong to reference a promise inside this method.
The only thing I can say I don't have any choice over the configureRouter method. Any guidance is appreciated.
Thanks!
Kelly
Short answer:
I recently discovered I was trying to make configureRouter method be a synchronous call (making it use async await keywords). What I found out was Aurelia does allow that method to be promised. Because of this, the test in question is no longer an issue.
Longer answer:
The other part of this is that I had a slew of babel issues lining up between babelling for mocha, and then babelling for wallaby.js. For some reason these two were not playing well together.
in the test above, another thing was to also change the following:
it('should update router config', function () {
to
it('should update router config', async function () {
I feel like there was another step, but at this time I cannot recall. In either case, knowing that I could use a promise made my world much easier for Aurelia.
I'm setting up unit tests on my Sails application's models, controllers and services.
I stumbled upon a confusing issue, while testing my User model. Excerpt of User.js:
module.exports = {
attributes: {
username: {
type: 'string',
required: true
},
[... other attributes...] ,
isAdmin: {
type: 'boolean',
defaultsTo: false
},
toJSON: function() {
var obj = this.toObject();
// Don't send back the isAdmin attribute
delete obj.isAdmin;
delete obj.updatedAt;
return obj;
}
}
}
Following is my test.js, meant to be run with mocha. Note that I turned on the pluralize flag in blueprints config. Also, I use sails-ember-blueprints, in order to have Ember Data-compliant blueprints. So my request has to look like {user: {...}}.
// Require app factory
var Sails = require('sails/lib/app');
var assert = require('assert');
var request = require('supertest');
// Instantiate the Sails app instance we'll be using
var app = Sails();
var User;
before(function(done) {
// Lift Sails and store the app reference
app.lift({
globals: true,
// load almost everything but policies
loadHooks: ['moduleloader', 'userconfig', 'orm', 'http', 'controllers', 'services', 'request', 'responses', 'blueprints'],
}, function() {
User = app.models.user;
console.log('Sails lifted!');
done();
});
});
// After Function
after(function(done) {
app.lower(done);
});
describe.only('User', function() {
describe('.update()', function() {
it('should modify isAdmin attribute', function (done) {
User.findOneByUsername('skippy').exec(function(err, user) {
if(err) throw new Error('User not found');
user.isAdmin = false;
request(app.hooks.http.app)
.put('/users/' + user.id)
.send({user:user})
.expect(200)
.expect('Content-Type', /json/)
.end(function() {
User.findOneByUsername('skippy').exec(function(err, user) {
assert.equal(user.isAdmin, false);
done();
});
});
});
});
});
});
Before I set up a policy that will prevent write access on User.isAdmin, I expect my user.isAdmin attribute to be updated by this request.
Before running the test, my user's isAdmin flag is set to true. Running the test shows the flag isn't updated:
1) User .update() should modify isAdmin attribute:
Uncaught AssertionError: true == false
This is even more puzzling since the following QUnit test, run on client side, does update the isAdmin attribute, though it cannot tell if it was updated, since I remove isAdmin from the payload in User.toJSON().
var user;
module( "user", {
setup: function( assert ) {
stop(2000);
// Authenticate with user skippy
$.post('/auth/local', {identifier: 'skippy', password: 'Guru-Meditation!!'}, function (data) {
user = data.user;
}).always(QUnit.start);
}
, teardown: function( assert ) {
$.get('/logout', function(data) {
});
}
});
asyncTest("PUT /users with isAdmin attribute should modify it in the db and return the user", function () {
stop(1000);
user.isAdmin = true;
$.ajax({
url: '/users/' + user.id,
type: 'put',
data: {user: user},
success: function (data) {
console.log(data);
// I can not test isAdmin value here
equal(data.user.firstName, user.firstName, "first name should not be modified");
start();
},
error: function (reason) {
equal(typeof reason, 'object', 'reason for failure should be an object');
start();
}
});
});
In the mongoDB console:
> db.user.find({username: 'skippy'});
{ "_id" : ObjectId("541d9b451043c7f1d1fd565a"), "isAdmin" : false, ..., "username" : "skippy" }
Yet even more puzzling, is that commenting out delete obj.isAdmin in User.toJSON() makes the mocha test pass!
So, I wonder:
Is the toJSON() method on Waterline models only used for output filtering? Or does it have an effect on write operations such as update().
Might this issue be related to supertest? Since the jQuery.ajax() in my QUnit test does modify the isAdmin flag, it is quite strange that the supertest request does not.
Any suggestion really appreciated.