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',
+        });
+      });
+    });
   });
 });