I have a component using intersection observer and want to test in a jest test the effects if the element is intersecting. I already managed to mock the Intersection Observer like in this answer: https://stackoverflow.com/a/58651649/4716214
Now I want to "fake callback" the isIntersecting trigger at a specific element. Is it possible to mock this in the test?
const observe = jest.fn();
const disconnect = jest.fn();
setupIntersectionObserverMock({ observe: observe, disconnect: disconnect });
const page = await newSpecPage({
components: [TestComponent],
html: `<div></div>`, // This is the element I want to trigger an "isIntersecting" on
});
export const setupIntersectionObserverMock = ({
root = null,
rootMargin = '',
thresholds = [],
disconnect = () => null,
observe = () => null,
takeRecords = () => null,
unobserve = () => null,
} = {}): void => {
class MockIntersectionObserver implements IntersectionObserver {
readonly root: Element | null = root;
readonly rootMargin: string = rootMargin;
readonly thresholds: ReadonlyArray<number> = thresholds;
disconnect: () => void = disconnect;
observe: (target: Element) => void = observe;
takeRecords: () => IntersectionObserverEntry[] = takeRecords;
unobserve: (target: Element) => void = unobserve;
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
});
Object.defineProperty(global, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
});
};
Yes it is possible. You need to do 2 changes:
takeRecords returns the values for each entry when an observe is triggered.
This means you need to change takeRecords either by giving it into setupIntersectionObserverMock or by default prop:
export function setupIntersectionObserverMock({
root = null,
rootMargin = '',
thresholds = [],
disconnect = () => null,
observe = () => null,
takeRecords = () => [
{
boundingClientRect: {} as DOMRectReadOnly,
intersectionRatio: 1,
intersectionRect: {} as DOMRectReadOnly,
isIntersecting: true,
rootBounds: null,
target: {} as Element,
time: 1,
},
],
unobserve = () => null,
} = {}): void {
The second step is to handle the IntersectionObserver constructor callback:
constructor(callback: (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void) {
callback(takeRecords(), this);
}
The whole code looks like:
export function setupIntersectionObserverMock({
root = null,
rootMargin = '',
thresholds = [],
disconnect = () => null,
observe = () => null,
takeRecords = () => [
{
boundingClientRect: {} as DOMRectReadOnly,
intersectionRatio: 1,
intersectionRect: {} as DOMRectReadOnly,
isIntersecting: true,
rootBounds: null,
target: {} as Element,
time: 1,
},
],
unobserve = () => null,
} = {}): void {
class MockIntersectionObserver implements IntersectionObserver {
readonly root: Element | null = root;
readonly rootMargin: string = rootMargin;
readonly thresholds: ReadonlyArray<number> = thresholds;
disconnect: () => void = disconnect;
observe: (target: Element) => void = observe;
takeRecords: () => IntersectionObserverEntry[] = takeRecords;
unobserve: (target: Element) => void = unobserve;
constructor(callback: (entries: IntersectionObserverEntry[], observer: IntersectionObserver) => void) {
callback(takeRecords(), this);
}
}
Object.defineProperty(window, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
});
Object.defineProperty(global, 'IntersectionObserver', {
writable: true,
configurable: true,
value: MockIntersectionObserver,
});
}
Related
The url I put inside axios.get is a wrong url. I am expecting an error message through action.error.message in the reducer section. But it's not returning any message despite the fetch request being failed. The code as follows:
usersSlice.js
const createSlice = require("#reduxjs/toolkit").createSlice;
const createAsyncThunk = require("#reduxjs/toolkit").createAsyncThunk;
const axios = require("axios");
const initialState = {
loading: false,
users: [],
error: "",
};
const fetchUsers = createAsyncThunk("users/fetchUsers", () => {
return axios
.get("https://jsonplaceholder.typicodeasdf.com/users")
.then((res) => console.log(res.data.map((user) => user.name)));
});
const usersSlice = createSlice({
name: "users",
initialState,
extraReducers: (builder) => {
builder.addCase(fetchUsers.pending, (state) => {
state.loading = true;
});
builder.addCase(fetchUsers.fulfilled, (state, action) => {
state.loading = false;
state.users = action.payload;
console.log("success");
});
builder.addCase(fetchUsers.rejected, (state, action) => {
state.loading = false;
state.users = [];
state.error = action.error.message;
console.log("failed");
});
},
});
module.exports = usersSlice.reducer;
module.exports.fetchUsers = fetchUsers;
store.js
const configureStore = require("#reduxjs/toolkit").configureStore;
const reduxLogger = require("redux-logger");
const usersSlice = require("./Features/users/userSlice");
const store = configureStore({
reducer: {
usersSlice: usersSlice,
},
});
module.exports = store;
index.js
const { fetchUsers } = require("./App/Features/users/userSlice.js");
const store = require("./App/store.js");
console.log("intial state", store.getState());
const unsubscribe = store.subscribe(() =>
console.log("updated state", store.getState())
);
store.dispatch(fetchUsers());
unsubscribe();
output
intial state { usersSlice: { loading: false, users: [], error: '' } }
updated state { usersSlice: { loading: true, users: [], error: '' } }
failed
Note
In the output I am expecting
updated state { usersSlice: { loading: false, users: [], error: 'some error message' } }
The thunk function in RTK has a built-in method to return an error message if the promise is rejected. I was actually willing to get that message. However, I resolved the problem. Instead of returning the data I actually console logged it. And completely forgot to fix it.
const fetchUsers = createAsyncThunk("users/fetchUsers", () => {
return axios
.get("https://jsonplaceholder.typicodeasdf.com/users")
**.then((res) => console.log(res.data.map((user) => user.name)));**
});
I am using Node with websocket and I have this function:
const validatedCep = async () => {
const data = await axios
.get(`https://viacep.com.br/ws/${message}/json/`)
.then((res) => {
return res.data;
})
.catch((err) => {
return err.response;
});
console.log(1, data);
return data;
};
if (this.props.dataType === "CEP") {
validatedCep();
}
How can I get the value returned in response and access that value outside the validatedCep function?
I need this value to be able to check if it will return the value of the answer or an error, so that I can proceed with the logic of the function.
Full function:
import { MessageSender } from "./message-sender";
import { WappMessage } from "./wapp-message";
import axios from "axios";
export type FormProps = {
error?: string;
text: string;
dataType: string;
typingDuration: number;
};
export class WappFormMessage extends WappMessage<FormProps> {
constructor(
readonly props: FormProps,
private next: WappMessage<any> | undefined,
protected messageSender: MessageSender<FormProps>
) {
super(props, "response", true, messageSender);
}
getNext(message: string): WappMessage<any> | undefined {
const regexs = [
{ type: "email", regex: "^[a-z0-9]+#[a-z0-9]+\\.[a-z]+\\.?([a-z]+)?$" },
{ type: "CPF", regex: "^\\d{3}\\.?\\d{3}\\.?\\d{3}\\-?\\d{2}$" },
{ type: "CNPJ", regex: "^d{2}.?d{3}.?d{3}/?d{4}-?d{2}$" },
{
type: "cellPhone",
regex: "(^\\(?\\d{2}\\)?\\s?)(\\d{4,5}\\-?\\d{4}$)",
},
{ type: "phone", regex: "(^\\(?\\d{2}\\)?\\s?)(\\d{4}\\-?\\d{4}$)" },
{ type: "birthDate", regex: "(^\\d{2})\\/(\\d{2})\\/(\\d{4}$)" },
];
const dataTypes = [
"email",
"birthDate",
"CPF",
"CNPJ",
"cellPhone",
"phone",
];
const validateData = (element: string) => {
if (this.props.dataType === element) {
const getRegex = regexs.find((regexs) => regexs.type === element);
const regexCreate = new RegExp(getRegex!.regex, "i");
const validate = regexCreate.test(message);
return validate;
}
return true;
};
const isValid = dataTypes.find(validateData);
if (!isValid) {
return new WappFormMessage(
{
error: "Invalid data!",
...this.props,
},
this.next,
this.messageSender
);
}
const validatedCep = async () => {
const data = await axios
.get(`https://viacep.com.br/ws/${message}/json/`)
.then((res) => {
return res.data;
})
.catch((err) => {
return err.response;
});
console.log(1, data);
return data;
};
if (this.props.dataType === "CEP") {
validatedCep();
}
return this.next;
}
async send(remoteJid: string): Promise<void> {
await this.messageSender.send(
remoteJid,
this.props,
this.props.typingDuration
);
}
}
How to change the implementation of a mock depending on the test.
this is the code I am trying to unit test.
open(url: string, target: string = '_self'): void {
const win = this.document.defaultView?.open(url, target);
win?.focus();
}
And below my unit test
const MockDocument = {
location: { replace: jest.fn() },
defaultView: undefined
};
const createService = createServiceFactory({
service: RedirectService,
providers: [
{ provide: DOCUMENT, useValue: MockDocument }
]
});
My strategy is to set defaultView to undefined for the first test. And inside the next test, I would change the implementation to contain the open function, like this
const defaultView = {
open: jest
.fn()
.mockImplementation((url: string, target: string = '_self') => {
url;
target;
return { focus: jest.fn().mockImplementation(() => {}) };
})
};
const MockDocument = {
location: { replace: jest.fn() },
defaultView: defaultView
};
How do I change the implementation depending on the test?
Thanks for helping
EDIT
it('should not call "open" if defaultView is not truthy', () => {
spectator.service.openFocus('foo');
expect(mockDocument.defaultView).not.toBeTruthy();
});
it('should call "open" if defaultView is truthy', () => {
//CHANGE THE IMPLEMENTATION HERE...
const spy = jest.spyOn(mockDocument.defaultView, 'open')
spectator.service.openFocus('foo');
expect(spy).toHaveBeenCalled();
});
You can set defaultView up as a variable and change the variable speed test.
let defaultView: any;
const MockDocument = {
location: { replace: jest.fn() },
defaultView};
beforeEach(() => {
...
});
it('should not call "open" if defaultView is not truthy', () => {
defaultView = undefined;
expect(mockDocument.defaultView).not.toBeTruthy();
});
it('should call "open" if defaultView is truthy', () => {
defaultView = {
open: jest
.fn()
.mockImplementation((url: string, target: string = '_self') => {
url;
target;
return { focus: jest.fn().mockImplementation(() => {}) };
})
};
const spy = jest.spyOn(mockDocument.defaultView, 'open')
expect(spy).toHaveBeenCalled();
});
I am currently trying to do a test on media queries but I can't figure out. How possibly I can change width of the window so it reflects to media query.
I have tried all commented code to mock the width but no use. I know it has something to do with jsdom but cant figure out.
it('when image is WIDE and media match with medium', () => {
Object.defineProperty(window, 'innerWidth', {writable: true, configurable: true, value: 900})
// window.resizeBy = jest.fn();
// window.resizeBy(900,300);
//const mediaQuery = '(min-width: 790px)';
// Object.defineProperty(window, "matchMedia", {
// value: jest.fn(() => { return { matches: true,media: mediaQuery } })
// });
// Object.defineProperty(window,'innerWidth', {
// configurable: true,
// writable: true,
// value: 790
// });
// Object.defineProperty(window,'innerHeight', {
// configurable: true,
// writable: true,
// value: 900
// });
window.matchMedia = jest.fn().mockImplementation(query => {
//query always display (min-width: 240px) and (max-width: 767px)
return {
matches: true,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn(),
};
});
const result = imageStretcher(aWideImage);
expect(result).toEqual({ imgWidth: '90%', imgHeight: '40%' });
});
This is the code I want to test.
const screenResolution = {
smallSize: '(min-width: 240px) and (max-width: 767px)',
mediumSize: '(min-width: 768px) and (max-width: 1200px)',
};
const sizeTypes = {
smallSize: 'smallSize',
mediumSize: 'mediumSize',
normalSize: 'normalSize',
};
const sizeSettings = {
[PORTRAIT]: {
smallSize: { imgWidth: '62%', imgHeight: '50%' },
mediumSize: { imgWidth: '95%', imgHeight: '75%' },
normalSize: { imgWidth: '70%', imgHeight: '90%' },
},
[WIDE]: {
smallSize: { imgWidth: '90%', imgHeight: '30%' },
mediumSize: { imgWidth: '90%', imgHeight: '40%' },
normalSize: { imgWidth: '65%', imgHeight: '80%' },
},
};
const screenResolutionMatcher = (imageType: number) => {
if (window.matchMedia(screenResolution.smallSize).matches) {
return setNewImageSize(imageType, sizeTypes.smallSize);
} else if (window.matchMedia(screenResolution.mediumSize).matches) {
return setNewImageSize(imageType, sizeTypes.mediumSize);
} else {
return setNewImageSize(imageType, sizeTypes.normalSize);
}
};
export const imageStretcher = (source: string) => {
//....
return screenResolutionMatcher(imageType);
}
};
OK finally after having crazy rough time with this I have figured it out this way it test medium screen.
it('when image is WIDE and media match with medium', () => {
window.matchMedia = jest.fn().mockImplementation(query => ({
matches: query !== '(min-width: 240px) and (max-width: 767px)',
media: '',
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn()
}));
const result = imageStretcher(aWideImage);
expect(result).toEqual({ imgWidth: '90%', imgHeight: '40%' });
});
I'm using Material-UI's useMediaQuery() function in one of my components to determine the size prop to use for a <Button> within the component.
I'm trying to test that it's working as expected in a jest test, however my current implementation isn't working:
describe("Unit: <Navbar> On xs screens", () => {
// Incorrectly returns `matches` as `false` ****************************
window.matchMedia = jest.fn().mockImplementation(
query => {
return {
matches: true,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn()
};
}
);
it("renders as snapshot", async () => {
const width = theme.breakpoints.values.sm - 1;
const height = Math.round((width * 9) / 16);
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: width
});
const { asFragment } = render(
<Container backgroundColor={"#ffffff"}>
<Navbar />
</Container>
);
expect(asFragment()).toMatchSnapshot();
const screenshot = await generateImage({
viewport: { width, height }
});
expect(screenshot).toMatchImageSnapshot();
});
});
describe("Unit: <Navbar> On md and up screens", () => {
// Correctly returns `matches` as `false` ****************************
window.matchMedia = jest.fn().mockImplementation(
query => {
return {
matches: false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn()
};
}
);
it("renders as snapshot", async () => {
const width = theme.breakpoints.values.md;
const height = Math.round((width * 9) / 16);
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: width
});
const { asFragment } = render(
<Container backgroundColor={"#ffffff"}>
<Navbar />
</Container>
);
expect(asFragment()).toMatchSnapshot();
const screenshot = await generateImage({
viewport: { width, height }
});
expect(screenshot).toMatchImageSnapshot();
});
});
And the component I'm testing (removed irrelevant parts):
const Navbar = () => {
const theme = useTheme();
const matchXs = useMediaQuery(theme.breakpoints.down("xs"));
return (
<Button size={matchXs ? "medium" : "large"}>
Login
</Button>
);
};
export default Navbar;
It's returning matches as false for the first test, even though I've set it to return as true. I know this because it's generating a screenshot and I can see that the button size is set to large for the first test when it should be set to medium.
It works as expected in production.
How do I correctly get mock useMediaQuery() in a jest test?
The recommended way is using css-mediaquery which is now mentioned in the MUI docs:
import mediaQuery from 'css-mediaquery';
function createMatchMedia(width) {
return query => ({
matches: mediaQuery.match(query, { width }),
addListener: () => {},
removeListener: () => {},
});
}
describe('MyTests', () => {
beforeAll(() => {
window.matchMedia = createMatchMedia(window.innerWidth);
});
});
I figured it out...
useMediaQuery() needs to re-render the component to work, as the first render will return whatever you define in options.defaultMatches (false by default).
Also, the mock needs to be scoped to each test (it), not in the describe.
As I'm using react-testing-library, all I have to do is re-render the component again and change the scope of the mock and it works.
Here's the working example:
const initTest = width => {
Object.defineProperty(window, "innerWidth", {
writable: true,
configurable: true,
value: width
});
window.matchMedia = jest.fn().mockImplementation(
query => {
return {
matches: width >= theme.breakpoints.values.sm ? true : false,
media: query,
onchange: null,
addListener: jest.fn(),
removeListener: jest.fn()
};
}
);
const height = Math.round((width * 9) / 16);
return { width, height };
};
describe("Unit: <Navbar> On xs screens", () => {
it("renders as snapshot", async () => {
const { width, height } = initTest(theme.breakpoints.values.sm - 1);
const { asFragment, rerender} = render(
<Container backgroundColor={"#ffffff"}>
<Navbar />
</Container>
);
rerender(
<Container backgroundColor={"#ffffff"}>
<Navbar />
</Container>
);
expect(asFragment()).toMatchSnapshot();
const screenshot = await generateImage({
viewport: { width, height }
});
expect(screenshot).toMatchImageSnapshot();
});
});
describe("Unit: <Navbar> On md and up screens", () => {
it("renders as snapshot", async () => {
const { width, height } = initTest(theme.breakpoints.values.md);
const { asFragment } = render(
<Container backgroundColor={"#ffffff"}>
<Navbar />
</Container>
);
rerender(
<Container backgroundColor={"#ffffff"}>
<Navbar />
</Container>
);
expect(asFragment()).toMatchSnapshot();
const screenshot = await generateImage({
viewport: { width, height }
});
expect(screenshot).toMatchImageSnapshot();
});
});
A simple approach that worked for me:
component.tsx
import { Fade, Grid, useMediaQuery } from '#material-ui/core';
...
const isMobile = useMediaQuery('(max-width:600px)');
component.spec.tsx
import { useMediaQuery } from '#material-ui/core';
// not testing for mobile as default
jest.mock('#material-ui/core', () => ({
...jest.requireActual('#material-ui/core'),
useMediaQuery: jest.fn().mockReturnValue(false),
}));
describe('...', () => {
it(...)
// test case for mobile
it('should render something for mobile', () => {
((useMediaQuery as unknown) as jest.Mock).mockReturnValue(true);
....
})
Inspired by #iman-mahmoudinasab's accepted answer, here is a TypeScript implementation. Essentially, we would want to create valid MediaQueryList-object:
// yarn add -D css-mediaquery #types/css-mediaquery
import mediaQuery from 'css-mediaquery';
describe('Foo Bar', () => {
beforeAll(() => {
function createMatchMedia(width: number) {
return (query: string): MediaQueryList => ({
matches: mediaQuery.match(query, { width }) as boolean,
media: '',
addListener: () => {},
removeListener: () => {},
onchange: () => {},
addEventListener: () => {},
removeEventListener: () => {},
dispatchEvent: () => true,
});
}
// mock matchMedia for useMediaQuery to work properly
window.matchMedia = createMatchMedia(window.innerWidth);
});
});
Maybe we can do something like this:
const applyMock = (mobile) => {
window.matchMedia = jest.fn().mockImplementation((query) => {
return {
matches: mobile,
media: query,
addListener: jest.fn(),
removeListener: jest.fn(),
};
});
};
and use it like this:
const mockMediaQueryForMobile = () => applyMock(true);
const mockMediaQueryForDesktop = () => applyMock(false);