Vitest -Unable to access the DOM inside a test - javascript

I am new to front-end testing, and I am trying to get a good grasp on it.
In the process, inside one project, I am creating a test for a Vue component. I want to test that the behaviour of the code is correct (The component has code inside the mounted() hook that has to perform basically some checks and an API call).
I want to check that the code reaches one method. Previously to that, the code creates a click event listener to one element in the DOM.
My test emulates a click event (triggers it), but it cannot assert that the proper method has been called after the click event.
This is due to it not finding the element in the DOM to which it has to add the event listener. It seems that the code cannot find anything inside the document (using .getElementById()).
I wonder why, and how I would resolve this, since I have been stuck here for hours and I haven't found any solution that could work here, even when I have learned some interesting things in the process. I will leave a code example with the code structure I have built:
Inside the component:
<template>
// ...
<button id = "myButton">Add</button>
</template>
<script>
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile ";
let classIESF = new classInExternalScriptsFile();
export default {
methods: {
setup: function () {
classIESF.setupMethod();
},
},
mounted() {
this.setupMethod();
},
};
</script>
Inside the scriptsFile
export class classInExternalScriptsFile {
setupMethod() {
let myButton = document.getElementById("myButton") // <-- getElementById() returns a null here
if (typeof myButton !== "undefined" && myButton !== null) {
myButton.onclick = () => { // <-- The test code complains because it cannot enter here
// Some lines...
this.mySuperMethod()
}
}
}
mySuperMethod() {
// API call etc.
}
}
Inside the .spec.js test file:
// imports...
import { classInExternalScriptsFile } from "#/scripts/externalScriptsFile.js";
describe("description...", () => {
const mySuperMethodMock = vi
.spyOn(classInExternalScriptsFile.prototype, "mySuperMethod")
.mockImplementation(() => {});
test("That the button performs x when clicked", () => {
let wrapper = mount(myComponent, {
props: ...,
});
let myButton = wrapper.find('[test-id="my-button"]');
myButton.trigger("click");
expect(mySuperMethodMock).toHaveBeenCalled(); // <-- The test fails here
}
}

Related

Calling the vue method from inside an event from VueDraggable

I am trying to get drag and drop function working in the vue.js app using vue-draggable https://vuejsexamples.com/vuejs-drag-and-drop-library-without-any-dependency/
The library has few events you can listen to and I would like to execute some logic once the item is dropped. However, I am not able to access vue component 'this' along with the data and methods. I've tried to use this.$dispatch('symDragged', event); but it is not working for the same reason. 'this' is not a vue instance but rather instance of draggable element.
Here is the code:
export default {
components: {
ICol,
SymptomsChooser, MultiSelectEditor, TempPressureChooser, BodyPartsEditor, MandatorySymptomsChooser},
data() {
return {
// data ommited...
options: {
dropzoneSelector: 'ul',
draggableSelector: 'li',
excludeOlderBrowsers: true,
showDropzoneAreas: true,
multipleDropzonesItemsDraggingEnabled: true,
onDrop(event) {
// delete symptom from old basket and add it to new one
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
//this is not working
//this.symDragged(this.draggedSymId, oldBasket, newBasket);
},
onDragstart(event) {
this.draggedSymId = event.items[0].accessKey;
}
}
}
},
methods: {
symDragged(symId, oldBasketId, newBasketId) {
console.log("symDragged!");
let draggedSym = this.getSymById(symId);
let basketOld = this.getBasketById(oldBasketId);
this.delSym(basketOld, draggedSym);
this.addSym({baskedId: newBaskedId, sym: draggedSym});
}
//other methods ommited
}
}
So, what is the correct way to call the vue component method from callback event? Or maybe I need to create another event so that vue instance could listen to it?
Thanks for you help!
The problem you are facing is that with this you are referencing to the returned data object scope and not component scope. The best way to solve this is to make reference to the component instance, so later on you can call anything attached to that instance. You can also take a look at codesandbox example https://codesandbox.io/embed/7kykmmmznq
data() {
const componentInstance = this;
return {
onDrop() {
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
let draggedItemsAccessKeys = event.items.map(element => element.accessKey);
componentInstance.symDragged(
draggedItemsAccessKeys,
oldBasket,
newBasket
);
}
}
}

Call a method within a React Component from Electron's globalShortcut

I have a React component with a method:
class Timer extends Component{
start(){
this.timerInterval=setInterval(this.tick,1000);
}
[...]
}
I want to be able to call the method start() whenever the user presses a combination of keys.
In my main.js file I have:
app.on('ready', function(){
createWindow();
globalShortcut('Cmd+Alt+K',function(){
//call start() here
})
});
How can I achieve this? There's not much information I could find on this subject.
When in need of use of electron library inside react you should import it using electron remote
const { globalShortcut } = require('electron').remote;
class Timer extends Component{
componentDidMount = () => {
globalShortcut.register('Cmd+Alt+K', () => {
//your call
this.start()
});
}
start(){
this.timerInterval=setInterval(this.tick,1000);
}
[...]
}
I was hoping someone with Electron-specific experience would address this, but it's been a full day now.
To cross the outside world / React component barrier, you're probably best off using a custom event. To do that, you'll need access to the DOM element created by React in response to your render call, then listen for your custom event directly on the element. Here's an example, see the comments for details. Note that in this example I'm passing an object as the event detail; that's just to show you can do that (not just simple strings), but you could just do {detail: "Tick #" + ticker} instead and use event.detail directly as the message.
class Example extends React.Component {
// Normal component setup
constructor(...args) {
super(...args);
this.state = {message: ""};
// Our event handler. (We could also define this at the class level using the class
// fields syntax that's currently at Stage 3 in the ECMA TC39 process; most React
// setups with JSX transpile class fields syntax.)
this.doSomething = event => {
this.setState({message: event.detail.message || ""});
};
}
// Render with a way of finding the element in the DOM from outside (in this case
// I'm using a class, but it could be an ID, or just knowing where it is in the DOM).
// We use a ref so we can hook the event when the elemet is created.
render() {
return <div className="foo" ref={element => this.element = element}>{this.state.message}</div>;
}
// Hook the event on mount, unhook it on unmount
componentDidMount() {
this.element.addEventListener("my-event", this.doSomething);
}
componentWillUnmount() {
this.element.removeEventListener("my-event", this.doSomething);
}
}
// Top-level app rendering
ReactDOM.render(<Example />, document.getElementById("root"));
// Demonstrate triggering the event, in our case we do it every half-second or so
let ticker = 0;
setInterval(() => {
const foo = document.querySelector("#root .foo");
++ticker;
foo.dispatchEvent(new CustomEvent("my-event", {detail: {message: "Tick #" + ticker}}));
}, 500);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>

Can I call a method without using .simulate() - Jest Enzyme

I'm unit testing for a React flight seat selecting app using Jest/Enzyme. Is there a way I can test a method within my class based component which would run after a button is clicked, but without actually simulating the button click? This is because the button is within a child of a child component and the method is being passed down as a prop.
Albeit a very simple function, I'd still like to test it
inputSeats(chosenSeats) {
this.setState({
chosenSeats: chosenSeats
})
}
This is in a parent component called FlightSeats, with a child of SeatMaps, and SeatMaps has 2 children of SeatMap (inbound/outbound).
Within each of these SeatMap components there is the 'Reserve Seats' button which when clicked it performs some validation tests, calls another method in SeatMap and eventually calls inputSeats() from within SeatMaps component. I'm struggling to simulate the button click since it is deep within the app.
Ideally, in my unit test, I'd just to like to call it with something like
FlightSeats.inputSeats(chosenSeats)
and pass in my mock data as chosenSeats... Or would I have to import the child components and mount them and use .simulate('click') on the button?
My test so far:
let chosenSeats = {
outbound: [{
seatNo: "11A",
seatPrice: "11.11",
}, {
seatNo: "12A",
seatPrice: "12.12"
}],
inbound: [{
seatNo: "14A",
seatPrice: "14.14",
}, {
seatNo: "15A",
seatPrice: "15.15"
}]
};
let wrapper, buildEmptySeats, clearSeats, comp;
beforeEach(() => {
comp = ( < FlightSeats seats = {
seatsArr
}
party = {
partyArr
}
flights = {
flightArr
}
/>);
wrapper = shallow(comp); component = mount(comp);
});
test('should handle inputSeats correctly', () => {
// what to call here??
expect(component.state().chosenSeats.outbound[1].seatNo).toEqual("12A");
expect(component.state().chosenSeats.outbound[1].seatPrice).toEqual(12.12);
});
I assume you are just testing the functionality, then it should be fine to directly call the method in parent component but its usually good to test the button clicked simulation process as this what the user clicks.
Here is a simple example based on your code above
const chosenSeats = {...};
const component = mount(comp);
test('should handle inputSeats correctly', () =>{
//here get the instance of the component before you can call its method
component.instance().inputSeats(chosenSeats);
//here you call state like you've already done
expect(component.state().chosenSeats.outbound[1].seatNo).toEqual("12A");
expect(component.state().chosenSeats.outbound[1].seatPrice).toEqual(12.12);
};

Flux/Alt setTimeout not updating store

I'm trying to create a basic "Toast" like service in my React app using Alt.
I've got most of the logic working, I can add new items to the array which appear on my view when triggering the add(options) action, however I'm trying to also allow a timeout to be sent and remove a toast item after it's up:
onAdd(options) {
this.toasts.push(options);
const key = this.toasts.length - 1;
if (options.timeout) {
options.timeout = window.setTimeout(() => {
this.toasts.splice(key, 1);
}, options.timeout);
}
}
On add, the toast appears on my page, and the timeout also gets triggered (say after a couple of seconds), however manipulating this.toasts inside of this setTimeout does not seem to have any effect.
Obviously this is missing the core functionality, but everything works apart from the setTimeout section.
It seems that the timeout is setting the state internally and is not broadcasting a change event. It might be as simple as calling forceUpdate(). But the pattern I use is to call setState() which is what I think you might want in this case.
Here is an example updating state and broadcasting the change event.
import alt from '../alt'
import React from 'react/addons'
import ToastActions from '../actions/ToastActions'
class ToastStore {
constructor() {
this.toasts = [];
this.bindAction(ToastActions.ADD, this.add);
this.bindAction(ToastActions.REMOVE, this.remove);
}
add(options) {
this.toasts.push(options);
this.setState({toasts: this.toasts});
if (options.timeout) {
// queue the removal of this options
ToastActions.remove.defer(options);
}
}
remove(options) {
const removeOptions = () => {
const toasts = this.toasts.filter(t => t !== options);
this.setState({toasts: toasts});
};
if (options.timeout) {
setTimeout(removeOptions, options.timeout);
} else {
removeOptions();
}
}
}
module.exports = alt.createStore(ToastStore, 'ToastStore');

this.myService.myEvent.toRx().subscribe() called but no DOM refresh (Zone trigger)

I'm playing with angular2 alpha 40 with ng2-play starter from pawel.
Examples are in typescript.
I have a service MovieList like this:
export class Movie {
selected: boolean = false
constructor(public name:string, public year:number, public score:number) {}
}
export class MovieListService {
list: Array<Movie>
selectMovie = new EventEmitter();
constructor() {
this.list = [new Movie('Star Wars', 1977, 4.4)];
}
add(m:Movie) {
this.list.push(m);
}
remove(m:Movie) {
for(var i = this.list.length - 1; i >= 0; i--) {
if(this.list[i] === m) {
if(m.selected) this.selectMovie.next();
this.list.splice(i, 1);
}
}
}
select(m:Movie) {
this.list.map((m) => m.selected = false);
m.selected = true;
this.selectMovie.next(m);
}
}
I have a component showing the movies list and make possible to select one by clicking on it, which call select() in the service above.
And I have another component (on the same level, I don't want to use (selectmovie)="select($event)") which subscribe to the movie selection event like this:
#Component({
selector: 'movie-edit',
})
#View({
directives: [NgIf],
template: `
<div class="bloc">
<p *ng-if="currentMovie == null">No movie selected</p>
<p *ng-if="currentMovie != null">Movie edition in progress !</p>
</div>
`
})
export class MovieEditComponent {
currentMovie:Movie
constructor(public movieList: MovieListService) {
this.movieList.selectMovie.toRx().subscribe(this.movieChanged);
setTimeout(() => { this.movieChanged('foo'); }, 4000);
}
movieChanged(f:Movie = null) {
this.currentMovie = f;
console.log(this.currentMovie);
}
}
The event is subscribed using .toRx().subscribe() on the eventEmitter.
movieChanged() is called but nothing happen in the template..
I tried using a timeout() calling the same function and changes are refleted in the template.
The problem seems to be the fact that subscribe expects an Observer or three functions that work as an observer while you are passing a normal function. So in your code I just changed movieChanged to be an Observer instead of a callback function.
movieChanged: Observer = Observer.create(
(f) => { this.currentMovie = f; }, // onNext
(err) => {}, // onError
() => {} // onCompleted
);
See this plnkr for an example. It would have been nice to see a minimal working example of your requirement so my solution would be closer to what you are looking for. But if I understood correctly this should work for you. Instead of a select I just used a button to trigger the change.
Update
You can avoid creating the Òbserver just by passing a function to the subscriber method (clearly there's a difference between passing directly a function and using a class method, don't know really why is different)
this.movieList.selectMovie.toRx().subscribe((m: Movie = null) => {
this.currentMovie = m;
});
Note
EventEmitter is being refactored, so in future releases next will be renamed to emit.
Note 2
Angular2 moved to #reactivex/rxjs but in the plnkr I'm not able to use directly those libs (didn't find any cdn). But you can try in your own project using these libs.
I hope it helps.
The movieChanged function expects the movie object and not the String. Try changing below code
setTimeout(() => { this.movieChanged('foo'); }, 4000);
to
setTimeout(() => { this.movieChanged(new Movie('Troy', 2000 , 8)); }, 4000);

Categories