SimpleDialog.jsx
const [imagePreview, setImagePreview] = React.useState(null);
const handleChangeImage = event => {
let reader = new FileReader();
let file = event.target.files[0];
reader.onload = event => {
console.log(event);
setImagePreview(event.target.result);
};
reader.readAsDataURL(file);
};
return (
<div>
<input
accept="image/*"
id="contained-button-file"
multiple
type="file"
style={{ display: 'none' }}
onChange={handleChangeImage}
/>
<img id="preview" src={imagePreview} />
</div>
);
SimpleDialog.test.js
it('should change image src', () => {
const event = {
target: {
files: [
{
name: 'image.png',
size: 50000,
type: 'image/png'
}
]
}
};
let spy = jest
.spyOn(FileReader.prototype, 'onload')
.mockImplementation(() => null);
wrapper.find('input[type="file"]').simulate('change', event);
expect(spy).toHaveBeenCalled();
expect(wrapper.find('#preview').prop('src')).not.toBeNull();
});
When running the test it gives me the error TypeError: Illegal invocation.
Anyone who can help me with this unit test? I Just want to simulate on change if the src of an image has value or not.
The cause of the error is that onload is defined as property descriptor and assigning it to FileReader.prototype which is done by spyOn isn't supported.
There's no reason to mock onload because it's assigned in tested code and needs to be tested.
The straightforward way is to not patch JSDOM FileReader implementation but stub it entirely:
jest.spyOn(global, 'FileReader').mockImplementation(function () {
this.readAsDataURL = jest.fn();
});
wrapper.find('input[type="file"]').simulate('change', event);
let reader = FileReader.mock.instances[0];
expect(reader.readAsDataURL).toHaveBeenCalledWith(...);
expect(reader.onload).toBe(expect.any(Function));
expect(wrapper.find('#preview').prop('src')).toBeNull();
reader.onload({ target: { result: 'foo' } });
expect(wrapper.find('#preview').prop('src')).toBe('foo');
Had the same issue with checking some side effect that should be invoked on FileReader.onload, so I just ended up setting a short pause after triggering the event (I'm using enzyme + jest). Setting the timeout is probably not the best solution here, but it was the only thing that worked.
const pauseFor = milliseconds => new Promise(resolve => setTimeout(resolve, milliseconds));
...
wrapper.find('.upload-box').simulate('drop', someMockedDropEvent);
// set pause was the only way to make reader.onload to fire
await pauseFor(100);
expect(something).toEqual(something)
Related
I have a module that provides some convenience functions for DOM manipulation that I'm trying to test with Jest and jsdom, but I seem to be doing something wrong in instantiating it.
The expect test within dom.window.document.addeventlistener ('domcontentloaded'... is not passing, the console.log I added inside ../mocks/forTesting.js also does not appear when running the test.
Does anyone have any idea how to make this test work? I need the script to run and the div with id="container" change to 'test'
Here is the test code.
import { runScript } from '../export/script.js';
import { JSDOM } from 'jsdom'
const dom = new JSDOM(`<!DOCTYPE html><html><body><div id="container">Hello</div></body></html>`, { resources: 'usable', runScripts: "dangerously" })
global.window = dom.window
global.document = dom.window.document
describe("Rendering Module", () => {
test("Verify if mocked script was executed", async () => {
const script = { name: 'testScript', value: `<script src="../mocks/forTesting.js" ></script>` }
const scriptFreezed = Object.freeze(script);
await runScript(scriptFreezed, dom.window.document);
dom.window.document.addEventListener('DOMContentLoaded', () => {
const text = dom.window.document.getElementById('container').innerHTML;
expect(text).toEqual('test')
});
dom.window.document.dispatchEvent(new dom.window.Event('DOMContentLoaded', {"bubbles": true}))
})
})
The test is giving success as it was passing but is giving this error when running
console.error
Error: Uncaught [Error: expect(received).toEqual(expected) // deep equality
Expected: "test"
Received: "Hello"
Here is the code in ../mocks/forTesting.js.
window.document.addEventListener("DOMContentLoaded", function (event) {
console.log('forTesting.js')
window.document.getElementById('container').innerHTML = 'test';
});
Here is the runScript code.
const runScript = (script, dom) => {
const node = dom.createRange().createContextualFragment(script.value);
dom.head.appendChild(node);
}
I've created a custom hook within my React app, but for some reason when I update the internal state via an event listener, it causes an infinite loop to be triggered (when it shouldn't). Here's my code:
// Note that this isn't a React component - just a regular JavaScript class.
class Player{
static #audio = new Audio();
static #listenersStarted = false;
static #listenerCallbacks = {
playing: [],
paused: [],
loaded: []
};
static mount(){
const loaded = () => {
this.removeListenerCallback("loaded", loaded);
};
this.addListenerCallback("loaded", loaded);
}
// This method is called on the initialization of the React
// app and is only called once. It's only purpose is to ensure
// that all of the listeners and their callbacks get fired.
static startListeners(){
const eventShorthands = {
playing: "play playing",
paused: "pause ended",
loaded: "loadedmetadata"
};
Object.keys(eventShorthands).forEach(key => {
const actualEvents = eventShorthands[key];
actualEvents.split(" ").forEach(actualEvent => {
this.#audio.addEventListener(actualEvent, e => {
const callbacks = this.#listenerCallbacks[key];
callbacks.forEach(callback => {
callback(e)
});
});
});
});
}
static addListenerCallback(event, callback){
const callbacks = this.#listenerCallbacks;
if(callbacks.hasOwnProperty(event)){
// Remember this console log
console.log(true);
this.#listenerCallbacks[event].push(callback);
}
}
static removeListenerCallback(event, callback){
const listenerCallbacks = this.#listenerCallbacks;
if(listenerCallbacks.hasOwnProperty(event)){
const index = listenerCallbacks[event].indexOf(callback);
this.#listenerCallbacks[event].splice(index, 1);
}
}
}
const usePlayer = (slug) => {
// State setup
const [state, setState] = useReducer(
(state, newState) => ({ ...state, ...newState }), {
mounted: false,
animationRunning: false,
allowNextFrame: false
}
);
const _handleLoadedMetadata = () => {
// If I remove this _stopAnimation, the console log mentioned
// in the player class only logs true to the console 5 times.
// Whereas if I keep it, it will log true infinitely.
_stopAnimation();
};
const _stopAnimation = () => {
setState({
allowNextFrame: false,
animationRunning: false
});
}
useEffect(() => {
Player.addListenerCallback("loaded", _handleLoadedMetadata);
return () => {
Player.removeListenerCallback("loaded", _handleLoadedMetadata);
};
}, []);
return {
mounted: state.mounted
};
};
This makes me think that the component keeps on re-rendering and calling Player.addListenerCallback(), but the strange thing is, if I put a console.log(true) within the useEffect() at the end, it'll only output it twice.
All help is appreciated, cheers.
When you're hooking (pun unintended) up inner functions in React components (or hooks) to external event handlers, you'll want to be mindful of the fact that the inner function's identity changes on every render unless you use useCallback() (which is a specialization of useMemo) to guide React to keep a reference to it between renders.
Here's a small simplification/refactoring of your code that seems to work with no infinite loops.
instead of a class with only static members, Player is a regular class of which there is an app-wide singletonesque instance.
instead of hooking up separate event listeners for each event, the often-overlooked handleEvent protocol for addEventListener is used
the hook event listener callback is now properly useCallbacked.
the hook event listener callback is responsible for looking at the event.type field to figure out what's happening.
the useEffect now properly has the ref to the callback it registers/unregisters, so if the identity of the callback does change, it gets properly re-registered.
I wasn't sure what the state in your hook was used for, so it's not here (but I'd recommend three separate state atoms instead of (ab)using useDispatch for an object state if possible).
The same code is here in a Codesandbox (with a base64-encoded example mp3 that I didn't care to add here for brevity).
const SMALL_MP3 = "https://...";
class Player {
#audio = new Audio();
#eventListeners = [];
constructor() {
["play", "playing", "pause", "ended", "loadedmetadata", "canplay"].forEach((event) => {
this.#audio.addEventListener(event, this);
});
}
play(src) {
if (!this.#audio.parentNode) {
document.body.appendChild(this.#audio);
}
this.#audio.src = src;
}
handleEvent = (event) => {
this.#eventListeners.forEach((listener) => listener(event));
};
addListenerCallback(callback) {
this.#eventListeners.push(callback);
}
removeListenerCallback(callback) {
this.#eventListeners = this.#eventListeners.filter((c) => c !== callback);
}
}
const player = new Player();
const usePlayer = (slug) => {
const eventHandler = React.useCallback(
(event) => {
console.log("slug:", slug, "event:", event.type);
},
[slug],
);
React.useEffect(() => {
player.addListenerCallback(eventHandler);
return () => player.removeListenerCallback(eventHandler);
}, [eventHandler]);
};
export default function App() {
usePlayer("floop");
const handlePlay = React.useCallback(() => {
player.play(SMALL_MP3);
}, []);
return (
<div className="App">
<button onClick={handlePlay}>Set player source</button>
</div>
);
}
The output, when one clicks on the button, is
slug: floop event: loadedmetadata
slug: floop event: canplay
I'm trying to retrieve file information from input field of type="file" in my Vuejs application. I'm having a render function which renders the input fields something like this and calls a function on input, code as below:
input: (event) => {
var reader = new FileReader()
reader.readAsDataURL(event.target.files[0])
let baseFile = ''
reader.onload = function () {
baseFile = reader.result
console.log(baseFile)
};
console.log(baseFile)
const docs = {
name: event.target.files[0].name,
size: event.target.files[0].size,
lastModifiedDate: event.target.files[0].lastModifiedDate,
base64: baseFile
}
this.$emit('input', docs)
}
When I run this function console.log inside my reader.onload function gives me converted files but when I do console outside of it, the value is just an empty string. How I can I retrieve and assign to my const docs variable. Help me out with this. Thanks.
I recommend to put the rest of code inside the block of that function after changing it to an arrow function as follows :
input: (event) => {
var reader = new FileReader()
reader.readAsDataURL(event.target.files[0])
let baseFile = ''
reader.onload = () => {// <------ use arrow function
baseFile = reader.result
const docs = {
name: event.target.files[0].name,
size: event.target.files[0].size,
lastModifiedDate: event.target.files[0].lastModifiedDate,
base64: baseFile
}
this.$emit('input', docs)
};
}
My problem was that I am trying to make a unit test for a function but can't figure out how to test a part of it.
This is a react / redux action that does the following:
1) retrieves json data with an image url
2) loads the image into an Image instance and dispatches its size to the reducer (asynchronously when image is loaded using Image.onload)
3) dispatches that the fetch was completed to the reducer
The image onload happens asynchronously, so when I try to unit test it it wouldn't be called. Moreover, I can't just mock things out because the image instance is created within the function...
Here's the code I wanted to test (removing some checks, branching logic, and stuff):
export function fetchInsuranceCardPhoto() {
return dispatch => {
dispatch(requestingInsuranceCardPhoto());
return fetch(`${api}`,
{
headers: {},
credentials: 'same-origin',
method: 'GET',
})
.then(response => {
switch (response.status) {
case 200:
return response.json()
.then(json => {
dispatch(receivedInsuranceCardPhoto(json));
})
}
});
};
}
function receivedInsuranceCardPhoto(json) {
return dispatch => {
const insuranceCardFrontImg = json.insuranceCardData.url_front;
const insuranceCardBackImg = json.insuranceCardData.url_back;
if (insuranceCardFrontImg) {
dispatch(storeImageSize(insuranceCardFrontImg, 'insuranceCardFront'));
}
return dispatch(receivedInsuranceCardPhotoSuccess(json));
};
}
function receivedInsuranceCardPhotoSuccess(json) {
const insuranceCardFrontImg = json.insuranceCardData.url_front;
const insuranceCardBackImg = json.insuranceCardData.url_back;
const insuranceCardId = json.insuranceCardData.id;
return {
type: RECEIVED_INSURANCE_CARD_PHOTO,
insuranceCardFrontImg,
insuranceCardBackImg,
insuranceCardId,
};
}
function storeImageSize(imgSrc, side) {
return dispatch => {
const img = new Image();
img.src = imgSrc;
img.onload = () => {
return dispatch({
type: STORE_CARD_IMAGE_SIZE,
side,
width: img.naturalWidth,
height: img.naturalHeight,
});
};
};
}
Notice in that last storeImageSize private function how there's an instance of Image created and an image.onload that is assigned to a function.
Now here's my test:
it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned without data', async () => {
givenAPICallSucceedsWithData();
await store.dispatch(fetchInsuranceCardPhoto());
expectActionsToHaveBeenTriggered(
REQUESTING_INSURANCE_CARD_PHOTO,
RECEIVED_INSURANCE_CARD_PHOTO,
STORE_CARD_IMAGE_SIZE,
);
});
This test though will fail because the test finishes before the image.onload callback is called.
How can I force the image.onload callback to be called so that I can test that the `STORE_CARD_IMAGE_SIZE action gets broadcasted?
After some investigation, I found a very interesting javascript function that would solve my issue.
It is this: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
Here's how I used Object.defineProperty(...) to solve my issue:
describe('fetchInsuranceCardPhoto', () => {
let imageOnload = null;
/** Override Image global to save onload setting here so that I can trigger it manually in my test */
function trackImageOnload() {
Object.defineProperty(Image.prototype, 'onload', {
get: function () {
return this._onload;
},
set: function (fn) {
imageOnload = fn;
this._onload = fn;
},
});
}
it('triggers RECEIVED_INSURANCE_CARD_PHOTO when 200 returned with data', async () => {
trackImageOnload();
givenAPICallSucceedsWithData();
await store.dispatch(fetchInsuranceCardPhoto());
imageOnload();
expectActionsToHaveBeenTriggered(
REQUESTING_INSURANCE_CARD_PHOTO,
RECEIVED_INSURANCE_CARD_PHOTO,
STORE_CARD_IMAGE_SIZE,
);
});
What I did here was use define property to override the setter of any instance of Image. the setter would continue to get or set like normal but would also save the value (in this case a function) that was set to a variable in the scope of the unit test. After which, you can just run that function you captured before the verification step of your the test.
Gotchas
- configurable needs to be set
- note that defineProperty is a different function than defineProperties
- This is bad practice in real code.
- remember to use the prototype
Hope this post can help a dev in need!
It is so easy to use eventEmitter in node.js:
var e = new EventEmitter();
e.on('happy', function(){console.log('good')});
e.emit('happy');
Any client side EventEmitter in browser native?
In modern browsers, there is EventTarget.
class MyClass extends EventTarget {
doSomething() {
this.dispatchEvent(new Event('something'));
}
}
const instance = new MyClass();
instance.addEventListener('something', (e) => {
console.log('Instance fired "something".', e);
});
instance.doSomething();
Additional Resources:
Maga Zandaqo has an excellent detailed guide here: https://medium.com/#zandaqo/eventtarget-the-future-of-javascript-event-systems-205ae32f5e6b
MDN has some documentation: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
Polyfill for Safari and other incapable browsers: https://github.com/ungap/event-target
There is a NPM package named "events" which makes you able to make event emitters in a browser environment.
const EventEmitter = require('events')
const e = new EventEmitter()
e.on('message', function (text) {
console.log(text)
})
e.emit('message', 'hello world')
in your case, it's
const EventEmitter = require('events')
const e = new EventEmitter();
e.on('happy', function() {
console.log('good');
});
e.emit('happy');
This is enough for given case.
class EventEmitter{
constructor(){
this.callbacks = {}
}
on(event, cb){
if(!this.callbacks[event]) this.callbacks[event] = [];
this.callbacks[event].push(cb)
}
emit(event, data){
let cbs = this.callbacks[event]
if(cbs){
cbs.forEach(cb => cb(data))
}
}
}
Update:
I just published little bit more evolved version of it. It is very simple yet probably enough:
https://www.npmjs.com/package/alpeventemitter
Create a customized event in the client, and attach to dom element:
var event = new Event('my-event');
// Listen for the event.
elem.addEventListener('my-event', function (e) { /* ... */ }, false);
// Dispatch the event.
elem.dispatchEvent(event);
This is referred from: https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events
Thanks Naeem Shaikh
I ended up using this:
export let createEventEmitter = () => {
let callbackList: (() => any)[] = []
return {
on(callback: () => any) {
callbackList.push(callback)
},
emit() {
callbackList.forEach((callback) => {
callback()
})
},
}
}
2022 update: The BroadcatsChannel may provide a solution.
https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API
I like the answer from Alparslan above. Here's one that uses the browser CustomEvent.
let EventEmitter = (function () {
let elem = document.createElement("div")
return {
on: function (name, cb) {
elem.addEventListener(name, (e) => cb(e.detail), false )
},
emit: function (name, data) {
elem.dispatchEvent(new CustomEvent(name, {detail: data}))
}
}
})()
I have created an npm package that do the same. You can use in Javascript or Typescript
event-emitter
Example
import { EventEmitter } from 'tahasoft-event-emitter';
const onStatusChange = new EventEmitter();
function updateStatus() {
// ...
onStatusChange.emit();
}
// somewhere else, we want to add a listener when status change
onStatusChange.add(() => {
// ...
});
Node gained a native EventTarget in Node 15 (Oct 2020;) this question no longer applies
https://nodejs.org/api/events.html#eventtarget-and-event-api
You need a JavaScript library, like this https://github.com/Olical/EventEmitter?