mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-14 15:14:56 -04:00
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:
parent
0419113d36
commit
880e542351
19 changed files with 282 additions and 166 deletions
|
@ -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>
|
||||
`;
|
|
@ -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])
|
||||
}
|
|
@ -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()
|
||||
})
|
||||
})
|
|
@ -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>
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue