I have component within I containment other components. In browser It looks like good but during unit testing I'm getting below an error. I don't know why. I have try render with .map() method but it didn't work. Thanks for help!
Error: Uncaught [Invariant Violation: Objects are not valid as a React child (found
: object with keys {$$typeof, render, attrs, componentStyle, displayName, foldedComponentIds, styledComponentId, target, withComponent, warnTooManyClasses, toString}). If you meant to render a collection of children, use an array instead.
AppBarTop.js
const AppBarTopWrapper = styled.div`
background: ${props => props.theme.color.backgroundSecondary};
border-bottom: 1px solid ${props => props.theme.color.line};
padding: 2rem 2rem 0rem 2rem;
color: ${props => props.theme.color.secondaryText};
`
const AppBarTop = ({ children }) => (
<AppBarTopWrapper>{children}</AppBarTopWrapper>
)
AppBarTop.propTypes = {
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}
export default AppBarTop
Test
const Head = styled.div
function renderTitle(props) {
return renderWithRouter(
<AppBarTop>
<Head>
<UpwardButton level={2} />
<Title level={1} {...props.message} />
</Head>
</AppBarTop>,
{
withStore: true,
},
)
}
const testFormattedMessage = text => ({
id: 'test',
defaultMessage: text,
})
describe('<Heading />', () => {
it('Expect to not log errors in console', () => {
const spy = jest.spyOn(global.console, 'error')
console.log(renderTitle({ message: testFormattedMessage('test title') }))
renderTitle({ message: testFormattedMessage('test title') })
expect(spy).not.toHaveBeenCalled()
})It looks like your post is mostly code; please add some more details.
})
styled.div returns a template function rather than a component. Hence in your code
const Head = styled.div
function renderTitle(props) {
return renderWithRouter(
<AppBarTop>
<Head>
<UpwardButton level={2} />
<Title level={1} {...props.message} />
</Head>
</AppBarTop>,
{
withStore: true,
},
)
}
Head is not a component (but a regular function), i.e. it's not a valid React child. Replacing <Head> with <div>, or invoking template function with empty string: const Head = styled.div`` would be sufficient.
Related
I am testing my Text component with jest and jest-styled-components and toHaveStyleRule breaks my test with this error
"Value mismatch for property 'font-size'"
Expected
"font-size: 42px"
Received:
"font-size: 1.6rem"
This is my component.
const StyledText = styled.h1`
font-size: '1.6rem';
&.headline {
color: red;
&.headline-1 {
font-size: '42px';
}
}
`
const Text = ({h1}) => {
const className = h1 ? 'headline headline-1' : ' ';
return (
<StyledText className={className}>h1</StyledText>
)
}
And the test suit.
test("It should render correct heading when h1 is passed", () => {
const { getByRole } = render(<Text h1>h1</Text>);
const h1Styled = getByRole('heading', { level: 1 });
expect(h1Styled).toHaveClass('headline headline-1');
const baseHeadlineStyles = {
color: red,
};
expect(h1Styled).toHaveStyle(baseHeadlineStyles);
expect(h1Styled).toHaveStyleRule('font-size', '42px')
}
It seems like it only considers direct css rules on styled components.
ClassNames are applied properly, otherwise the below assertion would fail.
expect(h1Styled).toHaveClass('headline headline-1');
Interesting thing is that it works well with
expect(elem).toHaveStyle('font-size: 42px');
I need to use toHaveStyleRule as it also accepts media queries.
The WebComponents do work fine with this custom script but I have problems to.
I want to be able to add event listener from outside to listen to the custom Vue events fired from within the component (in this example on click on the WC). Thus I created an event proxy but for some reason the listener is never triggered. Obviously I made a mistake somewhere. So I hope that someone can point me to the right solution.
I made a CodeSandbox for you to take a look and play around with the code.
(as CodeSandbox has some issues you'll need to click on the reload button on the right side of the layout in order to make it work)
//main.js
import { createApp } from "vue";
import App from "./App.vue";
import WebComponentService from "./services/wc";
WebComponentService.init();
createApp(App).mount("#app");
Vue Component used as source file for Web Component
//src/wcs/MyComp.vue
<template>
<div class="m" #click="click">
Hello My Comp {{ name }}
<div v-for="(i, index) in values" :key="index">ID: {{ i.title }}</div>
</div>
</template>
<script>
export default {
__useShadowDom: false,
emits: ["my-click"],
// emits: ["my-click", "myclick"],
props: {
name: {
default: "Test",
},
values: {
type: Object,
default: () => {
return [{ title: "A" }, { title: "B" }];
},
},
},
methods: {
click() {
// emit the event
this.$emit("my-click");
console.log("EMIT");
},
},
};
</script>
<style lang="less" scoped>
.m {
border: 5px solid red;
margin: 10px;
padding: 10px;
border-radius: 5px;
}
</style>
Index.html where I test the web component and added an event listener
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" />
<link rel="icon" href="<%= BASE_URL %>favicon.ico" />
<title><%= htmlWebpackPlugin.options.title %></title>
</head>
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
<h3>WC Test here</h3>
<div class="component-canvas">
<!-- web component testing -->
<xvue-my-comp
id="vtest"
name="MyComp HTML Rendering"
:values='[{"title": "Element 1"}, {"title": "Element"}]'
>Inner HTML Caption</xvue-my-comp
>
</div>
<script>
const mySelector = document.querySelector("xvue-my-comp");
//listen to the event
mySelector.addEventListener("my-click", function (ev) {
console.log("#LISTEN");
alert(ev);
});
</script>
</body>
</html>
Custom WebComponents wrapper
import HTMLParsedElement from "html-parsed-element";
import { createApp, h, toHandlerKey } from "vue";
import { snakeCase, camelCase } from "lodash";
let registeredComponents = {};
const tagPrefix = "xvue-"; // Prefix for Custom Elements (HTML5)
const vueTagPrefix = "xvue-init-"; // Prefix for VUE Components - Must not be tagPrefix to avoid loops
// We use only one file as web component for simplicity
// and because codesandbox doesen't work with dynamic requires
const fileName = "MyComp.vue";
const webComponent = require(`../wcs/MyComp.vue`);
const componentName = snakeCase(
camelCase(
// Gets the file name regardless of folder depth
fileName
.split("/")
.pop()
.replace(/.ce/, "")
.replace(/\.\w+$/, "")
)
).replace(/_/, "-");
// store our component
registeredComponents[componentName] = {
component: webComponent.default
};
export default {
init() {
// HTMLParsedElement is a Polyfil Library (NPM Package) to give a clean parsedCallback as the browser
// default implementation has no defined callback when all props and innerHTML is available
class VueCustomElement extends HTMLParsedElement {
// eslint-disable-next-line
constructor() {
super();
}
parsedCallback() {
console.log("Legacy Component Init", this);
if (!this.getAttribute("_innerHTML")) {
this.setAttribute("data-initialized", this.innerHTML);
}
let rootNode = this;
let vueTagName = this.tagName
.toLowerCase()
.replace(tagPrefix, vueTagPrefix);
let compBaseName = this.tagName.toLowerCase().replace(tagPrefix, "");
let compConfig = registeredComponents[compBaseName];
// Optional: Shadow DOM Mode. Can be used by settings __useShadowDom in component vue file
if (compConfig.component.__useShadowDom) {
rootNode = this.attachShadow({ mode: "open" });
document
.querySelectorAll('head link[rel=stylesheet][href*="core."]')
.forEach((el) => {
rootNode.appendChild(el.cloneNode(true));
});
}
if (vueTagName) {
// If we have no type we do nothing
let appNode = document.createElement("div");
// let appNode = rootNode;
appNode.innerHTML +=
"<" +
vueTagName +
">" +
this.getAttribute("data-initialized") +
"</" +
vueTagName +
">"; // #TODO: Some issues with multiple objects being created via innerHTML and slots
rootNode.appendChild(appNode);
// eslint-disable-next-line #typescript-eslint/no-this-alias
const self = this;
function createCustomEvent(name, args) {
return new CustomEvent(name, {
bubbles: false,
cancelable: false,
detail: args.length === 1 ? args[0] : args
});
}
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
console.log("eventNames", eventNames);
// const handlerName = toHandlerKey(camelCase(name));
eventNames.forEach((name) => {
const handlerName = name;
eventProxies[handlerName] = (...args) => {
this.dispatchEvent(createCustomEvent(name, args));
};
});
}
return eventProxies;
};
const eventProxies = createEventProxies(compConfig.component.emits);
this._props = {};
const app = createApp({
render() {
let props = Object.assign({}, self._props, eventProxies);
// save our attributes as props
[...self.attributes].forEach((attr) => {
let newAttr = {};
newAttr[attr.nodeName] = attr.nodeValue;
props = Object.assign({}, props, newAttr);
});
console.log("props", props);
delete props.dataVApp;
return h(compConfig.component, props);
},
beforeCreate: function () {}
});
// Append only relevant VUE components for this tag
app.component(vueTagPrefix + compBaseName, compConfig.component);
this.vueObject = app.mount(appNode);
console.log("appNode", app.config);
}
}
disconnectedCallback() {
if (this.vueObject) {
this.vueObject.$destroy(); // Remove VUE Object
}
}
adoptedCallback() {
//console.log('Custom square element moved to new page.');
}
}
// Register for all available component tags ---------------------------
// Helper to Copy Classes as customElement.define requires separate Constructors
function cloneClass(parent) {
return class extends parent {};
}
for (const [name, component] of Object.entries(registeredComponents)) {
customElements.define(tagPrefix + name, cloneClass(VueCustomElement));
}
}
};
If I whack this into your render method, it all works fine.
Thus your problem is not Web Components or Events related
document.addEventListener("foo", (evt) => {
console.log("FOOd", evt.detail, evt.composedPath());
});
self.dispatchEvent(
new CustomEvent("foo", {
bubbles: true,
composed: true,
cancelable: false,
detail: self
})
);
addendum after comment:
Your code has bubbles:false - that will never work
This code properly emits an Event
You need to look into how you trigger your proxy code
const createCustomEvent = (name, args = []) => {
return new CustomEvent(name, {
bubbles: true,
composed: true,
cancelable: false,
detail: !args.length ? self : args.length === 1 ? args[0] : args
});
};
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
eventNames.forEach((name) => {
const handlerName =
"on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
eventProxies[handlerName] = (...args) => {
appNode.dispatchEvent(createCustomEvent(name));
};
});
document.addEventListener("foo",evt=>console.warn("foo!",evt));
appNode.dispatchEvent(createCustomEvent("foo"));
console.log(
"#eventProxies",
eventProxies,
self,
"appnode:",
appNode,
"rootNode",
rootNode
);
}
return eventProxies;
};
I guess you could try to use Vue3's (since 3.2) defineCustomElement
import CustomElement from './some/path/CustomElement.ce.vue'
const CustomElementCE = defineCustomElement(CustomElement)
customElements.define('custom-element', CustomElementCE);
More on that here: Vue web components
I found one possible solution. As stated in the vue docs we can use static event listeners that are passed as props. They have to be prefixed with "on" i.E. onMyevent for event name my-event. Though it works fine - the downside is that I can't pass any arguments as I don't see where to get them from.
One strange thing is that I cant add this event to the instance. Something like self.dispatchEvent() doesn't work when placed inside createEventProxies method. So I have to stick to document.dispatchEvent()
Working CodeSandbox.
const createCustomEvent = (name, args = []) => {
return new CustomEvent(name, {
bubbles: false,
composed: true,
cancelable: false,
detail: !args.length ? self : args.length === 1 ? args[0] : args
});
};
const createEventProxies = (eventNames) => {
const eventProxies = {};
if (eventNames) {
eventNames.forEach((name) => {
const handlerName =
"on" + name[0].toUpperCase() + name.substr(1).toLowerCase();
eventProxies[handlerName] = (...args) => {
document.dispatchEvent(createCustomEvent(name));
};
});
console.log("#eventProxies", eventProxies);
}
return eventProxies;
};
I am writing a page showing current posts, I use Contentful CMS and fetch the GraphQl API. I have normal string types in the Content model, but the last one is rich text.
I'm going to add a card with the post's name, title, date, and just a short post text. In this case, I reset the rich text formatting to make it behave like normal text.
When I try to add a list of regular strings in a component everything works, but when I add functions with a rich text render, it returns an error:
Cannot read properties of undefined (reading 'props')
I've tried different ways to do it, and it still won't work. I want to combine one query that fetches normal strings and another that fetch the raw Rich text and render Rich text without formatting
Visually, the list should look like this:
And my error displayed in Gatsby looks like this:
Finally, I would like to show you, most importantly - my code:
import React from "react"
import { graphql } from 'gatsby'
import { BLOCKS, MARKS } from "#contentful/rich-text-types"
import { renderRichText } from "gatsby-source-contentful/rich-text"
import Layout from "../components/base/layout"
import {
PageWrapper,
BlogCardWrapper,
BlogCardElement,
ThumbnailImage,
ContentInlineWrapper,
PreContentParagraph,
ReadMoreParagraph,
BlogTitleHeader,
BlogDateParagraph,
} from "../styles/aktualnosci.style"
import { Headers } from "../utils/data/headersData"
import H1 from "../components/headers/h1"
const AktualnosciPage = ({ data }) => {
const articles = data.allContentfulArtykul.edges
const post = this.props.data.allContentfulArtykul.edges[0].node.content
const option = {
renderNode: {
[BLOCKS.EMBEDDED_ASSET]: node => {
return <img/>
},
[BLOCKS.HEADING_1]: (node, children) => {
return <p style={{padding: '0', margin: '0', display: 'inline-block'}}>{children}</p>
},
[BLOCKS.HEADING_5]: (node, children) => {
return <p style={{padding: '0', margin: '0', display: 'inline-block'}}>{children}</p>
},
[BLOCKS.PARAGRAPH]: (node, children) => {
return <p style={{padding: '0', margin: '0', display: 'inline-block'}}>{children}</p>
},
[BLOCKS.QUOTE]: (node, children) => {
return <p style={{padding: '0', margin: '0', display: 'inline-block'}}>{children}</p>
},
[BLOCKS.UL_LIST]: (node, children) => {
return <p style={{padding: '0', margin: '0', display: 'inline-block'}}>{children}</p>
},
[BLOCKS.LIST_ITEM]: (node, children) => {
return <p style={{padding: '0', margin: '0', display: 'inline-block'}}>{children}</p>
},
},
renderMark: {
[MARKS.BOLD]: (node, children) => {
return <p style={{fontWeight: '100'}}>{children}</p>
},
}
}
const output = renderRichText(post, option)
return (
<Layout>
<PageWrapper>
<H1 name={ Headers.Aktualnosci }/>
<BlogCardWrapper>
{articles.reverse().map(({node}) => {
return (
<div key={node.slug}>
<a href={"/aktualnosci/" + node.slug}>
<BlogCardElement>
<ThumbnailImage
className="j10_dfg4gvBDG"
src={node.thumbnailPhoto.fluid.src}
srcSet={node.thumbnailPhoto.fluid.srcSet}
/>
<ContentInlineWrapper>
<BlogTitleHeader>{node.title}</BlogTitleHeader>
<PreContentParagraph>{output}</PreContentParagraph>
<BlogDateParagraph>{node.createdAt}</BlogDateParagraph>
</ContentInlineWrapper>
<ReadMoreParagraph className="j5_dfg4gvBDG">Czytaj więcej <span style={{color: '#BF1E2D', fontSize: '11px'}}>➤</span></ReadMoreParagraph>
</BlogCardElement>
</a>
</div>
)
})}
</BlogCardWrapper>
</PageWrapper>
</Layout>
)
}
export const query = graphql`
query {
allContentfulArtykul {
edges {
node {
id
thumbnailPhoto {
fluid {
src
srcSet
}
}
title
slug
content {
raw
references {
... on ContentfulAsset {
__typename
contentful_id
fixed(width: 200) {
src
srcSet
}
}
}
}
createdAt(formatString: "YYYY-MM-DD")
}
}
}
}
`
export default AktualnosciPage
If something is missing, please let me know.
You are destructuring your queried data in the component declaration:
const AktualnosciPage = ({ data }) => {}
You are taking props.data directly with { data }
Moreover, because you are in a non-class-based component (you are using a functional component), the use of this is strictly restricted.
You should do directly:
const post = data.allContentfulArtykul.edges[0].node.content
As you do in the line above taking data.allContentfulArtykul.edges.
For a more succinct approach, you can also do:
const articles = data.allContentfulArtykul.edges
const post = articles[0].node.content
The function is getting the value of a button click as props. Data is mapped through to compare that button value to a key in the Data JSON called 'classes'. I am getting all the data correctly. All my console.logs are returning correct values. But for some reason, I cannot render anything.
I've tried to add two return statements. It is not even rendering the p tag with the word 'TEST'. Am I missing something? I have included a Code Sandbox: https://codesandbox.io/s/react-example-8xxih
When I click on the Math button, for example, I want to show the two teachers who teach Math as two bubbles below the buttons.
All the data is loading. Just having an issue with rendering it.
function ShowBubbles(props){
console.log('VALUE', props.target.value)
return (
<div id='bubbles-container'>
<p>TEST</p>
{Data.map((item,index) =>{
if(props.target.value == (Data[index].classes)){
return (
<Bubble key={index} nodeName={Data[index].name}>{Data[index].name}
</Bubble>
)
}
})}
</div>
)
}
Sandbox Link: https://codesandbox.io/embed/react-example-m1880
import React, {Component} from 'react';
import ReactDOM from 'react-dom';
const circleStyle = {
width: 100,
height: 100,
borderRadius: 50,
fontSize: 30,
color: "blue"
};
const Data = [
{
classes: ["Math"],
name: "Mr.Rockow",
id: "135"
},
{
classes: ["English"],
name: "Mrs.Nicastro",
id: "358"
},
{
classes: ["Chemistry"],
name: "Mr.Bloomberg",
id: "405"
},
{
classes: ["Math"],
name: "Mr.Jennings",
id: "293"
}
];
const Bubble = item => {
let {name} = item.children.singleItem;
return (
<div style={circleStyle} onClick={()=>{console.log(name)}}>
<p>{item.children.singleItem.name}</p>
</div>
);
};
function ShowBubbles(props) {
var final = [];
Data.map((item, index) => {
if (props.target.value == Data[index].classes) {
final.push(Data[index])
}
})
return final;
}
function DisplayBubbles(singleItem) {
return <Bubble>{singleItem}</Bubble>
}
class Sidebar extends Component {
constructor(props) {
super(props);
this.state = {
json: [],
classesArray: [],
displayBubble: true
};
this.showNode = this.showNode.bind(this);
}
componentDidMount() {
const newArray = [];
Data.map((item, index) => {
let classPlaceholder = Data[index].classes.toString();
if (newArray.indexOf(classPlaceholder) == -1) {
newArray.push(classPlaceholder);
}
// console.log('newArray', newArray)
});
this.setState({
json: Data,
classesArray: newArray
});
}
showNode(props) {
this.setState({
displayBubble: true
});
if (this.state.displayBubble === true) {
var output = ShowBubbles(props);
this.setState({output})
}
}
render() {
return (
<div>
{/* {this.state.displayBubble ? <ShowBubbles/> : ''} */}
<div id="sidebar-container">
<h1 className="sidebar-title">Classes At School</h1>
<h3>Classes To Search</h3>
{this.state.classesArray.map((item, index) => {
return (
<button
onClick={this.showNode}
className="btn-sidebar"
key={index}
value={this.state.classesArray[index]}
>
{this.state.classesArray[index]}
</button>
);
})}
</div>
{this.state.output && this.state.output.map(item=><DisplayBubbles singleItem={item}/>)}
</div>
);
}
}
ReactDOM.render(<Sidebar />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.0.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.0.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>
The issue here is ShowBubbles is not being rendered into the DOM, instead (according the sandbox), ShowBubbles (a React component) is being directly called in onClick button handlers. While you can technically do this, calling a component from a function will result in JSX, essentially, and you would need to manually insert this into the DOM.
Taking this approach is not very React-y, and there is usually a simpler way to approach this. One such approach would be to call the ShowBubbles directly from another React component, e.g. after your buttons using something like:
<ShowBubbles property1={prop1Value} <etc...> />
There are some other issues with the code (at least from the sandbox) that you will need to work out, but this will at least help get you moving in the right direction.
I want to check if aria-expanded changes after a button click. This is my component
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { ArrowTemplate } from './ArrowTemplate';
import { colors } from '../../utils/css';
import { Text } from '../Text';
const Accordion = ({
rtl, content, color, title, className,
}) => {
const element = useRef(null);
const [isAccordionExpanded, setIsAccordionExpanded] = useState(false);
const toggleAccordion = () => {
setIsAccordionExpanded(!isAccordionExpanded);
};
const height = element.current ? element.current.scrollHeight : '0';
return (
<div className={`accordion-section ${className}`}>
<button className={'accordion-btn'} onClick={toggleAccordion}>
<p className={'accordion-title'}>
<Text isRtl={rtl}>{title}</Text>
</p>
<ArrowTemplate
direction={isAccordionExpanded ? 'up' : 'down'}
onClick={toggleAccordion}
rtl={rtl}
color={color}
/>
</button>
<AccordionContent
className={'accordion-content'}
height={height}
isAccordionExpanded={isAccordionExpanded}
ref={element}
aria-expanded={isAccordionExpanded}
>
<div className={'accordion-text'}>
<Text isRtl={rtl}>{content}</Text>
</div>
</AccordionContent>
</div>
);
};
export const StyledAccordion = styled(Accordion)`
font-family: "Open Sans", sans-serif;
text-align: ${({ rtl }) => (rtl ? 'right' : 'left')};
display: flex;
flex-direction: column;
.accordion-btn {
position: relative;
width: 100%;
background-color: ${colors.LG_GREY_4};
color: ${colors.LG_GREY_5};
cursor: pointer;
padding: 40px 18px;
display: flex;
align-items: center;
border: none;
outline: none;
:hover,
:focus,
:active {
background-color: ${colors.LG_GREY_6};
}
.accordion-title {
${({ rtl }) => (rtl ? 'right: 50px;' : 'left: 50px;')};
position: absolute;
font-weight: 600;
font-size: 14px;
}
}
`;
export const AccordionContent = styled.div`
max-height: ${({ isAccordionExpanded, height }) => (isAccordionExpanded ? height : '0')}px;
overflow: hidden;
background: ${colors.LG_GREY_7};
transition: max-height 0.7s;
.accordion-text {
font-weight: 400;
font-size: 14px;
padding: 18px;
}
`;
Here are my tests
import React from 'react';
import { shallow, configure } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import { StyledAccordion, AccordionContent } from '../Accordion';
configure({ adapter: new Adapter() });
describe('<StyledAccordion/>',
() => {
let wrapper;
beforeEach(() => {
wrapper = shallow(<StyledAccordion/>);
});
it('should match the snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
it('should originally have aria-expanded set to false', () => {
expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(false);
});
it('should set aria-expanded to true onClick', () => {
wrapper.find('.accordion-btn').simulate('click');
expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(true);
});
});
And here's what I'm getting in the console
FAIL src/components/Accordion/test/Accordion.test.js (21.497s)
● <StyledAccordion/> › should originally have aria-expanded set to false
Method “props” is meant to be run on 1 node. 0 found instead.
16 | });
17 | it('should originally have aria-expanded set to false', () => {
> 18 | expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(false);
| ^
19 | });
20 | it('should set aria-expanded to true onClick', () => {
21 | wrapper.find('.accordion-btn').simulate('click');
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.single [as props] (node_modules/enzyme/src/ShallowWrapper.js:1160:17)
at Object.props (src/components/Accordion/test/Accordion.test.js:18:45)
● <StyledAccordion/> › should set aria-expanded to true onClick
Method “simulate” is meant to be run on 1 node. 0 found instead.
19 | });
20 | it('should set aria-expanded to true onClick', () => {
> 21 | wrapper.find('.accordion-btn').simulate('click');
| ^
22 | expect(wrapper.find(AccordionContent).props()['aria-expanded']).toBe(true);
23 | });
24 | });
at ShallowWrapper.single (node_modules/enzyme/src/ShallowWrapper.js:1636:13)
at ShallowWrapper.single [as simulate] (node_modules/enzyme/src/ShallowWrapper.js:1118:17)
at Object.simulate (src/components/Accordion/test/Accordion.test.js:21:38)
How can I test attributes?
Checking attribute by verifying props is fine. Simulating click is fine.
The only cause test fails is how shallow() works under the hood.
You may figure out that by yourselves by checking what wrapper.debug() returns(say by adding console.log(wrapper.debug()))
You will see something like <StyledComponentConsumer><Component ...></StyledComponentConsumer>. So the reason is shallow() does not render nested component. Say, if there were no styled-components and you tried to shallow(<Accordion />).find('span')(assuming that <Text> should be rendered as <span>) you will never find that as well.
First solution may be using mount() instead of shallow()(you should not even need to change test). But I would like to go different way(just an opinion though: https://hackernoon.com/why-i-always-use-shallow-rendering-a3a50da60942)
Read more about shallow vs mount difference at https://medium.com/#Yohanna/difference-between-enzymes-rendering-methods-f82108f49084
Another approach is to use .dive() like
wrapper.dive().find(...)
or even at initialization time:
const wrapper = shallow(...).dive();
And finally you may just export two versions: base one(and write tests against that) and wrapped into styled-components theming. Needs slightly more changes in code. And dislike approaches above tests will not require updates each time you add more HOC wrappers(say wrapping component into styled-components, then connect to redux and finally adding withRouter - tests will still work the same this way). But from the other side this approach does not test your component as it's actually integrated so I'm not really sure if this approach is better.