From 2e189ca8fac89d26838fe1cab6d31b0bc2e60e2a Mon Sep 17 00:00:00 2001 From: Artem Alexeyenko <ala@sitecore.net> Date: Wed, 14 Dec 2022 11:20:31 -0500 Subject: [PATCH] extra unit test coverage for react package (#1258) --- packages/sitecore-jss-react/.nycrc | 1 + .../src/components/Image.test.tsx | 10 + .../src/enhancers/withEditorChromes.test.tsx | 28 +++ .../src/enhancers/withPlaceholder.test.tsx | 196 ++++++++++++++++++ packages/sitecore-jss-react/src/utils.test.ts | 71 +++++-- 5 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 packages/sitecore-jss-react/src/enhancers/withEditorChromes.test.tsx create mode 100644 packages/sitecore-jss-react/src/enhancers/withPlaceholder.test.tsx diff --git a/packages/sitecore-jss-react/.nycrc b/packages/sitecore-jss-react/.nycrc index cd2cf1249f..2a7114e170 100644 --- a/packages/sitecore-jss-react/.nycrc +++ b/packages/sitecore-jss-react/.nycrc @@ -4,6 +4,7 @@ ".tsx" ], "exclude": [ + "**/index.ts", "**/*.d.ts", "**/*.test.tsx", "**/*.test.ts", diff --git a/packages/sitecore-jss-react/src/components/Image.test.tsx b/packages/sitecore-jss-react/src/components/Image.test.tsx index 918f89613a..e9598c073e 100644 --- a/packages/sitecore-jss-react/src/components/Image.test.tsx +++ b/packages/sitecore-jss-react/src/components/Image.test.tsx @@ -283,4 +283,14 @@ describe('<Image />', () => { const rendered = mount(<Image media={img} />); expect(rendered.find('img')).to.have.length(0); }); + + it('should render when field prop is used instead of media prop', () => { + const imgField = { + src: '/assets/img/test0.png', + width: 8, + height: 10, + }; + const rendered = mount(<Image field={imgField} />); + expect(rendered.find('img')).to.have.length(1); + }); }); diff --git a/packages/sitecore-jss-react/src/enhancers/withEditorChromes.test.tsx b/packages/sitecore-jss-react/src/enhancers/withEditorChromes.test.tsx new file mode 100644 index 0000000000..07665463e7 --- /dev/null +++ b/packages/sitecore-jss-react/src/enhancers/withEditorChromes.test.tsx @@ -0,0 +1,28 @@ +/* eslint-disable no-unused-expressions */ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { spy } from 'sinon'; +import { ExperienceEditor } from '@sitecore-jss/sitecore-jss/utils'; + +import { withEditorChromes } from '../enhancers/withEditorChromes'; + +const SampleComponent: React.FC<{ stringProp: string }> = (props: { stringProp: string }) => { + return <div>{props.stringProp}</div>; +}; + +describe('withEditorChromes', () => { + it('should update chromes on update', () => { + const WrappedComponent = withEditorChromes(SampleComponent as React.FC<unknown>); + const props = { + stringProp: '123', + }; + // sinon cannot spy on the resetEditorChromes instance used in withEditorChromes - so we test for method that is called by it + const utilSpy = spy(ExperienceEditor, 'isActive'); + + const rendered = mount(<WrappedComponent {...props} />); + expect(rendered.children.length).to.not.equal(0); + rendered.setProps({ stringProp: '456' }); + expect(utilSpy.called).to.be.true; + }); +}); diff --git a/packages/sitecore-jss-react/src/enhancers/withPlaceholder.test.tsx b/packages/sitecore-jss-react/src/enhancers/withPlaceholder.test.tsx new file mode 100644 index 0000000000..78901cbcb0 --- /dev/null +++ b/packages/sitecore-jss-react/src/enhancers/withPlaceholder.test.tsx @@ -0,0 +1,196 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable react/prop-types */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import React, { ReactElement } from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { altData, convertedDevData as nonEeDevData } from '../test-data/non-ee-data'; +import { convertedData as eeData } from '../test-data/ee-data'; +import { withPlaceholder } from '../enhancers/withPlaceholder'; +import { SitecoreContext } from '../components/SitecoreContext'; +import { PlaceholderProps } from '../components/PlaceholderCommon'; +import PropTypes from 'prop-types'; +import { ComponentFactory } from '../components/sharedTypes'; +import { ComponentRendering, RouteData } from '@sitecore-jss/sitecore-jss/layout'; +import { Placeholder } from '../components/Placeholder'; + +type CalloutProps = PlaceholderProps & { + [prop: string]: unknown; + fields?: { message?: { value?: string } }; + subProp?: ReactElement; +}; + +type HomeProps = PlaceholderProps & { + [prop: string]: unknown; + rendering?: RouteData | ComponentRendering; + subProp?: ReactElement; +}; + +const DownloadCallout: React.FC<CalloutProps> = (props) => ( + <div className="download-callout-mock"> + {props.fields?.message ? props.fields.message.value : ''} + </div> +); + +const Home: React.FC<HomeProps> = ({ + rendering, + render, + renderEach, + renderEmpty, + name, + subProp, + ...otherProps +}) => { + if (subProp && !otherProps.reset) { + return <div className="home-mock-with-prop">{subProp}</div>; + } else { + return ( + <div className="home-mock"> + <Placeholder name={name} rendering={rendering} {...otherProps} /> + </div> + ); + } +}; + +const ErrorComponent: React.FC = () => { + throw 'Error!'; +}; + +const ErrorMessageComponent: React.FC = () => ( + <div className="error-handled">Your error has been... dealt with.</div> +); + +const componentFactory: ComponentFactory = (componentName: string) => { + const components = new Map<string, React.FC>(); + + // pass otherProps to page-content to test property cascading through the Placeholder + + Home.propTypes = { + placeholders: PropTypes.object, + }; + + components.set('Home', Home); + + DownloadCallout.propTypes = { + fields: PropTypes.shape({ + message: PropTypes.shape({ + value: PropTypes.string, + }), + }).isRequired, + }; + + components.set('DownloadCallout', DownloadCallout); + components.set('Jumbotron', () => <div className="jumbotron-mock"></div>); + + return components.get(componentName) || null; +}; + +const testData = [ + { label: 'Dev data', data: nonEeDevData }, + { label: 'LayoutService data - EE on', data: eeData }, +]; + +describe('withPlaceholder HOC', () => { + describe('Error handling', () => { + it('should render default error component on wrapped component error', () => { + const phKey = 'page-content'; + const props: PlaceholderProps = { + name: phKey, + rendering: null, + componentFactory: componentFactory, + }; + const Element = withPlaceholder(phKey)(ErrorComponent); + const renderedComponent = mount(<Element {...props} />); + expect(renderedComponent.find('.sc-jss-placeholder-error').length).to.equal(1); + }); + + it('should render custom component error on wrapped component error, when provided', () => { + const phKey = 'page-content'; + const props: PlaceholderProps = { + name: phKey, + rendering: null, + componentFactory: componentFactory, + errorComponent: ErrorMessageComponent, + }; + const Element = withPlaceholder(phKey)(ErrorComponent); + const renderedComponent = mount(<Element {...props} />); + expect(renderedComponent.find('.error-handled').length).to.equal(1); + }); + }); + + testData.forEach((dataSet) => { + describe(`with ${dataSet.label}`, () => { + it('should render a placeholder with given key', () => { + const component = (dataSet.data.sitecore.route.placeholders.main as ( + | ComponentRendering + | RouteData + )[]).find((c) => (c as ComponentRendering).componentName); + const phKey = 'page-content'; + const props: PlaceholderProps = { + name: phKey, + rendering: component, + componentFactory: componentFactory, + }; + const Element = withPlaceholder(phKey)(Home); + const renderedComponent = mount( + <SitecoreContext componentFactory={componentFactory}> + <Element {...props} /> + </SitecoreContext> + ); + expect(renderedComponent.find('.download-callout-mock').length).to.equal(1); + }); + it('should render a placeholder with given key and prop', () => { + const component = (dataSet.data.sitecore.route.placeholders.main as ( + | ComponentRendering + | RouteData + )[]).find((c) => (c as ComponentRendering).componentName); + const phKeyAndProp = { + placeholder: 'page-header', + prop: 'subProp', + }; + const props: PlaceholderProps = { + name: 'page-header', + rendering: component, + componentFactory: componentFactory, + }; + const Element = withPlaceholder(phKeyAndProp)(Home); + const renderedComponent = mount( + <SitecoreContext componentFactory={componentFactory}> + <Element {...props} /> + </SitecoreContext> + ); + expect(renderedComponent.find('.home-mock-with-prop').length).to.not.equal(0); + expect(renderedComponent.find('.jumbotron-mock').length).to.equal(1); + }); + it('should use propsTransformer method when provided', () => { + const component = (dataSet.data.sitecore.route.placeholders.main as ( + | ComponentRendering + | RouteData + )[]).find((c) => (c as ComponentRendering).componentName); + const phKeyAndProp = { + placeholder: 'page-header', + prop: 'subProp', + }; + const phOptions = { + propsTransformer: (props) => { + return { ...props, reset: true }; + }, + }; + const props: PlaceholderProps = { + name: 'page-header', + rendering: component, + componentFactory: componentFactory, + }; + const Element = withPlaceholder(phKeyAndProp, phOptions)(Home); + const renderedComponent = mount( + <SitecoreContext componentFactory={componentFactory}> + <Element {...props} /> + </SitecoreContext> + ); + expect(renderedComponent.find('.home-mock-with-prop').length).to.equal(0); + expect(renderedComponent.find('.home-mock').length).to.not.equal(0); + }); + }); + }); +}); diff --git a/packages/sitecore-jss-react/src/utils.test.ts b/packages/sitecore-jss-react/src/utils.test.ts index 6d8611e259..6c0a07833e 100644 --- a/packages/sitecore-jss-react/src/utils.test.ts +++ b/packages/sitecore-jss-react/src/utils.test.ts @@ -1,19 +1,64 @@ import { expect } from 'chai'; -import { convertStyleAttribute } from './utils'; +import { addClassName, convertAttributesToReactProps, convertStyleAttribute } from './utils'; -describe('convertStyleAttribute', () => { - it('should return object representation of style attribute names and values', () => { - const data = { - style: 'background-color: white; opacity: 0.35; filter: alpha(opacity=35);', - }; +describe('jss-react utils', () => { + describe('convertStyleAttribute', () => { + it('should return object representation of style attribute names and values', () => { + const data = { + style: 'background-color: white; opacity: 0.35; filter: alpha(opacity=35);', + }; - const expected = { - backgroundColor: 'white', - opacity: 0.35, - filter: 'alpha(opacity=35)', - }; + const expected = { + backgroundColor: 'white', + opacity: 0.35, + filter: 'alpha(opacity=35)', + }; - const actual = convertStyleAttribute(data.style); - expect(actual).to.eql(expected); + const actual = convertStyleAttribute(data.style); + expect(actual).to.eql(expected); + }); + }); + describe('convertAttributesToReactProps', () => { + it('should covert class and style attributes', () => { + const inputAttr = { + class: 'classy', + style: 'background-color: white; opacity: 0.35; filter: alpha(opacity=35);', + }; + + const expected = { + className: 'classy', + style: { + backgroundColor: 'white', + opacity: 0.35, + filter: 'alpha(opacity=35)', + }, + }; + + const outputAttr = convertAttributesToReactProps(inputAttr); + expect(outputAttr).to.deep.equal(expected); + }); + }); + + describe('addClassName', () => { + it('should add class attribute value to className', () => { + const modifiableAttrs = { + className: 'first-class', + class: 'second-class', + }; + addClassName(modifiableAttrs); + expect(modifiableAttrs).to.deep.equal({ + className: 'first-class second-class', + }); + + it('should convert class attribute value to className when className is absent', () => { + const modifiableAttrs = { + class: 'second-class', + }; + addClassName(modifiableAttrs); + expect(modifiableAttrs).to.deep.equal({ + className: 'second-class', + }); + }); + }); }); });