-
Notifications
You must be signed in to change notification settings - Fork 276
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[sitecore-jss-react] Introduce ErrorBoundary to serve as error handli…
…ng per component (#1786) * introduce error boundary component * Added suspense and wrapped components by ErrorBoundary * wrap components in error boundary component; error boundary component * account for page editing state when displaying error for component * ErrorBoundary unit tests and small fix * 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 * add configurable loading message for Suspense component * update changelog * rework showing of error component logic, adjust unit tests * update text of error messages * unit test fix * specify proper types instead of any; reorder properties in props interface --------- Co-authored-by: illiakovalenko <[email protected]>
- Loading branch information
1 parent
09561a0
commit 8ae58c1
Showing
5 changed files
with
389 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
243 changes: 243 additions & 0 deletions
243
packages/sitecore-jss-react/src/components/ErrorBoundary.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,243 @@ | ||
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'; | ||
import { afterEach } from 'node:test'; | ||
|
||
describe('ErrorBoundary', () => { | ||
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 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 CustomErrorComponent: React.FC = () => { | ||
return <div>This is a custom error component!</div>; | ||
}; | ||
|
||
const rendered = mount( | ||
<SitecoreContextReactContext.Provider value={testComponentProps}> | ||
<ErrorBoundary rendering={rendering} customErrorComponent={CustomErrorComponent}> | ||
<TestErrorComponent /> | ||
</ErrorBoundary> | ||
</SitecoreContextReactContext.Provider> | ||
); | ||
|
||
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', () => { | ||
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( | ||
<SitecoreContextReactContext.Provider value={testComponentProps}> | ||
<ErrorBoundary rendering={rendering}> | ||
<TestErrorComponent /> | ||
</ErrorBoundary> | ||
</SitecoreContextReactContext.Provider> | ||
); | ||
|
||
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 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 <div>This is a custom error component!</div>; | ||
}; | ||
|
||
const rendered = mount( | ||
<ErrorBoundary customErrorComponent={CustomErrorComponent}> | ||
<TestErrorComponent /> | ||
</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 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( | ||
<SitecoreContextReactContext.Provider value={testComponentProps}> | ||
<ErrorBoundary rendering={rendering}> | ||
<TestErrorComponent /> | ||
</ErrorBoundary> | ||
</SitecoreContextReactContext.Provider> | ||
); | ||
|
||
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); | ||
}); | ||
|
||
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( | ||
<SitecoreContextReactContext.Provider value={testComponentProps}> | ||
<ErrorBoundary rendering={rendering}> | ||
<TestErrorComponent /> | ||
</ErrorBoundary> | ||
</SitecoreContextReactContext.Provider> | ||
); | ||
|
||
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 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 = () => { | ||
throw Error(errorMessage); | ||
}; | ||
|
||
const CustomErrorComponent: React.FC = () => { | ||
return <div>This is a custom error component!</div>; | ||
}; | ||
|
||
const rendered = mount( | ||
<ErrorBoundary customErrorComponent={CustomErrorComponent}> | ||
<TestErrorComponent /> | ||
</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( | ||
<ErrorBoundary> | ||
<TestErrorComponent /> | ||
</ErrorBoundary> | ||
); | ||
console.log(rendered.html()); | ||
expect(rendered.html()).to.contain('class="sc-jss-placeholder-error"'); | ||
expect(rendered.html()).to.contain( | ||
'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); | ||
}); | ||
}); | ||
}); |
80 changes: 80 additions & 0 deletions
80
packages/sitecore-jss-react/src/components/ErrorBoundary.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
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; | ||
}; | ||
|
||
export type ErrorBoundaryProps = { | ||
children: ReactNode; | ||
sitecoreContext: SitecoreContextValue; | ||
type: string; | ||
customErrorComponent?: | ||
| React.ComponentClass<CustomErrorComponentProps> | ||
| React.FC<CustomErrorComponentProps>; | ||
rendering?: ComponentRendering; | ||
componentLoadingMessage?: string; | ||
}; | ||
|
||
class ErrorBoundary extends React.Component<ErrorBoundaryProps> { | ||
defaultErrorMessage = | ||
'There was a problem loading this section. Refresh your browser to try again.'; // eslint-disable-line | ||
defaultLoadingMessage = 'Loading component...'; | ||
state: { error: Error }; | ||
|
||
constructor(props: ErrorBoundaryProps) { | ||
super(props); | ||
this.state = { error: null }; | ||
} | ||
|
||
static getDerivedStateFromError(error: Error) { | ||
return { error: error }; | ||
} | ||
|
||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { | ||
console.error({ error, errorInfo }); | ||
} | ||
|
||
isInDevMode(): boolean { | ||
return process.env.NODE_ENV === 'development'; | ||
} | ||
|
||
render() { | ||
if (this.state.error) { | ||
if (this.props.customErrorComponent) { | ||
return <this.props.customErrorComponent error={this.state.error} />; | ||
} else { | ||
if (this.isInDevMode() || this.props.sitecoreContext?.pageEditing) { | ||
return ( | ||
<div> | ||
<div className="sc-jss-placeholder-error"> | ||
A rendering error occurred in component{' '} | ||
<em>{this.props.rendering?.componentName}</em> | ||
<br /> | ||
Error: <em>{this.state.error.message}</em> | ||
</div> | ||
</div> | ||
); | ||
} else { | ||
return ( | ||
<div> | ||
<div className="sc-jss-placeholder-error">{this.defaultErrorMessage}</div> | ||
</div> | ||
); | ||
} | ||
} | ||
} | ||
|
||
return ( | ||
<Suspense | ||
fallback={<h4>{this.props.componentLoadingMessage || this.defaultLoadingMessage}</h4>} | ||
> | ||
{this.props.children} | ||
</Suspense> | ||
); | ||
} | ||
} | ||
|
||
export default withSitecoreContext()(ErrorBoundary); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.