Add note loading boundary (#2040)

* Remove redundant equal value

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Add NoteLoadingBoundary to fetch note from API before rendering

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Improve debug message for setHandler

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Add test for boundary

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Use common error page for note loading errors

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Fix tests

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Format code

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Add missing snapshot

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>

* Reformat code

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-05-11 12:47:58 +02:00 committed by GitHub
parent 0419113d36
commit 880e542351
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 282 additions and 166 deletions

View file

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Note loading boundary loads a note 1`] = `
<div>
<span
data-testid="success"
>
success!
</span>
</div>
`;
exports[`Note loading boundary shows an error 1`] = `
<div>
<span
data-testid="CommonErrorPage"
>
This is a mock for CommonErrorPage.
</span>
<span>
titleI18nKey:
noteLoadingBoundary.errorWhileLoadingContent
</span>
<span>
descriptionI18nKey:
CRAAAAASH
</span>
<span>
children:
</span>
</div>
`;

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { useAsync } from 'react-use'
import { getNote } from '../../../../api/notes'
import { setNoteDataFromServer } from '../../../../redux/note-details/methods'
import { useSingleStringUrlParameter } from '../../../../hooks/common/use-single-string-url-parameter'
import type { AsyncState } from 'react-use/lib/useAsyncFn'
/**
* Reads the note id from the current URL, requests the note from the backend and writes it into the global application state.
*
* @return An {@link AsyncState async state} that represents the current state of the loading process.
*/
export const useLoadNoteFromServer = (): AsyncState<void> => {
const id = useSingleStringUrlParameter('noteId', undefined)
return useAsync(async () => {
if (id === undefined) {
throw new Error('Invalid id')
}
const noteFromServer = await getNote(id)
setNoteDataFromServer(noteFromServer)
}, [id])
}

View file

@ -0,0 +1,115 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import * as useSingleStringUrlParameterModule from '../../../hooks/common/use-single-string-url-parameter'
import * as getNoteModule from '../../../api/notes'
import * as setNoteDataFromServerModule from '../../../redux/note-details/methods'
import type { Note } from '../../../api/notes/types'
import { Mock } from 'ts-mockery'
import { render, screen } from '@testing-library/react'
import { NoteLoadingBoundary } from './note-loading-boundary'
import { testId } from '../../../utils/test-id'
import { Fragment } from 'react'
import { mockI18n } from '../../markdown-renderer/test-utils/mock-i18n'
import * as CommonErrorPageModule from '../../error-pages/common-error-page'
import * as LoadingScreenModule from '../../../components/application-loader/loading-screen/loading-screen'
describe('Note loading boundary', () => {
const mockedNoteId = 'mockedNoteId'
afterEach(() => {
jest.resetAllMocks()
jest.resetModules()
})
beforeEach(async () => {
await mockI18n()
jest.spyOn(LoadingScreenModule, 'LoadingScreen').mockImplementation(({ errorMessage }) => {
return (
<Fragment>
<span {...testId('LoadingScreen')}>This is a mock for LoadingScreen.</span>
<span>errorMessage: {errorMessage}</span>
</Fragment>
)
})
jest
.spyOn(CommonErrorPageModule, 'CommonErrorPage')
.mockImplementation(({ titleI18nKey, descriptionI18nKey, children }) => {
return (
<Fragment>
<span {...testId('CommonErrorPage')}>This is a mock for CommonErrorPage.</span>
<span>titleI18nKey: {titleI18nKey}</span>
<span>descriptionI18nKey: {descriptionI18nKey}</span>
<span>children: {children}</span>
</Fragment>
)
})
mockGetNoteIdQueryParameter()
})
const mockGetNoteIdQueryParameter = () => {
const expectedQueryParameter = 'noteId'
jest.spyOn(useSingleStringUrlParameterModule, 'useSingleStringUrlParameter').mockImplementation((parameter) => {
expect(parameter).toBe(expectedQueryParameter)
return mockedNoteId
})
}
const mockGetNoteApiCall = (returnValue: Note) => {
jest.spyOn(getNoteModule, 'getNote').mockImplementation((id) => {
expect(id).toBe(mockedNoteId)
return new Promise((resolve) => {
setTimeout(() => resolve(returnValue), 0)
})
})
}
const mockCrashingNoteApiCall = () => {
jest.spyOn(getNoteModule, 'getNote').mockImplementation((id) => {
expect(id).toBe(mockedNoteId)
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('CRAAAAASH')), 0)
})
})
}
const mockSetNoteInRedux = (expectedNote: Note): jest.SpyInstance<void, [apiResponse: Note]> => {
return jest.spyOn(setNoteDataFromServerModule, 'setNoteDataFromServer').mockImplementation((givenNote) => {
expect(givenNote).toBe(expectedNote)
})
}
it('loads a note', async () => {
const mockedNote: Note = Mock.of<Note>()
mockGetNoteApiCall(mockedNote)
const setNoteInReduxFunctionMock = mockSetNoteInRedux(mockedNote)
const view = render(
<NoteLoadingBoundary>
<span data-testid={'success'}>success!</span>
</NoteLoadingBoundary>
)
await screen.findByTestId('LoadingScreen')
await screen.findByTestId('success')
expect(view.container).toMatchSnapshot()
expect(setNoteInReduxFunctionMock).toBeCalledWith(mockedNote)
})
it('shows an error', async () => {
const mockedNote: Note = Mock.of<Note>()
mockCrashingNoteApiCall()
const setNoteInReduxFunctionMock = mockSetNoteInRedux(mockedNote)
const view = render(
<NoteLoadingBoundary>
<span data-testid={'success'}>success!</span>
</NoteLoadingBoundary>
)
await screen.findByTestId('LoadingScreen')
await screen.findByTestId('CommonErrorPage')
expect(view.container).toMatchSnapshot()
expect(setNoteInReduxFunctionMock).not.toBeCalled()
})
})

View file

@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import type { PropsWithChildren } from 'react'
import React, { Fragment } from 'react'
import { useLoadNoteFromServer } from './hooks/use-load-note-from-server'
import { LoadingScreen } from '../../application-loader/loading-screen/loading-screen'
import { CommonErrorPage } from '../../error-pages/common-error-page'
/**
* Loads the note identified by the note-id in the URL.
* During the loading a {@link LoadingScreen loading screen} will be rendered instead of the child elements.
* The boundary also shows errors that occur during the loading process.
*
* @param children The react elements that will be shown when the loading was successful.
*/
export const NoteLoadingBoundary: React.FC<PropsWithChildren<unknown>> = ({ children }) => {
const { error, loading } = useLoadNoteFromServer()
if (loading) {
return <LoadingScreen />
} else if (error) {
return (
<CommonErrorPage
titleI18nKey={'noteLoadingBoundary.errorWhileLoadingContent'}
descriptionI18nKey={error.message}
/>
)
} else {
return <Fragment>{children}</Fragment>
}
}