Learning JEST and unit testing in Javascript, but have errors when testing. There's an error
Invalid hook call. Hooks can only be called inside of the body of a
function component
You might have mismatching versions of React and the renderer (such as React DOM)
You might be breaking the Rules of Hooks
You might have more than one copy of React in the same app
See https://reactjs.org/link/invalid-hook-call for tips about how to debug and fix this problem.
I believe that the error comes when the use of renderInTestApp.
import * as React from 'react';
import { render, RenderResult, screen } from '#testing-library/react';
import ReactDom from 'react-dom'
import { EntityJiraOverviewCard, isJiraAvailable } from '#roadiehq/backstage-plugin-jira';
import { permissionApiRef } from '#backstage/plugin-permission-react';
import { overviewContent } from './EntityPage';
import { renderWithEffects } from '#backstage/test-utils';
import {
MockPermissionApi,
renderInTestApp,
TestApiProvider,
} from '#backstage/test-utils';
import {
EntityProvider,
starredEntitiesApiRef,
MockStarredEntitiesApi,
} from '#backstage/plugin-catalog-react';
import { EntityLayout } from '#backstage/plugin-catalog';
describe('Developer Portal Plugin', () => {
const jira = {
apiVersion: 'vi',
kind: 'Component',
metadata: {
name: 'TestPlugin',
annotations: {
'jira/project-key': 'DET',
},
},
spec:{
'type': 'service',
'lifecycle' : 'production',
'owner' : 'test',
},
};
const mockedApi = {
listWorkflowRuns: jest.fn().mockResolvedValue([]),
};
const mockPermissionApi = new MockPermissionApi();
describe('jiraContent', () => {
it('Should render Jira View', async () => {
const rendered = await renderInTestApp(
<TestApiProvider
apis={[
[isJiraAvailable, mockedApi],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[permissionApiRef, mockPermissionApi],
]}
>
<EntityProvider entity={jira}>
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{overviewContent}
</EntityLayout.Route>
</EntityLayout>
</EntityProvider>
</TestApiProvider>,
);
expect(rendered.getByText('TestPlugin')).toBeInTheDocument();
await expect(
rendered.findByText('Devops Engineering Team | software'),
).resolves.toBeInTheDocument();
expect(rendered.getByText('Activity stream')).toBeInTheDocument();
});
});
})
Related
Using vue-test-utils to test the component using pinia, I need to modify the value of the state stored in pinia, but I have tried many methods to no avail. The original component and store files are as follows.
// HelloWorld.vue
<template>
<h1>{{ title }}</h1>
</template>
<script>
import { useTestStore } from "#/stores/test";
import { mapState } from "pinia";
export default {
name: "HelloWorld",
computed: {
...mapState(useTestStore, ["title"]),
},
};
</script>
// #/stores/test.js
import { defineStore } from "pinia";
export const useTestStore = defineStore("test", {
state: () => {
return { title: "hhhhh" };
},
});
The following methods have been tried.
Import the store used within the component to the test code and make changes directly, but the changes cannot affect the component.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
import { useTestStore } from "#/stores/test";
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: {
plugins: [createTestingPinia()],
},
});
const store = useTestStore();
store.title = "xxxxx";
console.log(wrapper.text()) //"hhhhh";
});
Using the initialState in an attempt to overwrite the contents of the original store, but again without any effect.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: {
plugins: [createTestingPinia({ initialState: { title: "xxxxx" } })],
},
});
console.log(wrapper.text()) //"hhhhh";
});
Modify the TestingPinia object passed to global.plugins in the test code, but again has no effect.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
test("pinia in component test", () => {
const pinia = createTestingPinia();
pinia.state.value.title = "xxxxx";
const wrapper = mount(HelloWorld, {
global: {
plugins: [pinia],
},
});
console.log(wrapper.text()) //"hhhhh";
});
Use global.mocks to mock the states used in the component, but this only works for the states passed in with setup() in the component, while the ones passed in with mapState() have no effect.
// test.spec.js
import { mount } from "#vue/test-utils";
import { createTestingPinia } from "#pinia/testing";
import HelloWorld from "#/components/HelloWorld.vue";
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: {
plugins: [createTestingPinia()],
mocks: { title: "xxxxx" },
},
});
console.log(wrapper.text()) //"hhhhh"
});
This has been resolved using jest.mock().
import { mount } from "#vue/test-utils";
import { createPinia } from "pinia";
import HelloWorld from "#/components/HelloWorld.vue";
jest.mock("#/stores/test", () => {
const { defineStore } = require("pinia");
const useTestStore = defineStore("test", { state: () => ({ title: "xxxxx" }) });
return { useTestStore };
});
test("pinia in component test", () => {
const wrapper = mount(HelloWorld, {
global: { plugins: [createPinia()] },
});
expect(wrapper.text()).toBe("xxxxx");
});
Thanks to Red Panda for this topic. I use "testing-library", and "vue-testing-library" instead of "vue-test-utils" and "jest", but the problem is the same - couldn't change pinia initial data of the store.
I finally found a solution for this issue without mocking the function.
When you $patch data, you just need to await for it. Somehow it helps. My code looks like this and it totally works:
Popup.test.js
import { render, screen } from '#testing-library/vue'
import { createTestingPinia } from '#pinia/testing'
import { popup } from '#/store1/popup/index'
import Popup from '../../components/Popup/index.vue'
describe('Popup component', () => {
test('displays popup with group component', async () => {
render(Popup, {
global: { plugins: [createTestingPinia()] }
})
const store = popup()
await store.$patch({ popupData: 'new name' })
screen.debug()
})
})
OR you can set initialState using this scheme:
import { render, screen } from '#testing-library/vue'
import { createTestingPinia } from '#pinia/testing'
import { popup } from '#/store1/popup/index'
import Popup from '../../components/Popup/index.vue'
test('displays popup with no inner component', async () => {
const { getByTestId } = render(Popup, {
global: {
plugins: [
createTestingPinia({
initialState: {
popup: {
popupData: 'new name'
}
}
})
]
}
})
const store = popup()
screen.debug()
})
Where popup in initialState - is the imported pinia store from #/store1/popup. You can specify any of them there the same way.
Popup.vue
<script>
import { defineAsyncComponent, markRaw } from 'vue'
import { mapState, mapActions } from 'pinia'
import { popup } from '#/store1/popup/index'
export default {
data () {
return {}
},
computed: {
...mapState(popup, ['popupData'])
},
....
I'm working on a project using Vue 3 with composition API styling.
Composition API is used for both components and defining my store.
Here is my store
player.js
import { defineStore } from 'pinia'
import { ref, reactive } from 'vue'
export const usePlayerStore = defineStore('player',()=>{
const isMainBtnGameClicked = ref(false)
return { isMainBtnGameClicked }
})
MyComponent.vue
//import { usePlayerStore } from '...'
const playerStore = usePlayerStore()
playerStore.isMainBtnGameClicked = true
isMainBtnGameClicked from my store is updated properly.
You can also update variables from components by passing them by reference to the pinia store. It's working in my project.
For sake of saving future me many hours of trouble, there is a non-obvious thing in play here - the event loop. Vue reactivity relies on the event loop running to trigger the cascade of state changes.
When you mount/shallowMount/render a component with vue-test-utils, there is no event loop running automatically. You have to trigger it manually for the reactivity to fire, e.g.
await component.vm.$nextTick;
If you don't want to mess around with ticks, you have to mock the store state/getters/etc. (which the docs strongly lean toward, without explaining the necessity). Here OP mocked the whole store.
See also: Vue-test-utils: using $nextTick multiple times in a single test
I'm trying to integrate CleverTap into my Next.js app. Followed the documentation Web SDK Quick Start Guide but facing issue:
Server Error ReferenceError: window is not defined in Next.js
_app.tsx
import React, { useEffect, useState } from "react";
import type { AppProps } from "next/app";
import { appWithTranslation } from "next-i18next";
import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
import { ReactQueryDevtools } from "react-query/devtools";
import nextI18NextConfig from "../next-i18next.config.js";
import "tailwindcss/tailwind.css";
import "styles/globals.scss";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
import { useRouter } from "next/router";
import SvgPageLoading from "components/color-icons/PageLoading";
// import { PageLoading } from "components/color-icons/";
import { DefaultSeo } from 'next-seo';
import SEO from 'next-seo.config';
import {cleverTap} from "utils/cleverTapHelper";
cleverTap.initialize('TEST-61c-a12');
function MyApp({ Component, pageProps }: AppProps) {
const [queryClient] = React.useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
staleTime: Infinity,
},
},
})
);
const router = useRouter();
const [isAnimating, setIsAnimating] = useState(false);
useEffect(() => {
const handleStart = () => {
setIsAnimating(true);
};
const handleStop = () => {
setIsAnimating(false);
};
router.events.on("routeChangeStart", handleStart);
router.events.on("routeChangeComplete", handleStop);
router.events.on("routeChangeError", handleStop);
return () => {
router.events.off("routeChangeStart", handleStart);
router.events.off("routeChangeComplete", handleStop);
router.events.off("routeChangeError", handleStop);
};
}, [router]);
return (
<QueryClientProvider client={queryClient}>
<Hydrate state={pageProps.dehydratedState}>
<DefaultSeo {...SEO} />
<Component {...pageProps} />
{isAnimating && (
<div className="fixed top-0 left-0 flex items-center justify-center w-screen h-screen overflow-visible bg-white bg-opacity-80 z-overlay top-z-index">
<SvgPageLoading />
</div>
)}
<ReactQueryDevtools initialIsOpen={false} />
</Hydrate>
</QueryClientProvider>
);
}
export default appWithTranslation(MyApp, nextI18NextConfig);
cleverTapHelper.ts
export const cleverTap = {
initialize: function (accountId) {
console.log('I see initialize req')
window.clevertap = {event: [], profile: [], account: [], onUserLogin: [], notifications: []};
window.clevertap.account.push({'id': accountId});
(function () {
var wzrk = document.createElement('script');
wzrk.type = 'text/javascript';
wzrk.async = true;
wzrk.src = ('https:' == document.location.protocol ? 'https://d2r1yp2w7bby2u.cloudfront.net' : 'http://static.clevertap.com') + '/js/a.js';
var s = document.getElementsByTagName('script')[0];
s.parentNode.insertBefore(wzrk, s);
})();
},
event: function (name, payload = {}) {
console.log('I see event req')
if (payload) {
window.clevertap.event.push(name, payload);
} else {
window.clevertap.event.push(name);
}
},
profile: function (payload) {
console.log('I see profile req')
window.clevertap.profile.push(payload);
},
logout: function () {
console.log('I see logout req')
window.clevertap.logout();
}
};
cleverTap.d.ts
declare global {
interface Window {
clevertap: any;
}
}
export {};
Window object should not be undefined but getting undefined! What's going on?
This is because NextJS is trying to execute that function on the server because it uses SSR, and window is a browser object. Since the window object is available only in the browser (client-side), the server is unable to identify the window object, hence getting undefined. In order to fix this, you should make sure that any functions/components that contain client-side related code be executed only on the browser or client-side. One way is using hooks such as useEffect that run only after the component is mounted. Another way is to use lazy loading which pretty much does the same thing.
Using useEffect hook.
In your _app.tsx component, add a new useEffect hook and move the initialization code into the newly created useEffect function.
useEffect(()=>{
cleverTap.initialize('TEST-61c-a12');
},[])
Using lazy loading. (Dynamic import)
Instead of directly importing the function, import it dynamically and set server-side rendering to false:
import dynamic from 'next/dynamic'
const cleverTap = dynamic(()=>{
return import("utils/cleverTapHelper")
},
{ssr:false}
)
cleverTap.initialize('TEST-61c-a12');
For TS folks struggling out there with clevertap and nextjs, install sdk and types:
npm i -S clevertap-web-sdk #types/clevertap-web-sdk
then, async import while initializing:
import CleverTap from 'clevertap-web-sdk/clevertap';
// ^^ this only imports types
let clevertap: CleverTap;
export const initClevertap = async (clevertapAccountID: string, region: string): Promise<void> => {
clevertap = (await import('clevertap-web-sdk')).default;
clevertap.init(clevertapAccountID, region);
clevertap.privacy.push({ optOut: false });
clevertap.privacy.push({ useIP: true });
clevertap.setLogLevel(0);
clevertap.spa = true;
};
PS: dynamic import didn't worked for me, looks like it's only for components and not libs
Intro:
I'm newbie in meteor, I have read the documentation and questions here about this issue but the doubt still persist. My main problem is that I cant load data of my MongoDb in client side (what methods to use to load data).
Example:
I have a project that has the following folder structure:
image
In my collections (People.js) I have this:
import { Mongo } from 'meteor/mongo';
export const People = new Mongo.Collection('people');
In the server folder ( main.js ). PS: I can't change this file.
import { People } from '../collections/people';
Meteor.startup(() => {
const TEST_DATA = [
{
something: 'This is a simple example',
}, ... ]
TEST_DATA.forEach(test => People.insert(test));
}
In UI folder (app.jsx)
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Meteor } from 'meteor/meteor';
import {People} from '../collections/people';
export class App extends Component {
render() {
return (
<div>
<h3>Teste </h3>
{ console.log(this.users) }
{ console.log(People.find({}).fetch()) }
{ console.log(Meteor.subscribe('people')) }
</div>
);
}
}
export default withTracker(() => {
return {
users: People.find({}).fetch(),
};
})(App);
cliente folder (main.jsx):
import React from 'react';
import { Meteor } from 'meteor/meteor';
import { render } from 'react-dom';
import { App } from '../ui/App';
Meteor.startup(() => {
render(<App />, document.getElementById('app'));
});
Debug:
I inspected the database and it's populated. The first console.log show undefined , the second an array of length:0, the third an object {stop: ƒ, ready: ƒ, subscriptionId: "mJQHdGxka4xTCX7FZ"} (I think it's returning this because I'm not using publish () on the server to populate the database)
What method should I use to obtain this data?
Change app.jsx to
import React, { Component } from 'react';
import { withTracker } from 'meteor/react-meteor-data';
import { Meteor } from 'meteor/meteor';
import {People} from '../collections/people';
export class App extends Component {
render() {
console.log(this.props.users)
return (
<div>
<h3>Teste </h3>
{ this.props.users.map((user) => { return (<div>user.something</div>) }) }
</div>
);
}
}
export default withTracker(() => {
Meteor.subscribe('people');
return {
users: People.find({}).fetch(),
};
})(App);
I've modified the code a little further in the direction that I would assume you'd want it to go. The core change is it use this.props.users. That's where the properties in React components can be found, and where they are places by the function returned by withTracker.
This all assumes that you are still using autopublish (as meteor does in new projects until you remove it). If you have already removed that package, then you need to add
Meteor.publish('people', () => {
return People.find();
});
in your server code.
I have tried many solutions I found on google to test if Component.propTypes was set properly at a react component, but none of them worked for me. Even though I get a console warning when running my React application on the browser if the properties are passed incorrectly, when I run jest I can't capture that warning in any way that I tried. Here is my best attempt:
App.js:
export class App extends Component {
constructor(props) {
super(props);
}
render() {
return <div/>;
}
}
App.propTypes = {
images: PropTypes.array.isRequired
};
function mapStateToProps(state) {
const {images} = state;
return {images: images};
}
export default connect(mapStateToProps)(App);
App.test.js:
import React from 'react';
import chai from 'chai';
import chaiEnzyme from 'chai-enzyme';
import {shallow} from 'enzyme';
import configureStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import sinon from 'sinon'
import {Provider} from 'react-redux';
import App from './App';
const expect = chai.use(chaiEnzyme()).expect
const mockStore = configureStore([thunk]);
const wrap = (initialState, props) => {
return shallow(<Provider store={mockStore(initialState)}><App {...props} /></Provider>)
};
describe('App container', () => {
it('validates properties', () => {
const stub = sinon.stub(console, 'warn');
console.warn.reset();
React.createElement(App, {});
expect(stub.calledOnce).to.equal(true);
expect(stub.calledWithMatch(/Failed prop type/)).to.equal(true);
console.warn.restore();
});
it('renders without crashing', () => {
wrap();
});
it('is react-redux connected', () => {
const wrapper = wrap();
expect(wrapper.find('Connect(App)')).to.have.length(1);
});
it('correctly maps properties', () => {
const wrapper = wrap({images: []});
expect(wrapper.props().images).to.equal([]);
});
});
According to what I've read online on various GitHub issue threads, it seems like a common approach is to make console.warn/console.error throw.
So when you are writing your tests you can do something like
expect(// Render Component //).to.throw();
Hope this helps.
More info: https://github.com/airbnb/enzyme/issues/588
A component is importing a library that includes a native module. Here is a contrived example:
import React from 'react';
import { View } from 'react-native';
import { Answers } from 'react-native-fabric';
export default function MyTouchComponent({ params }) {
return <View onPress={() => { Answers.logContentView() }} />
}
And here is the relevant part of Answers from react-native-fabric:
var { NativeModules, Platform } = require('react-native');
var SMXAnswers = NativeModules.SMXAnswers;
When importing this component in a mocha test, this fails on account that SMXAnswers is undefined:
How do you mock SMXAnswers or react-native-fabric so that it doesn't break and allows you to test your components?
p.s.: you can see the full setup and the component I'm trying to test on GitHub.
Use mockery to mock any native modules like so:
import mockery from 'mockery';
mockery.enable();
mockery.warnOnUnregistered(false);
mockery.registerMock('react-native-fabric', {
Crashlytics: {
crash: () => {},
},
});
Here is a complete setup example:
import 'core-js/fn/object/values';
import 'react-native-mock/mock';
import mockery from 'mockery';
import fs from 'fs';
import path from 'path';
import register from 'babel-core/register';
mockery.enable();
mockery.warnOnUnregistered(false);
mockery.registerMock('react-native-fabric', {
Crashlytics: {
crash: () => {},
},
});
const modulesToCompile = [
'react-native',
].map((moduleName) => new RegExp(`/node_modules/${moduleName}`));
const rcPath = path.join(__dirname, '..', '.babelrc');
const source = fs.readFileSync(rcPath).toString();
const config = JSON.parse(source);
config.ignore = function(filename) {
if (!(/\/node_modules\//).test(filename)) {
return false;
} else {
const matches = modulesToCompile.filter((regex) => regex.test(filename));
const shouldIgnore = matches.length === 0;
return shouldIgnore;
}
}
register(config);