How to inject Material-UI stylesheets into a jest/react-testing-library test? - jestjs

It seems that if you don't inject Material-UI stylesheets into a jest/react-testing-library test then jsdom will fail to get the correct styles from your components (e.g. running getComputedStyle(component) will return the incorrect styles for the component).
How you properly setup a jest/react-testing-library test so that the styles are correctly injected into the test? I've already wrapped the components in a theme provider, which works fine.

As a workaround reinserting the whole head (or the element where JSS styles are injected) before assertion seems to apply styles correctly with both getComputedStyle() and react testing library's toHaveStyle():
import React from "react";
import "#testing-library/jest-dom/extend-expect";
import { render } from "#testing-library/react";
test("test my styles", () => {
const { getByTestId } = render(
<div data-testid="wrapper">
<MyButtonStyledWithJSS/>
</div>
);
const button = getByTestId("wrapper").firstChild;
document.head.innerHTML = document.head.innerHTML;
expect(button).toHaveStyle(`border-radius: 4px;`);
});
This will still fail though when you're using dynamic styles, like:
myButton: {
padding: props => props.spacing,
...
}
That's because JSS uses CSSStyleSheet.insertRule method to inject these styles, and it won't appear as a style node in the head. One solution to this issue is to hook into the browser's insertRule method and add incoming rules to the head as style tags. To extract all this into a function:
function mockStyleInjection() {
const defaultInsertRule = window.CSSStyleSheet.prototype.insertRule;
window.CSSStyleSheet.prototype.insertRule = function (rule, index) {
const styleElement = document.createElement("style");
const textNode = document.createTextNode(rule);
styleElement.appendChild(textNode);
document.head.appendChild(styleElement);
return defaultInsertRule.bind(this)(rule, index);
};
// cleanup function, which reinserts the head and cleans up method overwrite
return function applyJSSRules() {
window.CSSStyleSheet.prototype.insertRule = defaultInsertRule;
document.head.innerHTML = document.head.innerHTML;
};
}
Example usage of this function in our previous test:
import React from "react";
import "#testing-library/jest-dom/extend-expect";
import { render } from "#testing-library/react";
test("test my styles", () => {
const applyJSSRules = mockStyleInjection();
const { getByTestId } = render(
<div data-testid="wrapper">
<MyButtonStyledWithJSS spacing="8px"/>
</div>
);
const button = getByTestId("wrapper").firstChild;
applyJSSRules();
expect(button).toHaveStyle("border-radius: 4px;");
expect(button).toHaveStyle("padding: 8px;");
});

This ultimately seems like an issue with JSS and various browser implementations like jsdom and and Blink (at least in Chrome). You can see it in Chrome when trying to modify/enable/disable these style rules (you can't).
The behavior appears to be a result of the JSS library using the CSSOM insertRule API. There's a stylesheet generated in the DOM for the styles we expect in our component, but the tag is empty - it's just used to link the shadow CSS back to the DOM. The styles are never written to the inline stylesheet in the DOM, and as a result, the getComputedStyle method does not return the expected results.
There's an open issue to address this behavior and make development easier.
I switched my custom components to styled-components, which does not have some of these idiosyncrasies.
Material-UI is planning on transitioning soon as well.

You could add this to a custom render function. After rendering, the function pulls the styles out of cssom and puts them into a style tag. Here is an implementation:
let customRender = (ui, options) => {
let renderResult = render(ui, options);
let styleElement = document.createElement("style");
let styleText = "";
for (let styleSheet of document.styleSheets) {
for (let rule of styleSheet.cssRules) {
styleText += rule.cssText + "\n";
}
}
styleElement.textContent = styleText.slice(0, -1);
document.head.appendChild(styleElement);
// remove old style elements
let emptyStyleElements = document.head.querySelectorAll('style[data-jss=""]');
for (let element of emptyStyleElements) {
element.remove();
}
return renderResult;
}

I can't speak specifically to Material-UI stylesheets, but you can inject a stylesheet into rendered component:
import {render} from '#testing-library/react';
import fs from 'fs';
import path from 'path';
const stylesheetFile = fs.reactFileSync(path.resolve(__dirname, '../path-to-stylesheet'), 'utf-8');
const styleTag = document.createElement('style');
styleTag.type = 'text/css';
styleTag.innerHTML = stylesheetFile;
const rendered = render(<MyComponent>);
rendered.append(style);
You don't necessarily have to read from a file, you can use whatever text you want.

Related

how to render svg icon in nuxt 3

I am using nuxt 3 and Compositions API.
I get such a nesting, how to get rid of the extra svg tag?
I would also like to receive svg attributes whenever possible and change, for example, fill
template
<template>
<div>
<component :is="render"></component>
</div>
</template>
Script
import { h } from "vue";
const { data, pending, error, refresh } = await useFetch(svgURL);
const getDataVal = data.value
const SvgToRaw = await getDataVal.text();
const render = () => {
return h("svg", {
class: "bar",
innerHTML: SvgToRaw,
});
};
Chrome Dev Tools
I tried to create a virtual DOM tree and get an HTML element from there, not text, but I think this is a bad solution
import hv from "virtual-dom/h";
import diff from "virtual-dom/diff";
import patch from "virtual-dom/patch";
import createElement from "virtual-dom/create-element";
const betaRender = hv("span", { innerHTML: svgString });
var rootNode = createElement(betaRender);
var patches = diff(rootNode);
return patches[0].vNode.innerHTML
SSR support is important to me so I can't use standard tools

How can I render dynamically imported react components?

let's consider a list of modules imported arbitrarily as so :
/**
* modules is a list of absolute paths to modules exporting react components
*/
const getAllComponents = async(modules) => {
const components = [];
modules.forEach((moduleName) => {
try {
const module = await import(moduleName);
components.push(module.default);
}catch(err) {
console.warn(err.message)
}
})
return components;
}
and a parent react component in project and a random component exported from a disk based module:
// my-component.js
function MyComponent({moduleNames}) {
const [components, setComponents] = useState([]);
useEffect(() => getAllComponents.then(setComponents), []);
// rendering a random component assuming it exists
const RenderedComponent = components[0];
return (
<div>
{/* failling here: */}
<RenderedComponent />
</div>
)
}
// a-random-component.js (disk based module whose path is in moduleNames in above component props)
function RandomComponent() {
return (<div>propless component</div>)
}
I get the following error when compiling:
Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: object.
Check the render method of `MyComponent`.
Is there a way to render arbitrary component in the dom without having to use the static import statement ?

React hook error when using Material-UI styles

I've been teaching myself React and node.js in my free time, and am hitting a wall with why my use of material-ui's makeStyles is breaking react hook rules.
I've been replicating the code from the material-ui website but even this appears to break the rules. The key difference between their code examples and mine is that my code has multiple exports. Included code below shows just one of these functions - they would all be very similar in function.
The expected behaviour is that the colour of the Icon changes to a non-OOTB color provided by material-UI.
Instead I get Invalid hook call. Hooks can only be called inside of the body of a function component. I believe I'm doing this at the top level and not inside of a condition/comparison statement. A bit lost!
Any guidance on where I'm going wrong would be amazing.
Thanks!
// ItemHelper.js
import React from 'react';
import { makeStyles } from '#material-ui/core/styles';
import AssignmentIcon from '#material-ui/icons/Assignment';
import BugIcon from '#material-ui/icons/BugReport';
import BookIcon from '#material-ui/icons/Book';
import Help from '#material-ui/icons/Help';
const useStyles = makeStyles(theme => ({
successIcon: {
color: 'green'
},
errorIcon: {
color: 'red'
}
}));
export function GetIconForItemType(itemTypeID) {
const classes = useStyles();
var data = null;
switch (itemTypeID) {
case 1:
data = <AssignmentIcon className={classes.successIcon} />;
break;
case 2:
data = <BugIcon color="error" />;
break;
case 3:
data = <BookIcon color="primary" />;
break;
default:
data = <Help />;
break;
}
return data;
}

How to mount styles inside shadow root using cssinjs/jss

I'm trying to use https://material-ui.com/ components inside shadow dom, and need a way to inject those styles inside shadow dom. by default material-ui, which uses jss under the hood injects styles in the head of the page.
Is that even possible? Can anyone come with an example?
This is what my web component looks like, it is a web component that renders a react app that contains material-ui styles.
import * as React from 'react';
import { render } from 'react-dom';
import { StylesProvider, jssPreset } from '#material-ui/styles';
import { create } from 'jss';
import { App } from '#myApp/core';
class MyWebComponent extends HTMLElement {
connectedCallback() {
const shadowRoot = this.attachShadow({ mode: 'open' });
const mountPoint = document.createElement('span');
const reactRoot = shadowRoot.appendChild(mountPoint);
const jss = create({
...jssPreset(),
insertionPoint: reactRoot
});
render(
<StylesProvider jss={jss}>
<App />
</StylesProvider>,
mountPoint
);
}
}
customElements.define('my-web-commponent', MyWebComponent);
Setting the insertionPoint on jss to the actual react root inside the shadow root will tell jss to insert those styles inside that shadow root.
Using https://github.com/Wildhoney/ReactShadow to create shadow dom (you could also do it by hand as shown in previous answer), I created a small WrapperComponent that encapsulates the logic.
import root from 'react-shadow';
import {jssPreset, StylesProvider} from "#material-ui/styles";
import {create} from 'jss';
import React, {useState} from "react"
const WrappedJssComponent = ({children}) => {
const [jss, setJss] = useState(null);
function setRefAndCreateJss(headRef) {
if (headRef && !jss) {
const createdJssWithRef = create({...jssPreset(), insertionPoint: headRef})
setJss(createdJssWithRef)
}
}
return (
<root.div>
<head>
<style ref={setRefAndCreateJss}></style>
</head>
{jss &&
<StylesProvider jss={jss}>
{children}
</StylesProvider>
}
</root.div>
)
}
export default WrappedJssComponent
Then you just need to Wrap your app, or the part of your app you want to shadow inside <WrappedJssComponenent><YourComponent></YourComponent></WrappedJssComponenent>.
Be careful, some of the material-UI component won't work as usual (I had some trouble with
ClickAwayListener, maybe because it uses the parent dom, did not investigate more than that to be honest.
Popper, and everything that will try to use document.body as container will not have access to jss defined in shadow node. You should give an element inside the shadow dom as container.
There is also a whole page in the docs now (MaterialUI 5) that covers how to integrate MUI with a shadow-dom. You also might have to set Portal defaults not to target the dom. https://mui.com/material-ui/guides/shadow-dom/
When using #material-ui/core/CssBaseline with MUI, also emotion styles are being used. In order to support both legacy jss and emotion you can extend the accepted answer above with a CacheProvider like this:
import ReactDOM from 'react-dom/client'
import App from './App'
import createCache from '#emotion/cache'
import { CacheProvider } from '#emotion/react';
import { StylesProvider, jssPreset } from '#material-ui/styles';
import { create } from 'jss';
class ReportComponent extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
const mountPoint = document.createElement('div');
const emotionPoint = this.shadowRoot!.appendChild(document.createElement('div'));
const emotionCache = createCache({
key: 'report-component',
container: emotionPoint
});
const reactRoot = this.shadowRoot!.appendChild(mountPoint);
const root = ReactDOM.createRoot(reactRoot);
const jss = create({
...jssPreset(),
insertionPoint: reactRoot
});
root.render(
<StylesProvider jss={jss}>
<CacheProvider value={emotionCache}>
<App />
</CacheProvider>
</StylesProvider>
);
}
}
customElements.define('report-component', ReportComponent);

Using Fragment to insert HTML rendered on the back end via dangerouslySetInnerHTML

I used to compile and insert JSX components via
<div key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />
which wrapped my HTML into a <div>:
<div>my html from the HTML object</div>
Now react > 16.2.0 has support for Fragments and I wonder if I can use that somehow to avoid wrapping my HTML in a <div> each time I get data from the back end.
Running
<Fragment key={ ID } dangerouslySetInnerHTML={ { __html: HTML } } />
will throw a warning
Warning: Invalid prop `dangerouslySetInnerHTML` supplied to `React.Fragment`. React.Fragment can only have `key` and `children` props.
in React.Fragment
Is this supported yet at all? Is there another way to solve this?
Update
Created an issue in the react repo for it if you want to upvote it.
Short Answer
Not possible:
key is the only attribute that can be passed to Fragment. In the
future, we may add support for additional attributes, such as event
handlers.
https://reactjs.org/docs/fragments.html
You may want to chime in and suggest this as a future addition.
https://github.com/facebook/react/issues
In the Meantime
You may want to consider using an HTML parsing library like:
https://github.com/remarkablemark/html-react-parser
Check out this example to see how it will accomplish your goal:
http://remarkablemark.org/blog/2016/10/07/dangerously-set-innerhtml-alternative/
In Short
You'll be able to do this:
<>
{require('html-react-parser')(
'<em>foo</em>'
)}
</>
Update December 2020
This issue (also mentioned by OP) was closed on Oct 2, 2019. - However, stemming from the original issue, it seems a RawHTML component has entered the RFC process but has not reached production, and has no set timeline for when a working solution may be available.
That being said, I would now like to allude to a solution I currently use to get around this issue.
In my case, dangerouslySetInnerHTML was utilized to render plain HTML for a user to download; it was not ideal to have additional wrapper tags included in the output.
After reading around the web and StackOverflow, it seemed most solutions mentioned using an external library like html-react-parser.
For this use-case, html-react-parser would not suffice because it converts HTML strings to React element(s). Meaning, it would strip all HTML that wasn't standard JSX.
Solution:
The code below is the no library solution I opted to use:
//HTML that will be set using dangerouslySetInnerHTML
const html = `<div>This is a div</div>`
The wrapper div within the RawHtml component is purposely named "unwanteddiv".
//Component that will return our dangerouslySetInnerHTML
//Note that we are using "unwanteddiv" as a wrapper
const RawHtml = () => {
return (
<unwanteddiv key={[]}
dangerouslySetInnerHTML={{
__html: html,
}}
/>
);
};
For the purpose of this example, we will use renderToStaticMarkup.
const staticHtml = ReactDomServer.renderToStaticMarkup(
<RawHtml/>
);
The ParseStaticHtml function is where the magic happens, here you will see why we named the wrapper div "unwanteddiv".
//The ParseStaticHtml function will check the staticHtml
//If the staticHtml type is 'string'
//We will remove "<unwanteddiv/>" leaving us with only the desired output
const ParseStaticHtml = (html) => {
if (typeof html === 'string') {
return html.replace(/<unwanteddiv>/g, '').replace(/<\/unwanteddiv>/g, '');
} else {
return html;
}
};
Now, if we pass the staticHtml through the ParseStaticHtml function you will see the desired output without the additional wrapper div:
console.log(ParseStaticHtml(staticHtml));
Additionally, I have created a codesandbox example that shows this in action.
Notice, the console log will throw a warning: "The tag <unwanteddiv> is unrecognized in this browser..." - However, this is fine because we intentionally gave it a unique name so we can easily differentiate and target the wrapper with our replace method and essentially remove it before output.
Besides, receiving a mild scolding from a code linter is not as bad as adding more dependencies for something that should be more simply implemented.
i found a workaround
by using react's ref
import React, { FC, useEffect, useRef } from 'react'
interface RawHtmlProps {
html: string
}
const RawHtml: FC<RawHtmlProps> = ({ html }) => {
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
if (!ref.current) return
// make a js fragment element
const fragment = document.createDocumentFragment()
// move every child from our div to new fragment
while (ref.current.childNodes[0]) {
fragment.appendChild(ref.current.childNodes[0])
}
// and after all replace the div with fragment
ref.current.replaceWith(fragment)
}, [ref])
return <div ref={ref} dangerouslySetInnerHTML={{ __html: html }}></div>
}
export { RawHtml }
Here's a solution that works for <td> elements only:
type DangerousHtml = {__html:string}
function isHtml(x: any): x is DangerousHtml {
if(!x) return false;
if(typeof x !== 'object') return false;
const keys = Object.keys(x)
if(keys.length !== 1) return false;
return keys[0] === '__html'
}
const DangerousTD = forwardRef<HTMLTableCellElement,Override<React.ComponentPropsWithoutRef<'td'>,{children: ReactNode|DangerousHtml}>>(({children,...props}, ref) => {
if(isHtml(children)) {
return <td dangerouslySetInnerHTML={children} {...props} ref={ref}/>
}
return <td {...props} ref={ref}>{children}</td>
})
With a bit of work you can make this more generic, but that should give the general idea.
Usage:
<DangerousTD>{{__html: "<span>foo</span>"}}</DangerousTD>

Resources