-
Notifications
You must be signed in to change notification settings - Fork 36
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(hooks): Add useClientErrors from cozy-client
- Loading branch information
Showing
5 changed files
with
264 additions
and
383 deletions.
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
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,140 @@ | ||
import filter from 'lodash/filter' | ||
import React, { useCallback, useState, useEffect } from 'react' | ||
|
||
import { useClient } from 'cozy-client' | ||
import { FetchError } from 'cozy-stack-client' | ||
|
||
import QuotaAlert from '../deprecated/QuotaAlert' | ||
|
||
/** | ||
* Rendering functions for client fetch errors by status code | ||
* | ||
* @private | ||
* @type {object} | ||
*/ | ||
const byHttpStatus = { | ||
413: QuotaError // <QuotaError /> | ||
} | ||
|
||
/** | ||
* Display for a quota error from the client | ||
* | ||
* @see QuotaAlert | ||
* @private | ||
* @param {object} props - Props | ||
* @param {Function} props.dismiss - remove the error from the stack to display | ||
* @returns {React.ReactElement} | ||
*/ | ||
function QuotaError({ dismiss }) { | ||
return <QuotaAlert onClose={dismiss} /> | ||
} | ||
|
||
/** | ||
* Returns the handler for an error | ||
* | ||
* @param {import("../types").ClientError} error - The error | ||
* @returns {Function|null} React Component | ||
*/ | ||
function getErrorComponent(error) { | ||
if (error instanceof Response || error instanceof FetchError) { | ||
const status = error.status || '' | ||
return byHttpStatus[status] || null | ||
} | ||
return null | ||
} | ||
|
||
/** | ||
* Renders a stack of errors | ||
* | ||
* @private | ||
* @see ClientErrors | ||
* @param {import("../types").ClientError[]} errorStack - array of errors/exceptions | ||
* @param {Function} setErrorStack - mutates the array of errors | ||
* @returns {Array<React.ReactElement>} React rendering | ||
*/ | ||
function renderErrors(errorStack, setErrorStack) { | ||
const errors = errorStack.map((error, key) => { | ||
const Component = getErrorComponent(error) | ||
if (Component) { | ||
const dismiss = () => | ||
setErrorStack(stack => filter(stack, e => e !== error)) | ||
return <Component key={key} error={error} dismiss={dismiss} /> | ||
} else { | ||
return null | ||
} | ||
}) | ||
return errors | ||
} | ||
|
||
/** | ||
* Manages the client errors and allows to display them | ||
* | ||
* Returns a `ClientErrors` React component that takes care | ||
* of how to display cozy-client errors (probably displaying a modal) | ||
* | ||
* Only Quota Errors are managed for now. | ||
* | ||
* @example | ||
* ``` | ||
* const App = () => { | ||
* const { ClientErrors } = useClientErrors() | ||
* | ||
* return <Layout> | ||
* <h1>My app</h1> | ||
* <ClientErrors /> | ||
* </Layout> | ||
* } | ||
* ``` | ||
* | ||
* @param {object} [props] - Props | ||
* @param {boolean} [props.handleExceptions] - should cozy-client directly handle errors before forwarding them to the caller? | ||
* @returns {{ClientErrors: Function}} React component | ||
*/ | ||
export default function useClientErrors({ handleExceptions = true } = {}) { | ||
const client = useClient() | ||
const [errorStack, setErrorStack] = useState([]) | ||
|
||
/** | ||
* Handle client errors, add them to the error stack | ||
* | ||
* @param {import("../types").ClientError} error - | ||
* @returns {boolean} true if the error was manager, false otherwise | ||
*/ | ||
const handleError = useCallback( | ||
error => { | ||
// `isErrorManaged` is there to avoid the same error to be added twice | ||
// once the error has been added once, the `isErrorManaged`is set to true | ||
// and any future push is ignored | ||
if (error.isErrorManaged) return false | ||
|
||
const isManageable = !!getErrorComponent(error) | ||
if (isManageable) { | ||
error.isErrorManaged = true | ||
setErrorStack(stack => stack.concat(error)) | ||
return true | ||
} else { | ||
error.isErrorManaged = false | ||
return false | ||
} | ||
}, | ||
[setErrorStack] | ||
) | ||
|
||
useEffect(() => { | ||
if (handleExceptions) { | ||
client.on('error', handleError) | ||
return () => client.removeListener('error', handleError) | ||
} else { | ||
return undefined | ||
} | ||
}, [client, handleError, handleExceptions]) | ||
|
||
// @ts-ignore | ||
const ClientErrors = useCallback( | ||
() => renderErrors(errorStack, setErrorStack), | ||
[errorStack, setErrorStack] | ||
) | ||
// @ts-ignore | ||
ClientErrors.displayName = 'ClientErrors' | ||
return { ClientErrors } | ||
} |
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,102 @@ | ||
import { renderHook, act } from '@testing-library/react-hooks' | ||
import { shallow } from 'enzyme' | ||
import React from 'react' | ||
|
||
import { CozyProvider } from 'cozy-client' | ||
import { FetchError } from 'cozy-stack-client' | ||
|
||
import useClientErrors from './useClientErrors' | ||
|
||
function createCozyClient() { | ||
return { | ||
on: jest.fn(), | ||
removeListener: jest.fn() | ||
} | ||
} | ||
|
||
function createWrapper(client = createCozyClient()) { | ||
function Wrapper({ children }) { | ||
return <CozyProvider client={client}>{children}</CozyProvider> | ||
} | ||
return Wrapper | ||
} | ||
|
||
function renderWrappedHook(client) { | ||
const wrapper = createWrapper(client) | ||
return renderHook(() => useClientErrors(), { wrapper }) | ||
} | ||
|
||
function wrappedShallow(tree, client) { | ||
return shallow(tree, { wrappingComponent: createWrapper(client) }) | ||
} | ||
|
||
describe('useClientErrors', () => { | ||
it('registers an `error` handler in client', done => { | ||
const client = createCozyClient() | ||
client.on.mockImplementation(name => name === 'error' && done()) | ||
renderWrappedHook(client) | ||
}) | ||
|
||
describe('renderErrors', () => { | ||
it('returns a function', () => { | ||
const { result } = renderWrappedHook() | ||
const { ClientErrors } = result.current | ||
expect(ClientErrors).toBeInstanceOf(Function) | ||
}) | ||
|
||
it('displays nothing by default', () => { | ||
const { result } = renderWrappedHook() | ||
const { ClientErrors } = result.current | ||
const node = wrappedShallow(<ClientErrors />) | ||
expect(node).toHaveLength(0) | ||
}) | ||
|
||
describe('for quota errors', () => { | ||
const findQuotaAlert = node => { | ||
return node.at(0).dive() | ||
} | ||
const setup = async () => { | ||
const client = createCozyClient() | ||
const response = new Response(null, { | ||
status: 413, | ||
statusText: 'Quota exceeded' | ||
}) | ||
const error = new FetchError( | ||
response, | ||
`${response.status} ${response.statusText}` | ||
) | ||
|
||
const { result, waitForNextUpdate } = renderWrappedHook(client) | ||
const nextUpdate = waitForNextUpdate() | ||
|
||
act(() => { | ||
const handler = client.on.mock.calls[0][1] | ||
handler(error) | ||
}) | ||
|
||
await nextUpdate | ||
const { ClientErrors } = result.current | ||
const node = wrappedShallow(<ClientErrors />, client) | ||
return { node, result, client } | ||
} | ||
|
||
it('displays a a QuotaAlert', async () => { | ||
const { node } = await setup() | ||
expect(node).toHaveLength(1) | ||
expect(findQuotaAlert(node).type().name).toEqual('QuotaAlert') | ||
}) | ||
|
||
it('can be dismissed', async () => { | ||
const { node, result, client } = await setup() | ||
const quotaAlert = findQuotaAlert(node) | ||
const onClose = quotaAlert.props().onClose | ||
act(() => onClose()) | ||
|
||
// re-render ClientErrors returned by the hook | ||
const { ClientErrors } = result.current | ||
const updatedNode = wrappedShallow(<ClientErrors />, client) | ||
expect(updatedNode.at(0).length).toBe(0) | ||
}) | ||
}) | ||
}) | ||
}) |
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.