From f8785b95da194513bec2c19e6fb17187cca09c01 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Wed, 17 Apr 2024 14:40:14 +0300 Subject: [PATCH 01/12] introduce error boundary component --- .../src/components/ErrorBoundary.tsx | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 packages/sitecore-jss-react/src/components/ErrorBoundary.tsx diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx new file mode 100644 index 0000000000..9e0e893913 --- /dev/null +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -0,0 +1,38 @@ +import React, { ReactNode } from 'react'; + +export type ErrorBoundaryProps = { + children: ReactNode; +}; + +class ErrorBoundary extends React.Component { + state: Readonly<{ error?: Error }>; + + constructor(props: any) { + super(props); + this.state = {}; + } + + static getDerivedStateFromError(error: Error) { + // Update state so the next render will show the fallback UI + return { error: error }; + } + + componentDidCatch(error: Error, errorInfo: any) { + // You can use your own error logging service here + console.log({ error, errorInfo }); + } + + render() { + if (this.state.error) { + return ( +
+

Oops, there is an error!

+
+ ); + } + + return this.props.children; + } +} + +export default ErrorBoundary; From 477fbeda10b1a1a20ad657701e2b0ccf7e4b7ef3 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Mon, 22 Apr 2024 14:11:58 +0300 Subject: [PATCH 02/12] Added suspense and wrapped components by ErrorBoundary --- .../src/components/ErrorBoundary.tsx | 12 ++++++++++-- .../src/components/PlaceholderCommon.tsx | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index 9e0e893913..6b22eeb242 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -1,4 +1,4 @@ -import React, { ReactNode } from 'react'; +import React, { ReactNode, Suspense } from 'react'; export type ErrorBoundaryProps = { children: ReactNode; @@ -31,7 +31,15 @@ class ErrorBoundary extends React.Component { ); } - return this.props.children; + return ( + Component is still loading... + } + > + {this.props.children} + + ); } } diff --git a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx index 6c2292801c..8c5db2b0f0 100644 --- a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx +++ b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx @@ -15,6 +15,7 @@ import { FEaaSComponent, FEAAS_COMPONENT_RENDERING_NAME } from './FEaaSComponent import { FEaaSWrapper, FEAAS_WRAPPER_RENDERING_NAME } from './FEaaSWrapper'; import { BYOCComponent, BYOC_COMPONENT_RENDERING_NAME } from './BYOCComponent'; import { BYOCWrapper, BYOC_WRAPPER_RENDERING_NAME } from './BYOCWrapper'; +import ErrorBoundary from './ErrorBoundary'; type ErrorComponentProps = { [prop: string]: unknown; @@ -256,7 +257,8 @@ export class PlaceholderCommon extends React.Compone this.props.modifyComponentProps ? this.props.modifyComponentProps(finalProps) : finalProps ); }) - .filter((element) => element); // remove nulls + .filter((element) => element) + .map((element, id) => {element}); } getComponentForRendering(renderingDefinition: ComponentRendering): ComponentType | null { From 6d751eac720ae282052cd89431d8cafe0956d6c6 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 25 Apr 2024 15:11:42 +0300 Subject: [PATCH 03/12] wrap components in error boundary component; error boundary component --- .../src/components/ErrorBoundary.tsx | 41 +++++++++++-------- .../src/components/PlaceholderCommon.tsx | 10 ++++- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index 6b22eeb242..414a825e42 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -1,44 +1,53 @@ import React, { ReactNode, Suspense } from 'react'; +import { ComponentRendering } from '@sitecore-jss/sitecore-jss/layout'; + +type CustomErrorComponentProps = { + [prop: string]: unknown; +}; export type ErrorBoundaryProps = { children: ReactNode; + customErrorComponent?: + | React.ComponentClass + | React.FC; + rendering?: ComponentRendering; }; class ErrorBoundary extends React.Component { - state: Readonly<{ error?: Error }>; + state: { error: Error }; constructor(props: any) { super(props); - this.state = {}; + this.state = { error: null }; } static getDerivedStateFromError(error: Error) { - // Update state so the next render will show the fallback UI return { error: error }; } componentDidCatch(error: Error, errorInfo: any) { - // You can use your own error logging service here console.log({ error, errorInfo }); } render() { if (this.state.error) { - return ( -
-

Oops, there is an error!

-
- ); + if (this.props.customErrorComponent) { + return ; + } else { + return ( +
+
+ A rendering error occurred in component {this.props.rendering?.componentName} +
+ Error: {this.state.error.message} +
+
+ ); + } } return ( - Component is still loading... - } - > - {this.props.children} - + Component is still loading...}>{this.props.children} ); } } diff --git a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx index 8c5db2b0f0..1fc0e35ef1 100644 --- a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx +++ b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx @@ -258,7 +258,15 @@ export class PlaceholderCommon extends React.Compone ); }) .filter((element) => element) - .map((element, id) => {element}); + .map((element, id) => ( + + {element} + + )); } getComponentForRendering(renderingDefinition: ComponentRendering): ComponentType | null { From cc929ba0678c56af46303d7b9dd10c2ff678fdc1 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 25 Apr 2024 20:49:49 +0300 Subject: [PATCH 04/12] account for page editing state when displaying error for component --- .../src/components/ErrorBoundary.tsx | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index 414a825e42..1d1631c626 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -1,5 +1,7 @@ import React, { ReactNode, Suspense } from 'react'; import { ComponentRendering } from '@sitecore-jss/sitecore-jss/layout'; +import { withSitecoreContext } from '../enhancers/withSitecoreContext'; +import { SitecoreContextValue } from './SitecoreContext'; type CustomErrorComponentProps = { [prop: string]: unknown; @@ -11,6 +13,7 @@ export type ErrorBoundaryProps = { | React.ComponentClass | React.FC; rendering?: ComponentRendering; + sitecoreContext: SitecoreContextValue; }; class ErrorBoundary extends React.Component { @@ -31,9 +34,7 @@ class ErrorBoundary extends React.Component { render() { if (this.state.error) { - if (this.props.customErrorComponent) { - return ; - } else { + if (this.props.sitecoreContext.pageEditing) { return (
@@ -43,6 +44,18 @@ class ErrorBoundary extends React.Component {
); + } else { + if (this.props.customErrorComponent) { + return ; + } else { + return ( +
+
+ A rendering error occurred in component {this.state.error.message} +
+
+ ); + } } } @@ -52,4 +65,4 @@ class ErrorBoundary extends React.Component { } } -export default ErrorBoundary; +export default withSitecoreContext()(ErrorBoundary); From 45b2bbba20718c82ab593c6c519ed4cbbbe409b2 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 26 Apr 2024 13:37:50 +0300 Subject: [PATCH 05/12] ErrorBoundary unit tests and small fix --- .../src/components/ErrorBoundary.test.tsx | 107 ++++++++++++++++++ .../src/components/ErrorBoundary.tsx | 2 +- 2 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx new file mode 100644 index 0000000000..3486cde391 --- /dev/null +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { expect } from 'chai'; +import { mount } from 'enzyme'; +import { spy } from 'sinon'; +import ErrorBoundary from './ErrorBoundary'; +import { SitecoreContextReactContext } from '../components/SitecoreContext'; +import { ComponentRendering } from '@sitecore-jss/sitecore-jss/layout'; + +describe('ErrorBoundary', () => { + it('Should render provided component when no error is thrown', () => { + const Foo = () =>

foo

; + + const rendered = mount( + + + + ); + + const hElement = rendered.find('h2'); + + expect(hElement.length).to.equal(1); + expect(hElement.text()).to.equal('foo'); + }); + + describe('when in page editing mode', () => { + it('Should render errors message and errored component name when error is thrown', () => { + const setContext = spy(); + + const testComponentProps = { + context: { + pageEditing: true, + }, + setContext, + }; + + const testComponentName = 'Test component Name'; + const rendering: ComponentRendering = { componentName: testComponentName }; + + const errorMessage = 'an error occured'; + const TestErrorComponent: React.FC = () => { + throw Error(errorMessage); + }; + + const rendered = mount( + + + + + + ); + + expect(rendered.html()).to.contain('class="sc-jss-placeholder-error"'); + expect(rendered.html()).to.contain('A rendering error occurred in component'); + expect(rendered.find('em').length).to.equal(2); + expect( + rendered + .find('em') + .at(0) + .text() + ).to.equal(testComponentName); + expect( + rendered + .find('em') + .at(1) + .text() + ).to.equal(errorMessage); + }); + }); + describe('when not in page editing mode', () => { + it('Should render errors message when error is thrown', () => { + const errorMessage = 'an error occured'; + const TestErrorComponent: React.FC = () => { + throw Error(errorMessage); + }; + + const rendered = mount( + + + + ); + + expect(rendered.html()).to.contain('class="sc-jss-placeholder-error"'); + expect(rendered.html()).to.contain('A rendering error occurred in component'); + expect(rendered.find('em').length).to.equal(1); + expect(rendered.find('em').text()).to.equal(errorMessage); + }); + + it('Should render custom error component when custom error component is provided and error is thrown', () => { + const errorMessage = 'an error occured'; + const TestErrorComponent: React.FC = () => { + throw Error(errorMessage); + }; + + const CustomErrorComponent: React.FC = () => { + return
This is a custom error component!
; + }; + + const rendered = mount( + + + + ); + expect(rendered.find('div').length).to.equal(1); + expect(rendered.find('div').text()).to.equal('This is a custom error component!'); + }); + }); +}); diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index 1d1631c626..9247a3aed2 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -34,7 +34,7 @@ class ErrorBoundary extends React.Component { render() { if (this.state.error) { - if (this.props.sitecoreContext.pageEditing) { + if (this.props.sitecoreContext?.pageEditing) { return (
From 855df13b141ce6c221de5a19e588a85e455175b5 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 26 Apr 2024 14:59:35 +0300 Subject: [PATCH 06/12] add unit test for placeholder to cover error boundary component wrapping; add 'type' prop in ErrorBoundary to cover case when renderEach function is being used --- .../src/components/ErrorBoundary.tsx | 1 + .../src/components/Placeholder.test.tsx | 40 +++++++++++++++++++ .../src/components/PlaceholderCommon.tsx | 23 ++++++----- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index 9247a3aed2..67ed9df57b 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -14,6 +14,7 @@ export type ErrorBoundaryProps = { | React.FC; rendering?: ComponentRendering; sitecoreContext: SitecoreContextValue; + type: string; }; class ErrorBoundary extends React.Component { diff --git a/packages/sitecore-jss-react/src/components/Placeholder.test.tsx b/packages/sitecore-jss-react/src/components/Placeholder.test.tsx index b4d6dd7467..0c249b52e0 100644 --- a/packages/sitecore-jss-react/src/components/Placeholder.test.tsx +++ b/packages/sitecore-jss-react/src/components/Placeholder.test.tsx @@ -534,6 +534,46 @@ describe('', () => { expect(renderedComponent.find('.sc-jss-placeholder-error').length).to.equal(1); }); + it('should render error message on error, only for the errored component', () => { + const componentFactory: ComponentFactory = (componentName: string) => { + const components = new Map(); + + const Home: React.FC<{ rendering?: RouteData }> = ({ rendering }) => ( +
+ +
+ ); + + components.set('Home', Home); + components.set('ThrowError', () => { + throw Error('an error occured'); + }); + components.set('Foo', () =>
foo
); + + return components.get(componentName) || null; + }; + + const route = ({ + placeholders: { + main: [ + { + componentName: 'ThrowError', + }, + { + componentName: 'Foo', + }, + ], + }, + } as unknown) as RouteData; + const phKey = 'main'; + + const renderedComponent = mount( + + ); + expect(renderedComponent.find('.sc-jss-placeholder-error').length).to.equal(1); + expect(renderedComponent.find('div.foo-class').length).to.equal(1); + }); + it('should render custom errorComponent on error, if provided', () => { const componentFactory: ComponentFactory = (componentName: string) => { const components = new Map>(); diff --git a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx index 1fc0e35ef1..5a124fe9e2 100644 --- a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx +++ b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx @@ -258,15 +258,20 @@ export class PlaceholderCommon extends React.Compone ); }) .filter((element) => element) - .map((element, id) => ( - - {element} - - )); + .map((element, id) => { + // assign type based on passed element - type='text/sitecore' should be ignored when renderEach Placeholder prop function is being used + const type = element.props.type === 'text/sitecore' ? element.props.type : ''; + return ( + + {element} + + ); + }); } getComponentForRendering(renderingDefinition: ComponentRendering): ComponentType | null { From 62bf325fc0fbfd6b0fbb78dbcff6c933d25f75e0 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 26 Apr 2024 15:25:28 +0300 Subject: [PATCH 07/12] add configurable loading message for Suspense component --- .../sitecore-jss-react/src/components/ErrorBoundary.tsx | 9 +++++++-- .../src/components/PlaceholderCommon.tsx | 6 ++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index 67ed9df57b..06a7bb69f7 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -14,6 +14,7 @@ export type ErrorBoundaryProps = { | React.FC; rendering?: ComponentRendering; sitecoreContext: SitecoreContextValue; + componentLoadingMessage?: string; type: string; }; @@ -30,7 +31,7 @@ class ErrorBoundary extends React.Component { } componentDidCatch(error: Error, errorInfo: any) { - console.log({ error, errorInfo }); + console.error({ error, errorInfo }); } render() { @@ -61,7 +62,11 @@ class ErrorBoundary extends React.Component { } return ( - Component is still loading...}>{this.props.children} + {this.props.componentLoadingMessage || 'Component is still loading...'}} + > + {this.props.children} + ); } } diff --git a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx index 5a124fe9e2..946f87c6ba 100644 --- a/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx +++ b/packages/sitecore-jss-react/src/components/PlaceholderCommon.tsx @@ -75,6 +75,11 @@ export interface PlaceholderProps { * the placeholder */ errorComponent?: React.ComponentClass | React.FC; + + /** + * The message that gets displayed while component is loading + */ + componentLoadingMessage?: string; } export class PlaceholderCommon extends React.Component { @@ -266,6 +271,7 @@ export class PlaceholderCommon extends React.Compone key={element.type + '-' + id} customErrorComponent={this.props.errorComponent} rendering={element.props.rendering as ComponentRendering} + componentLoadingMessage={this.props.componentLoadingMessage} type={type} > {element} From d5a5abb0f18be6eecc89f189923747fda6639fcf Mon Sep 17 00:00:00 2001 From: yavorsk Date: Fri, 26 Apr 2024 16:04:56 +0300 Subject: [PATCH 08/12] update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a76a03d875..87f113772c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,10 @@ Our versioning strategy is as follows: ## Unreleased +### 🎉 New Features & Improvements + +* `[sitecore-jss-react]`Introduce ErrorBoundary component. All rendered components are wrapped with it and it will catch client or server side errors from any of its children, display appropriate message and prevent the rest of the application from failing. It accepts and can display custom error component and loading message if it is passed as a prop to parent Placeholder. ([#1786](https://github.com/Sitecore/jss/pull/1786)) + ## 22.0.0 ### 🛠 Breaking Changes From b8e46e2d55c9ac712d27c26773d41f4f26ffa25b Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 29 Apr 2024 11:54:37 +0300 Subject: [PATCH 09/12] rework showing of error component logic, adjust unit tests --- .../src/components/ErrorBoundary.test.tsx | 170 ++++++++++++++++-- .../src/components/ErrorBoundary.tsx | 36 ++-- 2 files changed, 174 insertions(+), 32 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx index 3486cde391..207d34c8d6 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx @@ -5,24 +5,44 @@ import { spy } from 'sinon'; import ErrorBoundary from './ErrorBoundary'; import { SitecoreContextReactContext } from '../components/SitecoreContext'; import { ComponentRendering } from '@sitecore-jss/sitecore-jss/layout'; +import { afterEach } from 'node:test'; describe('ErrorBoundary', () => { - it('Should render provided component when no error is thrown', () => { - const Foo = () =>

foo

; + describe('when in page editing mode', () => { + it('Should render custom error component when custom error component is provided and error is thrown', () => { + const setContext = spy(); - const rendered = mount( - - - - ); + const testComponentProps = { + context: { + pageEditing: true, + }, + setContext, + }; - const hElement = rendered.find('h2'); + const testComponentName = 'Test component Name'; + const rendering: ComponentRendering = { componentName: testComponentName }; - expect(hElement.length).to.equal(1); - expect(hElement.text()).to.equal('foo'); - }); + const errorMessage = 'an error occured'; + const TestErrorComponent: React.FC = () => { + throw Error(errorMessage); + }; + + const CustomErrorComponent: React.FC = () => { + return
This is a custom error component!
; + }; + + const rendered = mount( + + + + + + ); + + expect(rendered.find('div').length).to.equal(1); + expect(rendered.find('div').text()).to.equal('This is a custom error component!'); + }); - describe('when in page editing mode', () => { it('Should render errors message and errored component name when error is thrown', () => { const setContext = spy(); @@ -66,25 +86,121 @@ describe('ErrorBoundary', () => { ).to.equal(errorMessage); }); }); - describe('when not in page editing mode', () => { - it('Should render errors message when error is thrown', () => { + describe('when in development mode', () => { + before(() => { + process.env.NODE_ENV = 'development'; + }); + + after(() => { + delete process.env.NODE_ENV; + }); + + it('Should render custom error component when custom error component is provided and error is thrown', () => { const errorMessage = 'an error occured'; const TestErrorComponent: React.FC = () => { throw Error(errorMessage); }; + const CustomErrorComponent: React.FC = () => { + return
This is a custom error component!
; + }; + const rendered = mount( - + ); + expect(rendered.find('div').length).to.equal(1); + expect(rendered.find('div').text()).to.equal('This is a custom error component!'); + }); + + it('Should render errors message and errored component name when error is thrown and is in page editing mode', () => { + const setContext = spy(); + + const testComponentProps = { + context: { + pageEditing: true, + }, + setContext, + }; + + const testComponentName = 'Test component Name'; + const rendering: ComponentRendering = { componentName: testComponentName }; + + const errorMessage = 'an error occured'; + const TestErrorComponent: React.FC = () => { + throw Error(errorMessage); + }; + + const rendered = mount( + + + + + + ); expect(rendered.html()).to.contain('class="sc-jss-placeholder-error"'); expect(rendered.html()).to.contain('A rendering error occurred in component'); - expect(rendered.find('em').length).to.equal(1); - expect(rendered.find('em').text()).to.equal(errorMessage); + expect(rendered.find('em').length).to.equal(2); + expect( + rendered + .find('em') + .at(0) + .text() + ).to.equal(testComponentName); + expect( + rendered + .find('em') + .at(1) + .text() + ).to.equal(errorMessage); }); + it('Should render errors message and errored component name when error is thrown and is not in page editing mode', () => { + const setContext = spy(); + + const testComponentProps = { + context: { + pageEditing: false, + }, + setContext, + }; + + const testComponentName = 'Test component Name'; + const rendering: ComponentRendering = { componentName: testComponentName }; + + const errorMessage = 'an error occured'; + const TestErrorComponent: React.FC = () => { + throw Error(errorMessage); + }; + + const rendered = mount( + + + + + + ); + + expect(rendered.html()).to.contain('class="sc-jss-placeholder-error"'); + expect(rendered.html()).to.contain('A rendering error occurred in component'); + expect(rendered.find('em').length).to.equal(2); + expect( + rendered + .find('em') + .at(0) + .text() + ).to.equal(testComponentName); + expect( + rendered + .find('em') + .at(1) + .text() + ).to.equal(errorMessage); + }); + }); + describe('when not in page editing and not i development mode', () => { it('Should render custom error component when custom error component is provided and error is thrown', () => { const errorMessage = 'an error occured'; const TestErrorComponent: React.FC = () => { @@ -103,5 +219,25 @@ describe('ErrorBoundary', () => { expect(rendered.find('div').length).to.equal(1); expect(rendered.find('div').text()).to.equal('This is a custom error component!'); }); + + it('Should render default errors message when error is thrown and custom error component is not provided', () => { + const errorMessage = 'an error occured'; + const TestErrorComponent: React.FC = () => { + throw Error(errorMessage); + }; + + const rendered = mount( + + + + ); + console.log(rendered.html()); + expect(rendered.html()).to.contain('class="sc-jss-placeholder-error"'); + expect(rendered.html()).to.contain( + "We're having trouble loading this section. Please try again by refreshing your browser." // eslint-disable-line + ); + expect(rendered.find('em').length).to.equal(0); + expect(rendered.html()).to.not.contain(errorMessage); + }); }); }); diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index 06a7bb69f7..c82e5ea1ce 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -19,6 +19,9 @@ export type ErrorBoundaryProps = { }; class ErrorBoundary extends React.Component { + defaultErrorMessage = + "We're having trouble loading this section. Please try again by refreshing your browser."; // eslint-disable-line + defaultLoadingMessage = 'Component is still loading...'; state: { error: Error }; constructor(props: any) { @@ -34,36 +37,39 @@ class ErrorBoundary extends React.Component { console.error({ error, errorInfo }); } + isInDevMode(): boolean { + return process.env.NODE_ENV === 'development'; + } + render() { if (this.state.error) { - if (this.props.sitecoreContext?.pageEditing) { - return ( -
-
- A rendering error occurred in component {this.props.rendering?.componentName} -
- Error: {this.state.error.message} -
-
- ); + if (this.props.customErrorComponent) { + return ; } else { - if (this.props.customErrorComponent) { - return ; - } else { + if (this.isInDevMode() || this.props.sitecoreContext?.pageEditing) { return (
- A rendering error occurred in component {this.state.error.message} + A rendering error occurred in component{' '} + {this.props.rendering?.componentName} +
+ Error: {this.state.error.message}
); + } else { + return ( +
+
{this.defaultErrorMessage}
+
+ ); } } } return ( {this.props.componentLoadingMessage || 'Component is still loading...'}} + fallback={

{this.props.componentLoadingMessage || this.defaultLoadingMessage}

} > {this.props.children}
From 96faff26e7b2bc000f28a53bfdc8745ffe59f53b Mon Sep 17 00:00:00 2001 From: yavorsk Date: Mon, 29 Apr 2024 15:30:03 +0300 Subject: [PATCH 10/12] update text of error messages --- packages/sitecore-jss-react/src/components/ErrorBoundary.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index c82e5ea1ce..af0146aea8 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -20,8 +20,8 @@ export type ErrorBoundaryProps = { class ErrorBoundary extends React.Component { defaultErrorMessage = - "We're having trouble loading this section. Please try again by refreshing your browser."; // eslint-disable-line - defaultLoadingMessage = 'Component is still loading...'; + 'There was a problem loading this section. Refresh your browser to try again.'; // eslint-disable-line + defaultLoadingMessage = 'Loading component...'; state: { error: Error }; constructor(props: any) { From 711a712920bb51767cbe3ce457041015be46707b Mon Sep 17 00:00:00 2001 From: yavorsk Date: Tue, 30 Apr 2024 10:43:40 +0300 Subject: [PATCH 11/12] unit test fix --- .../sitecore-jss-react/src/components/ErrorBoundary.test.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx index 207d34c8d6..893cdac087 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx @@ -200,7 +200,7 @@ describe('ErrorBoundary', () => { ).to.equal(errorMessage); }); }); - describe('when not in page editing and not i development mode', () => { + describe('when not in page editing and not in development mode', () => { it('Should render custom error component when custom error component is provided and error is thrown', () => { const errorMessage = 'an error occured'; const TestErrorComponent: React.FC = () => { @@ -234,7 +234,7 @@ describe('ErrorBoundary', () => { console.log(rendered.html()); expect(rendered.html()).to.contain('class="sc-jss-placeholder-error"'); expect(rendered.html()).to.contain( - "We're having trouble loading this section. Please try again by refreshing your browser." // eslint-disable-line + 'There was a problem loading this section. Refresh your browser to try again.' // eslint-disable-line ); expect(rendered.find('em').length).to.equal(0); expect(rendered.html()).to.not.contain(errorMessage); From 5efa8413e87add04bd9dcf095c0296df4c139112 Mon Sep 17 00:00:00 2001 From: yavorsk Date: Thu, 2 May 2024 11:25:57 +0300 Subject: [PATCH 12/12] specify proper types instead of any; reorder properties in props interface --- .../sitecore-jss-react/src/components/ErrorBoundary.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx index af0146aea8..ab3176c909 100644 --- a/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx +++ b/packages/sitecore-jss-react/src/components/ErrorBoundary.tsx @@ -9,13 +9,13 @@ type CustomErrorComponentProps = { export type ErrorBoundaryProps = { children: ReactNode; + sitecoreContext: SitecoreContextValue; + type: string; customErrorComponent?: | React.ComponentClass | React.FC; rendering?: ComponentRendering; - sitecoreContext: SitecoreContextValue; componentLoadingMessage?: string; - type: string; }; class ErrorBoundary extends React.Component { @@ -24,7 +24,7 @@ class ErrorBoundary extends React.Component { defaultLoadingMessage = 'Loading component...'; state: { error: Error }; - constructor(props: any) { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { error: null }; } @@ -33,7 +33,7 @@ class ErrorBoundary extends React.Component { return { error: error }; } - componentDidCatch(error: Error, errorInfo: any) { + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { console.error({ error, errorInfo }); }