Input not updating on react testing library, thus test failing, however it does update on the actual app - redux

I want to test that when i type a value in an input(inputA), anoter input(inputB) gets updated with a value.
inputA accepts a postal code e.g: "10999", after inputB shows a location: "Berlin"
This works on the actual app, i type in inputA, and inputB gets updated.
When ome types on inputA, an action is dispatched and then inputB gets a new value from the redux state.
This is my test code, any ideas why it doesnt updates the input with placeholder of "Ort" on the test, but it does on the actual app?
import { render, withIntl, withStore, configureStore, withState } from "test-utils-react-testing-library";
import { screen, fireEvent, withHistory, withRoute, within } from "#testing-library/react";
import configureMockStore from 'redux-mock-store';
import ProfileForm from "./ProfileForm";
import PersonalDetails from "../PersonalDetails/PersonalDetails";
const STATE = {
locations: { locations: {} },
streets: { streets: {} },
password: {}
};
const mockStore = configureMockStore();
const STORE = mockStore({
streets: {
isFetching: false,
},
locations: {
locations: {
isFetching: false,
},
},
user: {
session: {
impersonated_access_token: "",
},
updateError: "error",
},
});
const props = {
id: "user1",
user: { email: "max#muster.de" },
locations: {},
onSubmit: jest.fn(),
};
beforeEach(jest.resetAllMocks);
describe("ProfileForm", () => {
describe("on personal details change", () => {
it("auto selects only location when postalcode becomes selected", () => {
const locations = { electricity: { [PLZ_1]: [LOCATION_OBJ_1] } };
const user = { postalcode: null };
render(<ProfileForm {...props} user={user} locations={locations} />, [...decorators, withStore(STORE)]);
const input = screen.getByPlaceholderText("PLZ");
fireEvent.change(input, { target: { value: "10999" } })
screen.debug(screen.getByPlaceholderText("PLZ"))
screen.debug(screen.getByPlaceholderText("Ort"))
expect(screen.getByPlaceholderText("Ort")).toHaveValue("Berlin");
});
});

I guess your input hasn't been updated yet.
Try to use waitfor:
https://testing-library.com/docs/dom-testing-library/api-async#waitfor
import { waitFor } from "#testing-library/react";
const inputNode = screen. getByPlaceholderText("Ort");
// keep in mind that you need to make your test async like this
// it("auto selects only location when postalcode becomes selected", async () => {
await waitFor(() => expect(inputNode).toHaveValue("Berlin"));
If it won't work, try to add timeout:
await waitFor(() => expect(inputNode).toHaveValue("Berlin"), { timeout: 4000 });

I've encountered a similar proplem and found that changes in the microtask queue aren't always flushed, so the changes are not applied/rendered until the test is finished running. What worked for me, was to call jest.useFakeTimers() at the beginning of your testcase, and then await act(async () => { jest.runOnlyPendingTimers() }); after the call to fireEvent.<some-event>(...)
In your case:
it("auto selects only location when postalcode becomes selected", async () => {
jest.useFakeTimers();
const locations = { electricity: { [PLZ_1]: [LOCATION_OBJ_1] } };
const user = { postalcode: null };
render(<ProfileForm {...props} user={user} locations={locations} />, [...decorators, withStore(STORE)]);
const input = screen.getByPlaceholderText("PLZ");
fireEvent.change(input, { target: { value: "10999" } })
await act(async () => {
jest.runOnlyPendingTimers();
});
screen.debug(screen.getByPlaceholderText("PLZ"))
screen.debug(screen.getByPlaceholderText("Ort"))
expect(screen.getByPlaceholderText("Ort")).toHaveValue("Berlin");
});

Tried, but get this error: Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function. No idea where that comes from :(
Try to use findBy instead of getBy.
https://testing-library.com/docs/dom-testing-library/api-queries#findby
import { screen, waitFor } from "#testing-library/react";
const inputNode = await screen.findByPlaceholderText("Ort");
// or with timeout: await screen.findByPlaceholderText("Ort", { timeout: 4000 });
await waitFor(() => expect(inputNode).toHaveValue("Berlin"));

Related

I cannot understand WHY I cannot change state in Redux slice

I get the array of objects coming from backend, I get it with socket.io-client. Here we go!
//App.js
import Tickers from "./Components/TickersBoard";
import { actions as tickerActions } from "./slices/tickersSlice.js";
const socket = io.connect("http://localhost:4000");
function App() {
const dispatch = useDispatch();
useEffect(() => {
socket.on("connect", () => {
socket.emit("start");
socket.on("ticker", (quotes) => {
dispatch(tickerActions.setTickers(quotes));
});
});
}, [dispatch]);
After dispatching this array goes to Action called setTickers in the slice.
//slice.js
const tickersAdapter = createEntityAdapter();
const initialState = tickersAdapter.getInitialState();
const tickersSlice = createSlice({
name: "tickers",
initialState,
reducers: {
setTickers(state, { payload }) {
payload.forEach((ticker) => {
const tickerName = ticker.ticker;
const {
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
} = ticker;
state.ids.push(tickerName);
const setStatus = () => {
if (ticker.yeild > state.entities[tickerName].yeild) {
return "rising";
} else if (ticker.yeild < state.entities[tickerName].yeild) {
return "falling";
} else return "noChange";
};
state.entities[tickerName] = {
status: setStatus(),
price,
exchange,
change,
change_percent,
dividend,
yeild,
last_trade_time,
};
return state;
});
return state;
},
},
});
But the state doesn't change. I tried to log state at the beginning, it's empty. After that I tried to log payload - it's ok, information is coming to action. I tried even to do so:
setTickers(state, { payload }) {
state = "debag";
console.log(state);
and I get such a stack of logs in console:
debug
debug
debug
3 debug
2 debug
and so on.

Composition API - Axios request in setup()

I am experimenting with Vue3's Composition API in a Laravel/VueJS/InertiaJS stack.
A practice that I have used a lot in Vue2 with this stack is to have 1 route that returns the Vue page component (eg. Invoices.vue) and then in the created() callback, I would trigger an axios call to an additional endpoint to fetch the actual data.
I am now trying to replicate a similar approach in Vue3 with composition API like so
export default {
components: {Loader, PageBase},
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: getInvoices(),
selectedInvoices: [],
});
async function getInvoices() {
loading.value = true;
return await axios.get(props.fetch_url).then(response => {
return response.data.data;
}).finally(() => {
loading.value = false;
})
}
function handleSelectionChange(selection) {
state.selectedInvoices = selection;
}
return {
loading,
state,
handleSelectionChange,
}
}
}
This however keeps on giving me the propise, rather than the actual data that is returned.
Changing it like so does work:
export default {
components: {Loader, PageBase},
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: [],
selectedInvoices: [],
});
axios.get(props.fetch_url).then(response => {
state.invoices = response.data.data;
}).finally(() => {
loading.value = false;
})
function handleSelectionChange(selection) {
state.selectedInvoices = selection;
}
return {
loading,
state,
handleSelectionChange,
}
}
}
I want to use function though, so I can re-use it for filtering etc.
Very curious to read how others are doing this.
I have been googling about it a bit, but cant seem to find relevant docu.
All feedback is highly welcomed.
I tried this now with async setup() and await getInvoices() and <Suspense> but it never displayed any content.
So this is how I'd do it, except I wouldn't and I'd use vuex and vuex-orm to store the invoices and fetch the state from the store.
<template>
<div>loading:{{ loading }}</div>
<div>state:{{ state }}</div>
</template>
<script>
import {defineComponent, ref, reactive} from "vue";
import axios from "axios";
export default defineComponent({
name: 'HelloWorld',
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: []
})
async function getInvoices() {
loading.value = true;
await axios.get(props.fetch_url).then(response => {
state.invoices = response.data;
}).finally(() => {
loading.value = false;
})
}
return {
getInvoices,
loading,
state,
}
},
async created() {
await this.getInvoices()
}
})
</script>
<style scoped>
</style>
This is of course similar to what you're doing in option 2.

NextJS routing error, when changing pages, the wrong file is trying to open

What I want
I want to change pages without next thinking I am trying to open another page.
The Problem
I have this weird routing problem.
First, my folder structure
pages
[app]
[object]
index.js
index.js
manager.js
feed.js
I am at this path /[app] and navigate to /[app]/manager and then I want to navigate to /[app]/feed and I get this Unhandled Runtime Error.
TypeError: Cannot read property "title" of undefined
This error comes from [object] index.js. Stacktrace is below. Of course, it makes sense it cannot read title because I am trying to open another page. And yet it thinks I am trying to open [object].
This error happens from time to time, but it doesn't matter in what order I try to open the pages, it can be manager to feed or feed to manager, or whatever else I have there.
My getStaticPaths and getStaticProps are the same on all these pages, I will share the one for manager.js.
export const getStaticPaths = async () => {
const paths = appRoutes.map((appRoute) => {
const slug = appRoute.slug;
return {
params: {
app: slug,
manager: 'manager',
},
};
});
return {
fallback: false,
paths,
};
};
export const getStaticProps = async ({ locale }) => {
return {
props: {
...(await serverSideTranslations(locale, ['manager', 'common'])),
},
};
};
And the same again, but for [object]:
export const getStaticPaths = async () => {
const allObjects = await loadObjectData({ id: 'all' });
const paths = allObjects.flatMap((object) => {
return appRoutes.map((appRoute) => {
return {
params: {
object: object.type,
app: appRoute.slug,
},
};
});
});
return {
fallback: false,
paths,
};
};
export const getStaticProps = async ({ params, locale }) => {
const object = await loadObjectData({ type: params.object });
const app = appRoutes.find((appRoute) => appRoute?.slug === params.app);
if (!object) {
throw new Error(
`${object} is not a valid Object. Try checking out your parameters: ${params.object}`
);
}
if (!app) {
throw new Error(`${app} is not a valid App.`);
}
return {
props: {
...(await serverSideTranslation(locale, ['common'])),
object,
app,
},
};
};
This error is hard to reproduce because it happens only from time to time.
New Edits
This is the full file of [object]/index.js
import appRoutes from '../../../routes/appRoutes';
import loadObjectData from '../../../utils/loadObjects';
import { serverSideTranslation } from 'next-i18next/serverSideTranslations';
export default function ObjectPage({ object }) {
return <h1> {object.title} </h1>;
}
export const getStaticPaths = async () => {
const allObjects = await loadObjectData({ id: 'all' });
const paths = allObjects.flatMap((object) => {
return appRoutes.map((appRoute) => {
return {
params: {
object: object.type,
app: appRoute.slug,
},
};
});
});
return {
fallback: false,
paths,
};
};
export const getStaticProps = async ({ params, locale }) => {
const object = await loadObjectData({ type: params.object });
const app = appRoutes.find((appRoute) => appRoute?.slug === params.app);
if (!object) {
throw new Error(
`${object} is not a valid Object. Try checking out your parameters: ${params.object}`
);
}
if (!app) {
throw new Error(`${app} is not a valid App.`);
}
return {
props: {
...(await serverSideTranslation(locale, ['common'])),
object,
app,
},
};
};
Stacktrace:
ObjectPage: index.js:6 Uncaught TypeError: Cannot read property 'title' of undefined
at ObjectPage (http://localhost:3000/_next/static/chunks/pages/%5Bapp%5D/%5Bobject%5D.js:3733:21)
at div
at Grid (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:13654:35)
at WithStyles (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:179881:31)
at div
at StyledComponent (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:179652:28)
at div
at ProjectSelectionStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:234820:77)
at Layout (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:278:23)
at TaskStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:235454:77)
at UserDocumentStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:235663:77)
at StoneStore (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:235119:77)
at StoreMall (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:409:23)
at ThemeProvider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:178584:24)
at App (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:234333:24)
at I18nextProvider (http://localhost:3000/_next/static/chunks/pages/_app.js?ts=1624290251377:224427:19)
at AppWithTranslation
at ErrorBoundary (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:146:47)
at ReactDevOverlay (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:250:23)
at Container (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:8662:5)
at AppContainer (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:9151:24)
at Root (http://localhost:3000/_next/static/chunks/main.js?ts=1624290251377:9282:24)
25.06.2021
So I consoled logged the router from the ObjectPage and for each NavigationItem. I noticed something strange.
This is the href I am passing to teh <Link>:
{
pathname: "/[app]/[menuItem]"
query: {
app: "content"
menuItem: "files"
}
}
And this is the full router I am getting back on ObjectPage.
{
asPath: "/content/editor" // this the path i want to open
back: ƒ ()
basePath: ""
beforePopState: ƒ ()
components: {
"/[app]/[object]": {styleSheets: Array(0), __N_SSG: true, __N_SSP: undefined, props: {…}, Component: ƒ}
"/[app]/editor": {initial: true, props: {…}, err: undefined, __N_SSG: true, Component: ƒ, …}
"/_app": {styleSheets: Array(0), Component: ƒ}
}
defaultLocale: "de"
events: {on: ƒ, off: ƒ, emit: ƒ}
isFallback: false
isLocaleDomain: false
isPreview: false
isReady: true
locale: "de"
locales: ["de"]
pathname: "/[app]/[object]" // [object] is being loaded
prefetch: ƒ ()
push: ƒ ()
query: {app: "content", menuItem: "editor", object: "editor"} // this is interesting
reload: ƒ ()
replace: ƒ ()
route: "/[app]/[object]" // same as pathname
}
In the query you can see object was injected. But I cannot tell from where and why.
I had this code:
{
pathname: "/[app]/[menuItem]"
query: {
app: "content"
menuItem: "files"
}
}
This was incorrect because there is no dynamic path to [menuItem]. So instead I wrote:
{
pathname: "/[app]/files"
query: {
app: "content"
}
}
Which fixed the issue I had.
I have misunderstood the docs for parameters.

How do I receive promise values in meteor for helper functions?

I wanted to work with Shopify's address library. Since these work with promises I thought about implementing callbacks in order to receive the results
import { Template } from 'meteor/templating';
import { ReactiveDict } from 'meteor/reactive-dict'
import AddressFormatter from '#shopify/address';
import './main.html';
const address = {
company: 'Shopify',
firstName: '恵子',
lastName: '田中',
address1: '八重洲1-5-3',
address2: '',
city: '目黒区',
province: 'JP-13',
zip: '100-8994',
country: 'JP',
phone: '',
};
Template.hello.onCreated(function () {
const addressFormatter = new AddressFormatter('ja');
const instance = this
instance.state = new ReactiveDict()
instance.state.setDefault('result', {
"formattedAddress": "",
"orderedFields": ""
});
getData(addressFormatter, function(r) {
// the next line triggers the helper, since it "observes" the changes
// to this "result" property on the reactive-dictionary
instance.state.set('result', {
formattedAddress: r.formattedAddress,
orderedFields: r.orderedFields
});
});
})
Template.hello.helpers({
address: function() {
console.log(Template.instance().state.get("result"));
return Template.instance().state.get('result')
}
});
function getData(addressFormatter, callback) {
const fa = async () => {
const result = await addressFormatter.format(address);
console.log(result)
return result;
}
const of = async () => {
const promise = addressFormatter.getOrderedFields('CA');
promise.then(result => {
console.log(result);
return result;
});
}
let results = {
"formattedAddress": fa(),
"orderedFields": of()
}
callback(results);
}
The only thing that I receive in the template are [object Promise]. The console.logs in the getData() method actually show the accurate data but they are not displayed in teamplte. What can I do to receive the values and make my helper wait for them?
Edit: I have edited it according to #Jankapunkt answer but the objects are still empty, while the results in getData() are not.
You don't. Helpers are there to immediately return values but are triggered by reactive data sources.
If you want a helper to "run" once the data "arrived" then your should move this code into onCreated and store the value in a reactive data source:
import { Template } from 'meteor/templating';
import { ReactiveDict } from 'meteor/reactive-dict'
import AddressFormatter from '#shopify/address';
import './main.html';
const address = {
company: 'Shopify',
firstName: '恵子',
lastName: '田中',
address1: '八重洲1-5-3',
address2: '',
city: '目黒区',
province: 'JP-13',
zip: '100-8994',
country: 'JP',
phone: '',
};
Template.hello.onCreated(function () {
const instance = this
instance.state = new ReactiveDict()
instance.state.setDefault('result', {
"formattedAddress": "",
"orderedFields": ""
})
const addressFormatter = new AddressFormatter('ja')
getData(addressFormatter)
.then(({ formattedAddress, orderedFields }) => {
// the next line triggers the helper, since it "observes" the changes
// to this "result" property on the reactive-dictionary
instance.state.set('result', { formattedAddress, orderedFields })
})
.catch(e => console.error(e))
return results;
})
Template.hello.helpers({
address: function() {
return Template.instance().state.get('result')
}
});
const getData = async function (addressFormatter) {
const formattedAddress = await addressFormatter.format(address)
const orderedFields = await addressFormatter.getOrderedFields('CA')
return {
formattedAddress,
orderedFields
}
}
Readings: http://blazejs.org/
Edit: added a simplified getData that should work

Maximum call stack size exceeded( in Nuxt + Firebase Project)

I'm currently creating an authentication feature in Nuxt and Firebase.
The login and logout process itself can be done and the header display changes accordingly, but there is an error in console when I press the login button.
Error content (in console)
Uncaught RangeError: Maximum call stack size exceeded
at Function.keys (<anonymous>)
code
Header.vue(This is the page containing the login button.)↓
googleLogin () {
const provider = new firebase.auth.GoogleAuthProvider()
auth.signInWithPopup(provider)
.then(res => {
this.dialogAuthVisible = false
this.$store.dispatch('auth/setUser',res.user)
}).catch(e => console.log(e))
}
store/auth.js↓
export const strict = false
export const state = () => ({
user: null
})
export const mutations = {
SET_USER (state, payload) {
state.user = payload
}
}
export const actions = {
setUser ({ commit }, user) {
commit('SET_USER',user)
}
}
export const getters = {
isAuthenticated (state) {
return !!state.user
}
}
default.vue↓
mounted () {
auth.onAuthStateChanged(user => {
const { uid, displayName, photoURL} = user
if (user) {
this.$store.dispatch('auth/setUser', { uid, displayName, photoURL})
} else {
this.$store.dispatch('auth/setUser', null)
}
})
}
If there's any information I'm missing, please let me know 🙇️.
Please teach me how to do this 🙇️.
I think the problem is in this code lines :
export const mutations = {
SET_USER (state, payload) {
state.user = payload
}
}
export const actions = {
setUser ({ commit }, user) {
commit('SET_USER',user)
}
}
There is a loop between this mutations and actions
Instead of setting the entire payload into the store object, I just picked the fields I needed, and that resolved the problem for me.
Before:
AUTH_STATUS_CHANGED ({commit}, data: any): any {
if (data && data.authUser) {
commit('SetAuthUser', data.authUser);
} else {
commit('SetAuthUser', null);
}
}
After:
AUTH_STATUS_CHANGED ({commit}, data: any): any {
if (data && data.authUser) {
const user = data.authUser;
commit('SetAuthUser', {
uid: user.uid,
email: user.email,
emailVerified: user.emailVerified,
displayName: user.displayName,
isAnonymous: user.isAnonymous,
photoURL: user.photoURL,
stsTokenManager: user.stsTokenManager,
createdAt: user.createdAt,
lastLoginAt: user.lastLoginAt,
apiKey: user.apiKey,
});
} else {
commit('SetAuthUser', null);
}
}
Inside the mutation, just add the value received from the mutation payload.

Resources