Jest+React Native Testing Library: How to test an image src? - jestjs

In my new React Native app, I want to add some Jest tests.
One component renders a background image, which is located directly in the project in assets folder.
Now I stumbled about how to test if this image is actually taken from this path, therefore present in the component, and rendered correctly.
I tried using toHaveStyle from #testing-library/jest-native with a container, which returned the error toHaveStyleis not a function. Then I tried the same with queryByTestId, same error. When I do expect(getByTestId('background').toBeInTheDocument); then I feel this is useless, because it only checks if an element with this testId is present, but not the image source.
Please, how can I test this? Does it actually make sense to test an image source after all?
Here is my code:
1.) The component that should be tested (Background):
const Background: React.FC<Props> = () => {
const image = require('../../../../assets/images/image.jpg');
return (
<View>
<ImageBackground testID="background" source={image} style={styles.image}></ImageBackground>
</View>
);
};
2.) The test:
import React from 'react';
import {render, container} from 'react-native-testing-library';
import {toHaveStyle} from '#testing-library/jest-native';
import '#testing-library/jest-native/extend-expect';
import Background from '../Background';
describe('Background', () => {
test('renders Background image', () => {
const {getByTestId} = render(<Background></Background>);
expect(getByTestId('background').toBeInTheDocument);
/* const container = render(<Background background={background}></Background>);
expect(container).toHaveStyle(
`background-image: url('../../../../assets/images/image.jpg')`,
); */
/* expect(getByTestId('background')).toHaveStyle(
`background-image: url('../../../../assets/images/image.jpg')`,
); */
});
});

If you're using #testing-library/react rather than #testing-library/react-native, and you have an alt attribute on your image, you can avoid using getByDataTestId and instead use getByAltText.
it('uses correct src', async () => {
const { getByAltText } = await render(<MyComponent />);
const image = getByAltText('the_alt_text');
expect(image.src).toContain('the_url');
// or
expect(image).toHaveAttribute('src', 'the_url')
});
Documentation.
Unfortunately, it appears that React Native Testing Library does not include getByAltText. (Thank you, #P.Lorand!)

It's a little hard to say because we can't see <ImageBackground> component or what it does... But if it works like an <img> component we can do something like this.
Use a selector on the image component through its role / alt text / data-testid:
const { getByDataTestId } = render(<Background background={background}>
</Background>);
Then look for an attribute on that component:
expect(getByDataTestId('background')).toHaveAttribute('src', '../../../../assets/images/image.jpg')

When I used getByAltText and getByDataTestId I got is not a function error.
So what worked for me was:
const imgSource = require('../../../../assets/images/image.jpg');
const { queryByTestId } = render(<MyComponent testID='icon' source={imgSource}/>);
expect(queryByTestId('icon').props.source).toBe(imgSource);
I use #testing-library/react-native": "^7.1.0

I ran into this issue today and found that if your URI is a URL and not a required file, stitching the source uri onto the testID works nicely.
export const imageID = 'image_id';
...
<Image testID={`${imageID}_${props.uri}`} ... />
Test
import {
imageID
}, from '.';
...
const testURI = 'https://picsum.photos/200';
const { getByTestId } = render(<Component uri={testURI} />);
expect(getByTestId()).toBeTruthy();

I think that you are looking for:
const uri = 'http://example.com';
const accessibilityLabel = 'Describe the image here';
const { getByA11yLabel } = render (
<Image
source={{ uri }}
accessibilityLabel={accessibilityLabel}
/>
);
const imageEl = getByA11yLabel(accessibilityLabel);
expect(imageEl.props.src.uri).toBe(uri);

Related

How to solve react-hydration-error in Next.js when using `useLocalStorage` and `useDebounce`

When I try to use https://usehooks-ts.com/react-hook/use-local-storage in Next.js in the following way, I get
Unhandled Runtime Error Error: Text content does not match
server-rendered HTML.
See more info here:
https://nextjs.org/docs/messages/react-hydration-error
const [toleranceH, setToleranceH] = useLocalStorage<number>('toleranceH', 3);
const [toleranceS, setToleranceS] = useLocalStorage<number>('toleranceS', 3);
const [toleranceL, setToleranceL] = useLocalStorage<number>('toleranceL', 3);
const [results, setResults] = useState<MegaColor[]>([]);
const debouncedToleranceH = useDebounce<number>(toleranceH, 200);
const debouncedToleranceS = useDebounce<number>(toleranceS, 200);
const debouncedToleranceL = useDebounce<number>(toleranceL, 200);
useEffect(() => {
const targetColorDetailsObject = getColorDetailsObject(targetColor);
const degreeTolerance = (360 / 100) * debouncedToleranceH;
const [hueMin, hueMax] = getHueTolerance(targetColorDetailsObject.hue(), degreeTolerance);
const filteredColors = getFilteredColors(targetColorDetailsObject, loadedMegaColors, hueMin, hueMax, debouncedToleranceS, debouncedToleranceL);
setResults(filteredColors);
return () => {
// console.log('cleanup');
};
}, [targetColor, loadedMegaColors, debouncedToleranceH, debouncedToleranceS, debouncedToleranceL]);
From that help page, I still can't figure out what to adjust so that I can use both useLocalStorage and useDebounce.
I found https://stackoverflow.com/a/73411103/470749 but don't want to forcefully set a localStorage value (it should only be set by the user).
I'd suggest checking out this excellent post on rehydration by Josh W Comeau.
Since Next.js pre-renders every page by default you need to ensure that the component in which you are calling window.localstorage is only rendered on the client.
A simple solution is to:
Keep a hasMounted state
const [hasMounted, setHasMounted] = useState(false);
Toggle it inside a useEffect
useEffect(() => {
// This will only be called once the component is mounted inside the browser
setHasMounted(true);
}, []);
Add a check so that Next.js won't complain about prerendering stuff on the server that won't match the stuff that gets rendered on the client
if (!hasMounted) {
return null;
}
Ensure that the client-side stuff comes after the check
To make it more reusable you could use one of these two methods which essentially do the same:
ClientOnly Component
function ClientOnly({ children, ...delegated }) {
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
if (!hasMounted) {
return null;
}
/**
* Could also replace the <div></div> with
* <></> and remove ...delegated if no need
*/
return (
<div {...delegated}>
{children}
</div>
);
}
...
<ClientOnly>
<MyComponent /> // <--- client only stuff, safe to use useLocalStorage in here
</ClientOnly>
or
Custom useHasMounted hook
function useHasMounted() {
const [hasMounted, setHasMounted] = React.useState(false);
React.useEffect(() => {
setHasMounted(true);
}, []);
return hasMounted;
}
...
function ParentComponent() {
const hasMounted = useHasMounted();
if (!hasMounted) {
return null;
}
return (
<MyComponent />
);
}
...
function MyComponent() {
const [toleranceH, setToleranceH] = useLocalStorage<number>('toleranceH', 3);
const [toleranceS, setToleranceS] = useLocalStorage<number>('toleranceS', 3);
const [toleranceL, setToleranceL] = useLocalStorage<number>('toleranceL', 3);
...
}
...
Note:
By overdoing this or using this method at the top level of your component tree, you are killing the Next.js prerendering capabilities and turning your app into more of a "client-side heavy" app (see performance implications). If you are using window.localstorage (outside of components, where you don't have useEffect available), you should always wrap with:
if (typeof window !== 'undefined') {
// client-side code
}

make jest compile/transform/serve locally the module under test with puppeteer

I need to pass a function that is written in typescript which should run in the browser. The issue that I am having is that either I need to have the module I am testing transpiled and them encoded so I can pass it to the browser in puppeteer and it will run normally. This was the approach I was using, and it works. in short I was using es-build to bundle the module. and using readFile then encoding so I can, in the browser import it and run it there.
I am thinking if there is a better way to do this with jest-puppeteer? I can't use page.exposeFunction() because that is running on node environment. and passing the encoded function will give the browser ts code which is not what I want. To understand better look at the code bellow.
//file: module_under-test.e2e.test.ts
//importing does not help us because we might need the whole module encoded
import { testFn } from './module_under-test';
import fs, { readFileSync } from 'fs';
import util from 'util';
const readFile = util.promisify(fs.readFile);
//this will encode the module in a string, so it can be imported in the browser.
async function importer(path) {
return `data:text/javascript;utf-8,${encodeURIComponent((await readFile(path, { encoding: 'utf-8' })))}`;
}
describe('Basic authentication e2e tests', () => {
beforeAll(async () => {
await page.setViewport( {
width: 1920,
height: 1080,
deviceScaleFactor: 1
} );
//we do stuff like opening the page and logging in, etc
});
it('testToRunOnBrowser', async () => {
//module should be already transpiled but this was the old approach. I would use importer from the dist folder.
//with this the test pass but we don't want to have to transpile code everytime to run it.
//since we could already do it with only esbuild and puppeteer
expect(await page.evaluate(testToRunOnBrowser,await importer('../dist/module_under-test.mjs'))).toBe(true);
})
});
export async function testToRunOnBrowser(deps) {
const {testFn} = await import(deps)
const ctx = new browserGlobalFunctionCtx();
const data = ctx.DoGLobalBrowserThings();
ctx.load(data);
const dataLoaded = await testFn()
return dataLoaded === 'what I want to assert'
}
One way I did think but I was not able to do, is servng the whole src folder since the code from this project should all be tested on the browser. With that I can use babel standalone with "#babel/plugin-transform-modules-umd" and just import ts on the browser. any ideas or pointers how to do that with jest-puppeteer?

How do I set up dynamic imports correctly (for beyond localhost)?

I followed https://docs.meteor.com/packages/dynamic-import.html to set up dynamic imports, and it works fine on localhost.
For context, I am creating a blog (Meteor/React/Apollo) which renders MDX files, and these files need to be imported, so I have a list of all my posts as such:
import("./imports/posts/61a000d03a1931b8819dc17e.mdx")
import("./imports/posts/619cae2f03f4ff710aa3d980.mdx")
import("./imports/posts/619e002d386ebf2023ea85c3.mdx")
import("./imports/posts/619fff7c5b312d7622acda86.mdx")
I have a Post.jsx component:
import React, { useState, useRef } from "react"
import { useHistory, useParams } from "react-router-dom"
import { useQuery } from "#apollo/client"
import { GET_POST_ID } from "../../api/posts/queries"
const Post = () => {
const Post = useRef()
const history = useHistory()
const { slug } = useParams()
const [loadedPost, setLoaded] = useState(false)
const [viewer, showViewer] = useState(false)
const open = () => showViewer(true)
const { data, loading, error } = useQuery(GET_POST_ID, { variables: { slug }})
if (loading) return null
if (error) {
console.log(error)
return null
}
import(`./posts/${data._id}.mdx`).then(MDX => {
Post.current = MDX.default
setLoaded(true)
}, (err) => {
console.log(err)
})
return loadedPost ? (
<>
<div className="postContent">
<div className="markdownOverride markdown-body">
<Post.current />
</div>
</div>
</>
) : null
}
export default Post
This works well and good on my local network. However, if I attempt to access it from outside my local network, an error is thrown in the console that all the blog modules are not found. The Apollo/GraphQL portion works fine, but the actual module can't be imported.
How do I get this to work outside of localhost?
Thanks.
EDIT: The error messages are, for each post:
Uncaught (in promise) Error: Cannot find module '/imports/posts/61a000d03a1931b8819dc17e.mdx`
And when I load the actual post page:
Uncaught (in promise) TypeError: Failed to fetch
Isn't your error thrown by console.log(err) ?
import(`./posts/${data._id}.mdx`).then(MDX => {
Post.current = MDX.default
setLoaded(true)
}, (err) => {
console.log(err) // <---- here
})
This means your path isn't right for /imports/posts/61a000d03a1931b8819dc17e.mdx.
To me you can't use changing parameters when doing dynamic imports.
./posts/${data._id}.mdx, because your meteor or webpack compilation needs to treat all the data._ids avalaible in your database in order to compile and prepare the file...
This might be why it works in development mode but not in production.
You can just do dynamic imports of modules or components (already compiled), no more to me. Take a look at your output compilation bundles, and try to find where are your components...
It turns out that I needed to specify the ROOT_URL correctly when initializing Meteor. With an ngrok http tunnel on port 3000 pointing to https://some-hash.ngrok.io, I had to start Meteor with: ROOT_URL="https://some-hash.ngrok.io" meteor. When I do this, I can access it fine and everything loads from my local IP and the ngrok URL, but I can't seem to load it up from localhost (it times out).
Specifying my local or public IP did not work, I could not get it to load through any of those methods.

How to fix "ReferenceError: Blob is not defined" in NextJs?

Hi I was trying to use react-qr-reader in next js but having the problem
Server Error
ReferenceError: Blob is not defined
This error happened while generating the page. Any console logs will be displayed in the terminal window.
Source
external%20%22react-qr-reader%22 (1:0) # Object.react-qr-reader
> 1 | module.exports = require("react-qr-reader");
Call Stack
__webpack_require__
webpack\bootstrap (21:0)
How can I fix it?
This works for me:
const QrScan = dynamic(() => import('react-qr-reader'), { ssr: false })
The official docs says Server side rendering won't work for react-qr-reader. So you need to do is to avoid applying react-qr-reader in server-side. You can use the dynamic to solve the problem. You can also reference from the solution 2 of this solution to get some example code.
Great work guys, this works for me
import { useState } from "react";
import dynamic from "next/dynamic";
const QrReader = dynamic(() => import("react-qr-reader"), { ssr: false });
export default function ScanPage() {
const [state, setState] = useState("");
return (
<>
<div>{state}</div>
<QrReader delay={100}
onError={(err) => setState(err)}
onScan={(data) => setState(data)}
style={{ width: "95vw"}}
/>
</>
);}
I was having same issue on voice-recorder-react library. So after some work-arounds I was be able to solve it.
I just fixed by making the page csr (Client-Side-Rendering)
like this:
const HomePage = dynamic(() => import("./home"), { ssr: false });
And then I used the library as normal way of importing (in my home page).
import { useRecorder } from "voice-recorder-react";
So basically some of libraries need to be rendered on client-side.

How to use react-intl with jest

I'm working with React Testing Library an I need to get the translation for a text, we mocked in our setupTest file the react-intl library:
jest.mock('react-intl', () => {
const reactIntl = require.requireActual('react-intl');
const intl = reactIntl.createIntl({
locale: 'en'
});
return {
...reactIntl,
useIntl: () => intl
};
});
but I don't know how to use it in the test, could someone provide me an complete examble of a test using the library to get a translation, please?
I tried to do it in this way:
let intl = useIntl();
let i18n = {
header: intl.formatMessage({
id: 'header.myHeader',
defaultMessage: 'header.myHeader'
})
};
but there are any messages in intl from the locales.
Regards.
Don't mock it, try and directly wrap your component with IntlProvider, or even better, create a helper render where you wrap with IntlProvider and render it with that:
import {IntlProvider} from 'react-intl`;
import {render} from '#testing-library/react';
const renderWithReactIntl = (component, locale, messages) => {
return render(<IntlProvider locale={locale} messages={messages}>
{component}
</IntlProvider>
);
};
Within your test() or it() just wrap your unconnected component in renderWithReactIntl:
const { /testing library selector goes here/ }
= renderWithReactIntl(<YourComponent />, messages, locale)

Resources