I would like add a unit test to a simple react(TS) component that embeds a tableau dashboard using the Tableau Javascript API.
An error occurs when I try to run the test file, i get the following error:
When the page renders, under the <TableauReport /> an <iframe /> will be mounted which is generated by Tableau Javascript; see logic under the useEffect
Not sure if this is an issue with react-testing-library, but I've read also it could be an issue with jsdom version; version is 16.7.0
Here is the component:
import React, { useEffect, useRef } from 'react';
import { server } from '../../constants/tableau'
interface TableauProps {
options: {
hideToolbar: boolean,
};
view: string;
ticket?: string;
}
const { tableau }: any = window;
const setVizStyle = {
width: "800px",
height: "700px",
};
const TableauReport = ({ options, view }: TableauProps) => {
const url = `${server}${view}`
const tableauRef = useRef<HTMLDivElement>(null);
useEffect(() => {
new tableau.Viz(tableauRef.current, url, options)
}, []);
return (
<div style={setVizStyle} ref={tableauRef} />
);
};
export default TableauReport;
Here is the test:
describe("Tests", () => {
let windowSpy: any;
beforeEach(() => {
windowSpy = jest.spyOn(window, "window", "get");
})
afterEach(() => {
windowSpy.mockRestore();
})
const tableauOptions = {
hideToolbar: true,
}
it('should render the component', () => {
windowSpy.mockImplementation(() => {
tableau: {
Viz: jest.fn();
}
})
render(<TableauReport options={tableauOptions} view={dashboard}/>)
})
});
Initially, I had an issue with the Viz method in the test, it was not being recognized. I had to add a spyOn to then window object and mock the implementation.
Is that the right approach to test the window.tableau?
I was wondering is there a proper way for testing an embedded tableau dashboard using react-testing-library.
Here is the tutorial I followed on how to embed a tableau dashboard using react, link
I'm new to tableau, so any help will be highly appreciated, thanks :)
Related
I'm trying to write a unit test for one of my React components written in TS:
import React, { useContext } from 'react';
import Lottie from 'lottie-react-web';
import { ConfigContext } from '../ConfigProvider';
import type { UIKitFC } from '../../types/react-extensions';
// interfaces
export interface LoadingOverlayProps {
size: 'large' | 'medium' | 'small';
testId?: string;
}
interface LoaderProps {
size: 'large' | 'medium' | 'small';
}
const G3Loader: React.FC<LoaderProps> = ({ size }) => {
const options = { animationData };
const pxSize =
size === 'small' ? '100px' : size === 'medium' ? '200px' : '300px';
const height = pxSize,
width = pxSize;
return (
<div className="loader-container">
<Lottie options={options} height={height} width={width} />
<div className="loader__loading-txt">
<div>
<h4>Loading...</h4>
</div>
</div>
</div>
);
};
/**
* Description of Loading Overlay component
*/
export const LoadingOverlay: UIKitFC<LoadingOverlayProps> = (props) => {
const { testId } = props;
const { namespace } = useContext(ConfigContext);
const { baseClassName } = LoadingOverlay.constants;
const componentClassName = `${namespace}-${baseClassName}`;
const componentTestId = testId || `${namespace}-${baseClassName}`;
return (
<div id={componentTestId} className={componentClassName}>
<G3Loader size={props.size} />
</div>
);
};
LoadingOverlay.constants = {
baseClassName: 'loadingOverlay',
};
LoadingOverlay.defaultProps = {
testId: 'loadingOverlay',
};
export default LoadingOverlay;
The component uses an imported module "Lottie" for some animation, but I'm not interested in testing it, I just want to test my component and its props.
The problem is, when I run my unit test, I get an error:
Error: Not implemented: HTMLCanvasElement.prototype.getContext (without installing the canvas npm package)
After some research, I've concluded that the error is caused by the Lottie import so I would like to mock it for the purpose of my test. I'm using Mocha and Sinon's stub functionality to try and mock the library import, but the same error persists, making me feel like I'm not stubbing the module out correctly. Here's my latest attempt at a unit test:
import React from 'react';
import * as Lottie from 'lottie-react-web';
import { render } from '#testing-library/react';
import { expect } from 'chai';
import * as sinon from 'sinon';
import LoadingOverlay from '../src/components/LoadingOverlay';
const TEST_ID = 'the-test-id';
const FakeLottie: React.FC = (props) => {
return <div>{props}</div>;
};
describe('Loading Overlay', () => {
// beforeEach(function () {
// sinon.stub(Lottie, 'default').callsFake((props) => FakeLottie(props));
// });
console.log('11111');
it('should have a test ID', () => {
sinon.stub(Lottie, 'default').callsFake((props) => FakeLottie(props));
console.log(Lottie);
const { getByTestId, debug } = render(
<LoadingOverlay testId={TEST_ID} size="small" />
);
debug();
expect(getByTestId(TEST_ID)).to.not.equal(null);
});
});
I'm not really sure what else to try, unit tests are not my forte... If anyone can help, that would be great.
I answered my own questions... Posting in case somebody else runs into the same issue.
The error was complaining about HTMLCanvasElement. It turns out the component I was trying to stub out was using the Canvas library itself which wasn't required when running in the browser, but since I was building a test, I just added the Canvas library to my package and the issue was solved. Full code below:
import React from 'react';
import { render, cleanup } from '#testing-library/react';
import { expect, assert } from 'chai';
import * as lottie from 'lottie-react-web';
import { createSandbox } from 'sinon';
import LoadingOverlay from '../src/components/LoadingOverlay';
// test ID
const TEST_ID = 'the-test-id';
// mocks
const sandbox = createSandbox();
const MockLottie = () => 'Mock Lottie';
describe('Loading Overlay', () => {
beforeEach(() => {
sandbox.stub(lottie, 'default').callsFake(MockLottie);
});
afterEach(() => {
sandbox.restore();
cleanup();
});
it('should have test ID', () => {
const { getByTestId } = render(
<LoadingOverlay testId={TEST_ID} size="medium" />
);
expect(getByTestId(TEST_ID)).to.not.equal(null);
});
});
When ever I run the test it fails. I don't know what mistake I am making.
How can I test those if statements and the child component. I am using jest and enzyme for react tests.
This is my test file:
import React from "react";
import { shallow } from "enzyme";
import LaunchContainer from "./index";
import Launch from "./Launch";
describe("render LaunchContainer component", () => {
let container: any;
beforeEach(() => { container = shallow(<LaunchContainer setid={()=>{}} setsuccess={()=>{}} />) });
it("should render LaunchContainer component", () => {
expect(container.containsMatchingElement(<Launch setsuccess={()=>{}} setid={()=>{}} data={{}}/>)).toEqual(true);
});
})
The parent component for which the test is used:
import React from "react";
import { useLaunchesQuery } from "../../generated/graphql";
import Launch from './Launch';
interface Props {
setid: any;
setsuccess: any;
}
const LaunchContainer: React.FC<Props> = ({setid, setsuccess}) => {
const {data, loading, error} = useLaunchesQuery();
if (loading) {
return <div>loading...</div>
}
if (error || !data) {
return <div>error</div>
}
return <Launch setid={setid} data={data} setsuccess={setsuccess} />
}
export default LaunchContainer;
The child component to be added in test:
import React from "react";
import { LaunchesQuery } from "../../generated/graphql";
import './style.css';
interface Props {
data: LaunchesQuery;
setid: any;
setsuccess: any;
}
const Launch: React.FC<Props> = ({setid, data, setsuccess}) => {
return (
<div className="launches">
<h3>All Space X Launches</h3>
<ol className="LaunchesOL">
{!!data.launches && data.launches.map((launch, i) => !!launch &&
<li key={i} className="LaunchesItem" onClick={() => {
setid(launch.flight_number?.toString())
setsuccess(JSON.stringify(launch.launch_success))
}}>
{launch.mission_name} - {launch.launch_year} (<span className={launch.launch_success? "LaunchDetailsSuccess": launch.launch_success===null? "": "LaunchDetailsFailed"}>{JSON.stringify(launch.launch_success)}</span>)
</li>
)}
</ol>
</div>
);
}
export default Launch;
To test those if statements you should mock your useLaunchesQuery to return the loading, error and data values that hit your if statements. You can use mockImplementationOnce or mockReturnValueOnce. For example you could write
import { useLaunchesQuery } from "../../generated/graphql";
/**
* You mock your entire file /generated/graphql so it returns
* an object containing the mock of useLaunchesQuery.
* Note that every members in this file and not specified in this mock
* will not be usable.
*/
jest.mock('../../generated/graphql', () => ({
useLaunchesQuery: jest.fn(() => ({
loading: false,
error: false,
data: [/**Whatever your mocked data is like */]
}))
}))
const setid = jest.fn();
const setsuccess = jest.fn();
/**
* A good practice is to make a setup function that returns
* your tested component so so you can call it in every test
*/
function setup(props?: any) { // type of your component's props
// You pass mock functions declared in the upper scope so you can
// access it later to assert that they have been called the way they should
return <LaunchContainer setid={setid} setsuccess={setsuccess} {...props} />
// Spread the props passed in parameters so you overide with whatever you want
}
describe("render LaunchContainer component", () => {
it('should show loading indicator', () => {
/**
* Here I use mockReturnValueOnce so useLaunchesQuery is mocked to return
* this value only for the next call of it
*/
(useLaunchesQuery as jest.Mock).mockReturnValueOnce({ loading: true, error: false, data: [] })
/** Note that shallow won't render any child of LaunchContainer other than basic JSX tags (div, span, etc)
* So you better use mount instead
*/
const container = mount(setup()); // here i could pass a value for setid or setsuccess
// Put an id on your loading indicator first
expect(container.find('#loading-indicator').exists()).toBeTruthy()
/** The logic for the other if statement remains the same */
})
}
I am fairly new to testing with Gatsby and Jest and I am encountering issues with my current setup.
I am trying to build a site with Gatsby and I would like for CI purposes to run unit tests with Jest and react-testing-library.
I initially was able to run a simple test:
describe('<Header>', () => {
describe('mounts', () => {
test('component mounts', () => {
const { container } = render(
<Header />
);
expect(container).toBeInTheDocument();
});
});
describe('click', () => {
const { container } = render(<Header />);
expect(screen.getByText('Light mode ☀')).toBeInTheDocument();
fireEvent(
getByText(container, 'Light mode ☀'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
expect(screen.getByText('Dark mode ☾')).toBeInTheDocument();
});
where my render is a customized one:
const AllTheProviders = ({ children }) => {
const [dark, setDark] = useState(true);
return (
<ThemeContext.Provider
value={{
dark,
toggleDark: () => setDark(!dark),
}}
>
{children}
</ThemeContext.Provider>
);
};
const customRender = (ui, options) =>
render(ui, { wrapper: AllTheProviders, ...options });
// re-export everything
export * from '#testing-library/react';
// override render method
export { customRender as render };
and the Header a simple component that consumes the context and toggles the theme from dark to light.
I since then added gatsby-theme-i18n to handle i18n and tweaked my jest config to add:
transformIgnorePatterns: ['node_modules/(?!(gatsby-theme-i18n)/)']
The Header is now wrapped in a Layout component that provides the locale and location (with the Help for Location and #reach/router that comes with Gatsby) so I can have a language toggle in the header
In the Layout:
import { Location } from '#reach/router';
[...]
<Location>
{(location) => (
<Header
siteTitle={data.site.siteMetadata.title}
location={location}
locale={locale}
/>
)}
</Location>
The Header makes use of LocalizedLink from gatsby-theme-i18n` to prefix the right locale
<li key={langKey}>
<LocalizedLink key={langKey} to={to} language={langKey}>
{langKey}
</LocalizedLink>
</li>
I also changed the test to:
describe('mounts', () => {
test('component mounts', () => {
const { container } = render(
<Location>
{(location) => (
<Header siteTitle="site title" location={location} locale="en" />
)}
</Location>,
);
expect(container).toBeInTheDocument();
});
});
However, I get an error when trying to run the test:
TypeError: Cannot read property 'themeI18N' of undefined
21 |
22 | const customRender = (ui, options) =>
> 23 | render(ui, { wrapper: AllTheProviders, ...options });
| ^
specifically coming from at useLocalization (node_modules/gatsby-theme-i18n/src/hooks/use-localization.js:9:7)
where the Gatsby theme provides the i18n info through a graphQL query:
const {
themeI18N: { defaultLang, config },
} = useStaticQuery(graphql`
{
themeI18N {
defaultLang
config {
code
hrefLang
dateFormat
langDir
localName
name
}
}
}
`)
I understand that Jest does not 'understand' what to do with the i18n passed through the gatsby-theme-i18n via the Layout and if I switch my LocalizedLink to a normal Gatsby Link, the test passes (and I can console.log the container to see it's indeed a React element with the right info).
I tried a few things I saw online like using const { useLocalization } = require('gatsby-theme-i18n'); but to no avail.
Any idea how to handle this?
Should I mock the utils from gatsby-theme-i18n like other elements from Gatsby are mocked (Link etc.) following the docs?
EDIT
Thanks to the author of the library, LekoArts, and a friend a mine, I managed to make it work :)
If somebody has the same issue, you need to mock the response from the useStaticQuery of the useLocalization hook.
I have a mock file that does it:
// Libs
const React = require('react');
const gatsby = jest.requireActual('gatsby');
// Utils
const siteMetaData = require('../src/utils/siteMetaData');
module.exports = {
...gatsby,
graphql: jest.fn(),
Link: jest.fn().mockImplementation(
// these props are invalid for an `a` tag
({
activeClassName,
activeStyle,
getProps,
innerRef,
partiallyActive,
ref,
replace,
to,
...rest
}) =>
React.createElement('a', {
...rest,
href: to,
}),
),
StaticQuery: jest.fn(),
useStaticQuery: jest.fn().mockImplementation(() => ({
site: {
siteMetadata: {
...siteMetaData,
},
},
themeI18N: {
defaultLang: 'en',
config: {
code: 'en',
hrefLang: 'en-CA',
name: 'English',
localName: 'English',
langDir: 'ltr',
dateFormat: 'MM/DD/YYYY',
},
},
})),
};
and my test:
// Libs
import React from 'react';
// Utils
import { Location } from '#reach/router';
import { render, fireEvent, getByText, screen } from './utils/test-utils';
// Component
import Header from '../header';
describe('<Header>', () => {
describe('mounts', () => {
test('component mounts', () => {
const { container } = render(
<Location>
{(location) => (
<Header siteTitle="site title" location={location} locale="en" />
)}
</Location>,
);
expect(container).toBeInTheDocument();
});
});
describe('click', () => {
test('toggle language', () => {
const { container } = render(
<Location>
{(location) => (
<Header siteTitle="site title" location={location} locale="en" />
)}
</Location>,
);
expect(screen.getByText('Light mode ☀')).toBeInTheDocument();
fireEvent(
getByText(container, 'Light mode ☀'),
new MouseEvent('click', {
bubbles: true,
cancelable: true,
}),
);
expect(screen.getByText('Dark mode ☾')).toBeInTheDocument();
});
});
});
You can find more information here
I want to add functionality that will collect custom events in redux in entire react app.
What I want to achieve is to place all event functions it in one place and only use this functions in components in my app when I want to trigger some event.
interface IEventsLoggerContext {
[key: string]: (val?: any) => void
}
export const EventsLoggerContext = createContext<IEventsLoggerContext>({})
class EventsLogger extends Component<{}, any> {
constructor (props: Readonly<{}>) {
super(props)
}
// event methods like below
pageLoaded = () => {
// here will be saving the event to redux store
console.log('page loaded')
}
targetClick = (e: SyntheticEvent) => {
// here will be saving the event to redux store
console.log('target click', e.target)
}
// ...
render () {
return (
<EventsLoggerContext.Provider
value={{
pageLoaded: this.pageLoaded,
targetClick: this.targetClick
}}
>
{this.props.children}
</EventsLoggerContext.Provider>
)
}
}
export default EventsLogger
I want to make all event log actions available in app so I wrapped all into my event provider:
<EventsLogger>
...
</EventsLogger>
And using in component like this:
const MainApp: React.FC = () => {
const { pageLoaded } = useContext(EventsLoggerContext)
useEffect(() => {
pageLoaded()
}, [pageLoaded])
return (
<div>Main App page</div>
)
}
Is this correct way to do this or is there maybe better approach to get functionality like this?
Using React Context is a clever way to solve this although it will require more code when adding more events compared to simply using the browser native window.dispatchEvent() function.
// SomeComponent.tsx
export const SomeComponent : FC = props => {
useEffect(() => {
const pageLoadEvent = new CustomEvent(
"pageLoaded",
{
detail : {
at: Date.now()
}
}
);
window.dispatchEvent(pageLoadEvent);
}, []):
// ...
}
// SomeOtherComponent.tsx
export const SomeOtherComponent : FC = props => {
useEffect(() => {
window.addEventListener("pageLoaded", onPageLoaded);
return () => window.removeEventListener("pageLoaded", onPageLoaded);
}, []);
function onPageLoaded(e: CustomEvent) {
// Do whatever you want here :)
}
// ...
}
I am using react-test-renderer with Jest to test react components. But if I test a react-mui modal dialog like this:
describe('Dashboard', function () {
let dashboard;
beforeEach(async () => {
testRenderer = TestRenderer.create(<MemoryRouter><Route component={Dashboard} /></MemoryRouter>);
dashboard = testRenderer.root.findByType(Dashboard);
await waitForExpect(() => expect(dashboard.instance.state.hasLoaded).toBeTruthy());
});
it('opens dialog on clicking the new class', async () => {
const button = testRenderer.root.findByType(Button);
expect(dashboard.instance.state.showDialog).toBeFalsy();
button.props.onClick();
expect(dashboard.instance.state.showDialog).toBeTruthy();
});
});
But, then I get an error:
Error: Failed: "Error: Uncaught 'Warning: An invalid container has
been provided. This may indicate that another renderer is being used
in addition to the test renderer. (For example, ReactDOM.createPortal
inside of a ReactTestRenderer tree.) This is not supported.%s'
How should I test then react portals to make this test work?
Try putting this in your tests:
beforeAll(() => {
ReactDOM.createPortal = jest.fn((element, node) => {
return element
})
});
Based on Oliver's answer, but for TypeScript users:
describe("Tests", () => {
const oldCreatePortal = ReactDOM.createPortal;
beforeAll(() => {
ReactDOM.createPortal = (node: ReactNode): ReactPortal =>
node as ReactPortal;
});
afterAll(() => {
ReactDOM.createPortal = oldCreatePortal;
});
});
For me, the existing solutions don't address the root cause.
I needed to add jest mocks for all the sub-components in the component I was testing.
For example, consider this JSX that I want to test:
import { CustomTextInput } from 'components/CustomTextInput';
import { CustomButton } from 'components/CustomButton';
return (
<>
<CustomTextInput />
<CustomButton />
</>
)
I need to add mocks for CustomTextInput and CustomButton in my test file like this:
jest.mock(
'components/CustomTextInput',
() => ({ default: 'mock-CustomTextInput' }),
);
jest.mock(
'components/CustomButton',
() => ({ default: 'mock-CustomButton' }),
);