Shallow component not updating on jest.fn call - node.js

After updating a series of dependencies, most notably jest and react/react-dom, a once working Unit Test is no longer working. After spending the last week reading through the ChangeLogs of the dependencies that changed, I still cannot find out why it is breaking.
The Component - stripped down for the relevant portions
[imports, etc.] ->not code, just giving a stripped down version
class MyComponent extends React.Component {
render() {
const { Foo, errorNotice, disabled } = this.props;
return (
<form autoComplete="Off">
<Paper className="top-paper edit-form">
<h1>{ Foo.id ? 'Edit' : 'Add' } My Foo </h1>
<div className="flex">
<div className="flex-column">
<FormControl
className="has-columns"
component="fieldset"
>
<TextField
id="foo-name"
fullWidth={true}
disabled={disabled}
name="name"
inputProps={{ maxLength: 50 }}
className="block"
label="Name"
value={Foo.name}
onChange={this.props.onChange}
error={!!errorText.name}
helperText={errorText.name}
/>
[closing tags, etc.] -> as as above, not code, just giving a stripped down version
export default MyComponent
The Test
import React from 'react';
import { shallow } from 'enzyme';
import MyComponent from "./my-component";
const Foo = {
name: 'Foo Name',
val_x: 'NONE'
};
const handleTextChange = jest.fn(({ target: { name, value} }) => {
Foo[name] = value;
testMyComponent.setProps({ Foo });
});
const testMyComponent = shallow(
<MyComponent
Foo={Foo}
errorNotice={{}}
onChange={handleTextChange}
/>
);
describe('Test component display', () => {
it('Name field show display attachment point name', () => {
const nameInput = testMyComponent.find('[name="name"]');
expect(nameInput.props().value).toBe(Foo.name);
});
});
^^ This Test Passes
describe('Test Foo interactions', () => {
it('Updating Name field should update Foo name', () => {
const newName= 'New Name';
testMyComponent.find('[name="name"]').simulate('change', {
target: { name: "name", value: newName }
});
expect(testMyComponent.find('[name="name"]').props().value).toBe(newName);
});
});
^^ This Test Fails on the 'expect' line. The name remains the old name, 'Foo Name'
The output when I call testMyComponent.debug() after the .simulate('change' is as follows (again stripped down)
<WithStyles(ForwardRef(TextField)) id="foo-name" fullWidth={true} disabled={[undefined]} name="name" inputProps={{...}} className="block" label="Name" value="Foo Name" onChange={[Function: mockConstructor] { _isMockFunction: true, ... , results: [ Object [Object: null prototype] {type: 'return', value: undefined } ], lastCall: [ { target: { name: 'name', value: 'New Name' ....
^^ So I can see through lastCall that the handleTextChange function is being called, but its not actually performing the update. Moreover, if I put a test for
expect(handleTextChange).toHaveBeenCalledTimes(1);
Then that text passes, it effectively gets called. But again, the update doesn't actually occur.
The dependencies that were changed were
react 16.13.1 -> 17.0.2
react-dom 16.13.1 -> 17.0.2
jest 24.9.0 -> 27.5.1
material-ui/core 4.11.0 -> 4.12.13
but enzyme stayed the same a 3.11.0
Does any of this make any sense? Like I mentioned I've read changelogs and update posts on all of the dependencies that were updated and I cant see anything that needs to change in the test, but clearly it is failing. I have read Jest/Enzyme Shallow testing RFC - not firing jest.fn() but the solution there is not working for me. I should also mention I have called .update() on the component but to no avail.

Related

Can't get html element using js file in SPFX

I am trying to build dynamic content from a SharePoint list using SPFX. I'd like to use jQuery to build an accordion view of the data. The issue is that I can't even seem to get the element once the page is rendered.
In my code I am requiring a file called ota.js with the following code:
console.log('Start');
function otaExpand(){
console.log('otaExpand Function Called');
let spListContainer = document.getElementById('spListContainer');
console.log(spListContainer);
}
window.addEventListener("load", otaExpand());
In my ts file this is my render method:
public render(): void {
this.domElement.innerHTML = `
<div>
<div id="spListContainer">TEST</div>
</div>
`;
//this._renderListAsync();
//($('.accordion', this.domElement) as any).accordion();
}
When I review the console, I get my messages, but the element itself comes back as null.
console.log
I am using SharePoint 2019 on premise with the following configuration.
+-- #microsoft/generator-sharepoint#1.10.0
+-- gulp-cli#2.3.0
`-- yo#2.0.6
node --version
v8.17.0
I should also mention I am using TypeScript with no JavaScript framework.
Does anyone know why I can't access this element from my js file?
Thanks!
My overall goal is to call list data and apply an accordion style to it (https://jqueryui.com/accordion), but I can't even get passed capturing the element to change it.
I've tried calling my code from a js file as well as trying to put the code directly in the html. Neither worked.
OK, I finally figured out what I was doing wrong. I was calling my jQuery in the render() method rather than in _renderList where this.domElement actually makes sense.
Here's my code in case anyone wants to avoid the pain I put myself through. This allows you to specify a list in the site and you just need to add the fields you want to display.
import { Version } from '#microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneChoiceGroupOption,
IPropertyPaneConfiguration,
PropertyPaneChoiceGroup,
PropertyPaneCustomField,
PropertyPaneTextField
} from '#microsoft/sp-webpart-base';
import { escape } from '#microsoft/sp-lodash-subset';
import styles from './GetSpListItemsWebPart.module.scss';
import * as strings from 'GetSpListItemsWebPartStrings';
import {
SPHttpClient,
SPHttpClientResponse
} from '#microsoft/sp-http';
import * as jQuery from 'jquery';
import 'jqueryui';
import { SPComponentLoader } from '#microsoft/sp-loader';
import PropertyPane from '#microsoft/sp-webpart-base/lib/propertyPane/propertyPane/PropertyPane';
export interface IGetSpListItemsWebPartProps {
title: string;
description: string;
listField: string;
}
export interface ISPLists {
value: ISPList[];
}
export interface ISPList {
ID: string;
Title: string;
Website: {
Description : string,
Url : string
};
Description : string;
}
export default class GetSpListItemsWebPart extends BaseClientSideWebPart<IGetSpListItemsWebPartProps> {
private _getListData(): Promise<ISPLists> {
return this.context.spHttpClient.get(this.context.pageContext.web.absoluteUrl + "/_api/web/lists/GetByTitle('" + this.properties.listField + "')/Items",SPHttpClient.configurations.v1)
.then((response: SPHttpClientResponse) => {
return response.json();
});
}
private _renderListAsync(): void {
this._getListData()
.then((response) => {
this._renderList(response.value);
})
.catch(() => {});
}
private _renderList(items: ISPList[]): void {
let listData = `
<h1>${this.properties.title}</h1>
<h2>${this.properties.description}</h2>
<div class="accordion">
`;
items.forEach((item: ISPList) => {
let Description : string;
item.Description ? Description = item.Description : Description = "";
listData += `
<h3> ${item.Title}</h3>
<div>
<table>
<tr>
<td>OTA URL</td>
<td>${item.Website.Description}</td>
</tr>
<tr>
<td>Description</td>
<td>${Description}</td>
</tr>
</table>
</div>
`;
});
listData += '</div>';
this.domElement.innerHTML = listData;
const accordionOptions: JQueryUI.AccordionOptions = {
animate: true,
collapsible: true,
icons: {
header: 'ui-icon-circle-arrow-e',
activeHeader: 'ui-icon-circle-arrow-s'
}
};
jQuery('.accordion', this.domElement).accordion(accordionOptions);
}
public render(): void {
this._renderListAsync();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('title',{
label: strings.TitleFieldLabel
}),
PropertyPaneTextField('description', {
label: strings.DescriptionFieldLabel
}),
PropertyPaneTextField('listField', {
label: strings.ListFieldLabel
})
]
}
]
}
]
};
}
public constructor() {
super();
SPComponentLoader.loadCss('//code.jquery.com/ui/1.11.4/themes/smoothness/jquery-ui.css');
}
}
Your code from the "ota.js" file is probably called before your HTML is initialized (i.e. before the "render()" function is executed). To make sure this is the case, you could add log to the "render()" function to see when it's called.
In other words, "window.load" event happens long before "render()" function is called. This is how web parts are loaded - dynamically after full load of the page. Or "window.load" does not happen at all - web parts may be loaded by the user when using the page designer, i.e. without page reload.
To fix the issue, you should get the element after it's created, i.e. after the "render()" function creates the element you are trying to get.

React update component after loading data

So I have a component that shows categories from firestore, the component shows nothing the first time but when I click navbar button again it does show the data stored in firestore.
Here is the component file :
import * as React from "react";
import Category from "./Category";
import connect from "react-redux/es/connect/connect";
import {getCategories} from "../reducers/actions/categoryAction";
class CategoriesList extends React.Component{
constructor(props) {
super(props);
this.state = ({
categoriesList: [{}]
})
}
componentWillMount() {
this.props.getCategories();
this.setState({categoriesList: this.props.categories});
this.forceUpdate();
}
render() {
return (
<div className={'container categories'}>
<div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
<div className={'col s24 m12'}>
<p>Create New Category</p>
</div>
</div>
<div className={'row'}>
<div className={'col s24 m12'}>
{/*{() => this.renderCategories()}*/}
{this.state.categoriesList && this.state.categoriesList.map(category => {
return <Category category={category} key={category.id}/>
})}
</div>
</div>
</div>
);
}
}
const mapDisptachToProps = (dispatch) => {
return {
getCategories: () => dispatch(getCategories()),
}
};
const mapStateToProps = (state) => {
return {
categories: state.category.categories
}
};
export default connect(mapStateToProps, mapDisptachToProps)(CategoriesList)
And here is the reducer file:
import db from '../firebaseConfig'
const initState = {
categories: []
};
const categoryReducer = (state=initState, action) => {
switch (action.type) {
case 'CREATE_CATEGORY':
db.collection("Categories").add({
category: action.category.name
})
.then(function(docRef) {
db.collection("Categories").get().then((querySnapshot) => {
querySnapshot.forEach((doc) => {
// console.log(`${doc.id} => ${doc.data().category}`);
if(doc.id === docRef.id) {
state.categories.push({id: doc.id, name: doc.data().category});
console.log(state.categories)
}
});
});
})
.catch(function(error) {
console.error("Error adding document: ", error);
});
break;
case 'GET_CATEGORIES':
console.log('Getting data from firestore');
db.collection("Categories").get().then((querySnapshot) => {
if(state.categories.length !== querySnapshot.size) {
querySnapshot.forEach((doc) => {
state.categories.push({id: doc.id, name: doc.data().category});
});
}
});
break;
}
return state;
};
export default categoryReducer
Is there any way to update the component after fully loading the data? or a way to load all the data in the initalState?
There are few things one needs to understand. First, this.props.getCategories() performs an action that is asynchronous in nature and hence in the very next line this.setState({categoriesList: this.props.categories});, we wont get the required data.
Second, Storing props to state without any modification is un-necessary and leads to complications. So try to use the props directly without storing it. In case you are modifying the obtained props, make sure you override getDerivedStateFromProps apropiately.
Third, Try to use componentDidMount to perform such async operations than componentWillMount. Refer when to use componentWillMount instead of componentDidMount.
Fourth(important in your case), Reducer should not contain async operations. Reducer should be a synchronous operation which will return a new state. In your case, you need to fetch the data elsewhere and then dispatch within your db.collection(..).then callback. You can also use redux-thunk, if you are using too many async operations to get your redux updated.
So #Mis94 answer should work if you follow the fourth point of returning the new state in the reducer rather than mutating the redux directly in the db().then callback
First, you don't need to store the component's props in the state object. This is actually considered an anti-pattern in react. Instead of doing this, just use your props directly in your render method:
render() {
return (
<div className={'container categories'}>
<div className={'row center'} onClick={() => this.props.history.push('/addcategories')}>
<div className={'col s24 m12'}>
<p>Create New Category</p>
</div>
</div>
<div className={'row'}>
<div className={'col s24 m12'}>
{/*{() => this.renderCategories()}*/}
{this.props.categories && this.props.categories.map(category => {
return <Category category={category} key={category.id}/>
})}
</div>
</div>
</div>
);
}
Hence in your componentWillMount you only need to initiate your request:
componentWillMount() {
this.props.getCategories();
}
You can also do it in componentDidMount() lifecycle method.
Now when your request resolves and your categories update in the store (Redux) they will be passed again to your component causing it to update. This will also happen with every update in the categories stored in the store.
Also you don't have to call forceUpdate like this unless you have components implementing shouldComponentUpdate lifecycle method and you want them to ignore it and do a force update. You can Read about all these lifecycle methods (and you have to if you are using React) here.

Can't Edit and Update properties with form Reactjs and MongoDB

So I'm using Nodejs, MongoDB and Reactjs
and I'm trying to Edit properties of projects.
I have multiple projects and when I want to edit properties of one I can't do it. We can access to properties inside inputs, we can see Title and Type but can't even delete, write, he access to properties by its ID but then I can't change it, I guess I have multiple problems here than.
I'll write here my server code, and my Edit/Update project page and a gif with an example when I say that I can't even change anything on inputs.
My server code:
//Render Edit Project Page byId
app.get('/dashboard/project/:id/edit', function(req, res){
let id = req.params.id;
Project.findById(id).exec((err, project) => {
if (err) {
console.log(err);
}
res.json(project);
});
}
//Update Projects Properties byId
app.put('/dashboard/project/:id/edit', function(req, res){
var id = req.params.id;
var project = {
title: req.body.title,
typeOfProduction: req.body.typeOfProduction
};
Project.findByIdAndUpdate(id, project, {new: true},
function(err){
if(err){
console.log(err);
}
res.json(project);
})
};
My React Component Edit Project Page
import React, { Component } from 'react';
import { NavLink } from 'react-router-dom';
import './EditProject.css';
class EditProject extends Component {
constructor(props){
super(props);
this.state = {
//project: {}
title: '',
typeOfProduction: ''
};
}
inputChangedHandler = (event) => {
const updatedProject = event.target.value;
}
componentDidMount() {
// console.log("PROPS " + JSON.stringify(this.props));
const { match: { params } } = this.props;
fetch(`/dashboard/project/${params.id}/edit`)
.then(response => { return response.json()
}).then(project => {
console.log(JSON.stringify(project));
this.setState({
//project: project
title: project.title,
typeOfProduction: project.typeOfProduction
})
})
}
render() {
return (
<div className="EditProject"> EDIT
<form method="POST" action="/dashboard/project/${params.id}/edit?_method=PUT">
<div className="form-group container">
<label className="form--title">Title</label>
<input type="text" className="form-control " value={this.state.title} name="title" ref="title" onChange={(event)=>this.inputChangedHandler(event)}/>
</div>
<div className="form-group container">
<label className="form--title">Type of Production</label>
<input type="text" className="form-control " value={this.state.typeOfProduction} name="typeOfProduction" ref="typeOfProduction" onChange={(event)=>this.inputChangedHandler(event)}/>
</div>
<div className="form-group container button">
<button type="submit" className="btn btn-default" value="Submit" onClcik={() => onsubmit(form)}>Update</button>
</div>
</form>
</div>
);
}
}
export default EditProject;
Erros that I have:
1- DeprecationWarning: collection.findAndModify is deprecated. Use findOneAndUpdate, findOneAndReplace or findOneAndDelete instead.
2- Inputs can't change
3- When click "Update" button:
I think your update override the entire object because you forgot the $set operator. This is the operator to change only the atributtes of an object and not the entire object replacing!
Example:
Model.update(query, { $set: { name: 'jason bourne' }}, options, callback)
First of all, concerning the deprecation warning, you need to change the method findAndModify (As I do not see it here, I guess you're using it elsewhere, or maybe one of the methods you use is calling it) by one of the suggested methods and change your code accordingly.
Then, you need to learn about React and controlled components : https://reactjs.org/docs/forms.html
You need to set the component's state in your onChange handler, such as :
this.setState({
title: event.target.value // or typeOfProduction, depending on wich element fired the event
});
This is called a controlled component in React.
Concerning the response body you get when clicking on Update button, this is actually what you asked for :
res.json(project);
returns the project variable as a JSON file, which is displayed on your screenshot.
See this question for more information about it : Proper way to return JSON using node or Express
Try replace "value" in input tag with "placeholder"

Typescript compiles `tsx` to a wrong type of modue

I'm trying to use Typescript together with Node and React. However I'm struggling to set up the Typescript compiler to use the correct type of the “module convention”. I know that ES6 modules do work correctly, but Typescript always generates a non-standard module syntax that, for some reason, just doesn't work.
.
If I save this code into a .js file, it works correctly:
import React, { Component } from 'react';
import './App.css';
let logo = require('./logo.svg');
class App extends Component {
render() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<h1 className="App-title">Welcome to React</h1>
</header>
<p className="App-intro">
To get started, edit <code>src/App.js</code> and save to reload.
</p>
<p>{logo}</p>
</div>
);
}
}
export default App;
However, if I save it to a .tsx file, it compiles to this:
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = require("react");
require("./App.css");
let logo = require('./logo.svg');
class App extends react_1.Component {
render() {
return (react_1.default.createElement("div", { className: "App" },
react_1.default.createElement("header", { className: "App-header" },
react_1.default.createElement("img", { src: logo, className: "App-logo", alt: "logo" }),
react_1.default.createElement("h1", { className: "App-title" }, "Welcome to React")),
react_1.default.createElement("p", { className: "App-intro" },
"To get started, edit ",
react_1.default.createElement("code", null, "src/App.js"),
" and save to reload."),
react_1.default.createElement("p", null, logo)));
}
}
exports.default = App;
Which results in a Node error react_1.default is undefined. I tried adding a esModuleInterop flag to the Typescript compiler, which added this definition:
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
}
and changed require("react") to __importDefault(require("react")). That fixes the first problem, however react_1.Component is still undefined. Is there a way to set the Typescript compiler to use the standard ES6 modules?
Add this to tsconfig.json:
"compilerOptions": {
"module": "es6"
}

actions/reducers are not causing a rerender as expected

I am building a web client (react,redux) & API (mongo, express, node) that will show a list of deals to a user and allow them to "favorite/like" them. I am new to react/redux, as you will be able to tell. I am using axios to make my requests and have successfully rendered a list of deals. I have a "favorite" button that successfully makes the post request, and the request just sends back the deal that was favorited.. However, the "number of likes" is not updating and does not show the increased number until I manually refresh the page.
Here is my component that successfully produces a list of deals (2)
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { fetchDeals, favoriteDeal } from '../actions';
import DealCard from './DealCard';
class DealList extends Component {
componentDidMount(){
this.props.fetchDeals();
this.favoriteDeal = this.favoriteDeal.bind(this);
}
favoriteDeal = (dealId) => {
this.props.favoriteDeal(dealId)
}
renderDeals(){
return this.props.deals.map(deal => {
return(
<DealCard
onFavorite = {this.favoriteDeal}
key={deal._id}
{...deal}
/>
)
});
}
render(){
return(
<div>
{this.renderDeals()}
</div>
);
}
}
function mapStateToProps(state){
return {
deals: state.deals,
favoriteDeal: state.favoritedDeal
}
}
export default connect(mapStateToProps, {fetchDeals, favoriteDeal})(DealList)
Below is my individual deal card:
import React, { Component } from 'react';
class DealCard extends Component {
render() {
return (
<div key={this.props._id} className="card" style={{width: "18rem", marginTop: 10}}>
<img className="card-img-top" src={this.props.dealImage} style={{maxHeight: 200}} alt="${this.props.dealHeadline}" />
<div className="card-body">
<h4>{this.props.dealHeadline}</h4>
<p className="card-text">{this.props.dealDescription}</p>
<div>
<button onClick={() => this.props.onFavorite(this.props._id)}>Favorite</button>
<span>{this.props.dealId}</span>
<i className="fa fa-heart" aria-hidden="true"></i>
<p className="card-text">#of Likes: {this.props.dealNumberOfLikes}</p>
</div>
</div>
</div>
);
}
}
export default DealCard;
Below are my action creators:
export const fetchDeals = () => async dispatch => {
const res = await axios.get('/api/deals')
dispatch({type: FETCH_DEALS, payload: res.data})
};
export const favoriteDeal = (dealId) => async dispatch => {
const res = await axios.post(`/api/deals/${dealId}/favorites`)
dispatch({type: FAVORITE_DEAL, payload: res.data})
};
and finally my reducers:
// deals reducer
import { FETCH_DEALS } from '../actions/types';
export default function (state = [], action){
switch(action.type){
case FETCH_DEALS:
return action.payload;
default:
return state;
}
};
// favorite deals Reducer
import { FAVORITE_DEAL } from '../actions/types';
export default function (state = {}, action){
switch(action.type){
case FAVORITE_DEAL:
return action.payload;
default:
return state;
}
};
To summarize: I have a list of deals, and each deal has a button that when clicked, "favorites" a deal via an HTTP post request and increases the NumberOfDealLikes by 1. When the button is clicked, the request is successfully executed and the database shows that the NumberOfDealLikes is increased by one. However, on the screen, the update is not shown until I manually rerender. As twitter works, I would like to show that the increase happens simultaneously.
Thank you all for your help!
I think the problems lies in your favorite_deal reducer. As you said, the post request sends back the updated deal. It should then replace the old one in the deals array. Your deals reducer should look like:
import { FETCH_DEALS, FAVORITE_DEAL } from '../actions/types';
export default function (state = [], action){
switch(action.type){
case FETCH_DEALS:
return action.payload;
case FAVORITE_DEAL:
return state.map((d) => d._id === action.payload._id ? action.payload : d);
default:
return state;
}
};
As the deals array is updated, your component will be re-rendered. And you do not need another reducer.
By the way, as you defined the favoriteDeal function as a class property with an arrow function, you do not need to bind it to this.

Resources