mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-13 14:44:43 -04:00
refactor: move motd modal
Signed-off-by: Erik Michelson <github@erik.michelson.eu>
This commit is contained in:
parent
fa819c290a
commit
5b64392a98
8 changed files with 13 additions and 13 deletions
|
@ -1,49 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`motd modal doesn't render a modal if no motd has been fetched 1`] = `
|
||||
<div>
|
||||
<span
|
||||
data-testid="loaded not visible"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`motd modal renders a modal if a motd was fetched and can dismiss it 1`] = `
|
||||
<div>
|
||||
<span>
|
||||
This is a mock implementation of a Modal:
|
||||
<dialog>
|
||||
<div
|
||||
class="modal-body"
|
||||
>
|
||||
<span
|
||||
data-testid="motd-renderer"
|
||||
>
|
||||
This is a mock implementation of a iframe renderer. Props:
|
||||
{"frameClasses":"w-100","rendererType":"simple","markdownContentLines":["very important mock text!"],"adaptFrameHeightToContent":true,"showWaitSpinner":true}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="modal-footer"
|
||||
>
|
||||
<button
|
||||
class="btn btn-success"
|
||||
data-testid="motd-dismiss"
|
||||
type="button"
|
||||
>
|
||||
common.dismiss
|
||||
</button>
|
||||
</div>
|
||||
</dialog>
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`motd modal renders a modal if a motd was fetched and can dismiss it 2`] = `
|
||||
<div>
|
||||
<span>
|
||||
This is a mock implementation of a Modal:
|
||||
Modal is invisible
|
||||
</span>
|
||||
</div>
|
||||
`;
|
|
@ -1,129 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { fetchMotd } from './fetch-motd'
|
||||
import { Mock } from 'ts-mockery'
|
||||
|
||||
describe('fetch motd', () => {
|
||||
const motdUrl = 'public/motd.md'
|
||||
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
})
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
beforeAll(() => {
|
||||
global.fetch = jest.fn()
|
||||
})
|
||||
|
||||
const mockFetch = (
|
||||
responseText: string,
|
||||
lastModified: string | null,
|
||||
etag?: string | null
|
||||
): jest.SpyInstance<Promise<Response>> => {
|
||||
return jest.spyOn(global, 'fetch').mockImplementation((url: RequestInfo | URL) => {
|
||||
if (url !== motdUrl) {
|
||||
return Promise.reject('wrong url')
|
||||
}
|
||||
return Promise.resolve(
|
||||
Mock.of<Response>({
|
||||
headers: Mock.of<Headers>({
|
||||
get: (name: string) => {
|
||||
return name === 'Last-Modified' ? lastModified : name === 'etag' ? etag ?? null : null
|
||||
}
|
||||
}),
|
||||
text: () => Promise.resolve(responseText),
|
||||
status: 200
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const mockFileNotFoundFetch = () => {
|
||||
jest.spyOn(global, 'fetch').mockImplementation(() =>
|
||||
Promise.resolve(
|
||||
Mock.of<Response>({
|
||||
status: 500
|
||||
})
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
describe('date detection', () => {
|
||||
it('will return the last-modified value if available', async () => {
|
||||
mockFetch('mocked motd', 'yesterday-modified', null)
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual({
|
||||
motdText: 'mocked motd',
|
||||
lastModified: 'yesterday-modified'
|
||||
})
|
||||
})
|
||||
it('will return the etag if last-modified is not returned', async () => {
|
||||
mockFetch('mocked motd', null, 'yesterday-etag')
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual({
|
||||
motdText: 'mocked motd',
|
||||
lastModified: 'yesterday-etag'
|
||||
})
|
||||
})
|
||||
it('will prefer the last-modified header over the etag', async () => {
|
||||
mockFetch('mocked motd', 'yesterday-last', 'yesterday-etag')
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual({
|
||||
motdText: 'mocked motd',
|
||||
lastModified: 'yesterday-last'
|
||||
})
|
||||
})
|
||||
it('will return an empty value if neither the last-modified value nor the etag is returned', async () => {
|
||||
mockFetch('mocked motd', null, null)
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual({
|
||||
motdText: 'mocked motd',
|
||||
lastModified: null
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('can fetch a motd if no last modified value has been memorized', async () => {
|
||||
mockFetch('mocked motd', 'yesterday')
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual({
|
||||
motdText: 'mocked motd',
|
||||
lastModified: 'yesterday'
|
||||
})
|
||||
})
|
||||
|
||||
it("can detect that the motd hasn't been updated", async () => {
|
||||
mockFetch('mocked motd', 'yesterday')
|
||||
window.localStorage.setItem('motd.lastModified', 'yesterday')
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual(undefined)
|
||||
})
|
||||
|
||||
it('can detect that the motd has been updated', async () => {
|
||||
mockFetch('mocked motd', 'yesterday')
|
||||
window.localStorage.setItem('motd.lastModified', 'the day before yesterday')
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual({
|
||||
motdText: 'mocked motd',
|
||||
lastModified: 'yesterday'
|
||||
})
|
||||
})
|
||||
|
||||
it("won't fetch a motd if no file was found", async () => {
|
||||
mockFileNotFoundFetch()
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual(undefined)
|
||||
})
|
||||
|
||||
it("won't fetch a motd update if no file was found", async () => {
|
||||
mockFileNotFoundFetch()
|
||||
window.localStorage.setItem('motd.lastModified', 'the day before yesterday')
|
||||
const result = fetchMotd()
|
||||
await expect(result).resolves.toStrictEqual(undefined)
|
||||
})
|
||||
})
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { defaultConfig } from '../../../api/common/default-config'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
|
||||
export const MOTD_LOCAL_STORAGE_KEY = 'motd.lastModified'
|
||||
const log = new Logger('Motd')
|
||||
|
||||
export interface MotdApiResponse {
|
||||
motdText: string
|
||||
lastModified: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the current motd from the backend and sets the content in the global application state.
|
||||
* If the motd hasn't changed since the last time then the global application state won't be changed.
|
||||
* To check if the motd has changed the "last modified" header from the request
|
||||
* will be compared to the saved value from the browser's local storage.
|
||||
* @return A promise that gets resolved if the motd was fetched successfully.
|
||||
*/
|
||||
export const fetchMotd = async (): Promise<MotdApiResponse | undefined> => {
|
||||
const cachedLastModified = window.localStorage.getItem(MOTD_LOCAL_STORAGE_KEY)
|
||||
const motdUrl = `public/motd.md`
|
||||
|
||||
if (cachedLastModified) {
|
||||
const response = await fetch(motdUrl, {
|
||||
...defaultConfig,
|
||||
method: 'HEAD'
|
||||
})
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
}
|
||||
const lastModified = response.headers.get('Last-Modified') || response.headers.get('etag')
|
||||
if (lastModified === cachedLastModified) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
const response = await fetch(motdUrl, {
|
||||
...defaultConfig
|
||||
})
|
||||
|
||||
if (response.status !== 200) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const lastModified = response.headers.get('Last-Modified') || response.headers.get('etag')
|
||||
if (!lastModified) {
|
||||
log.warn("'Last-Modified' or 'Etag' not found for motd.md!")
|
||||
}
|
||||
|
||||
return { motdText: await response.text(), lastModified }
|
||||
}
|
|
@ -1,77 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import * as UseBaseUrlModule from '../../../hooks/common/use-base-url'
|
||||
import { mockI18n } from '../../../test-utils/mock-i18n'
|
||||
import { testId } from '../../../utils/test-id'
|
||||
import type { CommonModalProps } from '../modals/common-modal'
|
||||
import * as CommonModalModule from '../modals/common-modal'
|
||||
import * as RendererIframeModule from '../renderer-iframe/renderer-iframe'
|
||||
import * as fetchMotdModule from './fetch-motd'
|
||||
import { MotdModal } from './motd-modal'
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import React from 'react'
|
||||
|
||||
jest.mock('./fetch-motd')
|
||||
jest.mock('../modals/common-modal')
|
||||
jest.mock('../renderer-iframe/renderer-iframe')
|
||||
jest.mock('../../../hooks/common/use-base-url')
|
||||
|
||||
describe('motd modal', () => {
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(UseBaseUrlModule, 'useBaseUrl').mockImplementation(() => 'https://example.org')
|
||||
await mockI18n()
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
jest.resetAllMocks()
|
||||
jest.resetModules()
|
||||
})
|
||||
|
||||
beforeAll(() => {
|
||||
jest.spyOn(CommonModalModule, 'CommonModal').mockImplementation((({ children, show }) => {
|
||||
return (
|
||||
<span>
|
||||
This is a mock implementation of a Modal: {show ? <dialog>{children}</dialog> : 'Modal is invisible'}
|
||||
</span>
|
||||
)
|
||||
}) as React.FC<PropsWithChildren<CommonModalProps>>)
|
||||
jest.spyOn(RendererIframeModule, 'RendererIframe').mockImplementation((props) => {
|
||||
return (
|
||||
<span {...testId('motd-renderer')}>
|
||||
This is a mock implementation of a iframe renderer. Props: {JSON.stringify(props)}
|
||||
</span>
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
it('renders a modal if a motd was fetched and can dismiss it', async () => {
|
||||
jest.spyOn(fetchMotdModule, 'fetchMotd').mockImplementation(() => {
|
||||
return Promise.resolve({
|
||||
motdText: 'very important mock text!',
|
||||
lastModified: 'yesterday'
|
||||
})
|
||||
})
|
||||
const view = render(<MotdModal></MotdModal>)
|
||||
await screen.findByTestId('motd-renderer')
|
||||
expect(view.container).toMatchSnapshot()
|
||||
|
||||
const button = await screen.findByTestId('motd-dismiss')
|
||||
await act<void>(() => {
|
||||
button.click()
|
||||
})
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
|
||||
it("doesn't render a modal if no motd has been fetched", async () => {
|
||||
jest.spyOn(fetchMotdModule, 'fetchMotd').mockImplementation(() => {
|
||||
return Promise.resolve(undefined)
|
||||
})
|
||||
const view = render(<MotdModal></MotdModal>)
|
||||
await screen.findByTestId('loaded not visible')
|
||||
expect(view.container).toMatchSnapshot()
|
||||
})
|
||||
})
|
|
@ -1,74 +0,0 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: 2023 The HedgeDoc developers (see AUTHORS file)
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
import { cypressId } from '../../../utils/cypress-attribute'
|
||||
import { Logger } from '../../../utils/logger'
|
||||
import { testId } from '../../../utils/test-id'
|
||||
import { EditorToRendererCommunicatorContextProvider } from '../../editor-page/render-context/editor-to-renderer-communicator-context-provider'
|
||||
import { RendererType } from '../../render-page/window-post-message-communicator/rendering-message'
|
||||
import { CommonModal } from '../modals/common-modal'
|
||||
import { RendererIframe } from '../renderer-iframe/renderer-iframe'
|
||||
import { fetchMotd, MOTD_LOCAL_STORAGE_KEY } from './fetch-motd'
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Button, Modal } from 'react-bootstrap'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useAsync } from 'react-use'
|
||||
|
||||
const logger = new Logger('Motd')
|
||||
|
||||
/**
|
||||
* Reads the motd from the global application state and shows it in a modal.
|
||||
* If the modal gets dismissed by the user then the "last modified" identifier will be written into the local storage
|
||||
* to prevent that the motd will be shown again until it gets changed.
|
||||
*/
|
||||
export const MotdModal: React.FC = () => {
|
||||
useTranslation()
|
||||
|
||||
const { error, loading, value } = useAsync(fetchMotd)
|
||||
const [dismissed, setDismissed] = useState(false)
|
||||
|
||||
const lines = useMemo(() => value?.motdText.split('\n'), [value?.motdText])
|
||||
|
||||
const dismiss = useCallback(() => {
|
||||
if (value?.lastModified) {
|
||||
window.localStorage.setItem(MOTD_LOCAL_STORAGE_KEY, value.lastModified)
|
||||
}
|
||||
setDismissed(true)
|
||||
}, [value])
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
logger.error('Error while fetching motd', error)
|
||||
}
|
||||
}, [error])
|
||||
|
||||
if (process.env.NODE_ENV === 'test' && !loading && !value) {
|
||||
return <span {...testId('loaded not visible')}></span>
|
||||
}
|
||||
|
||||
return (
|
||||
<CommonModal
|
||||
show={!!lines && !loading && !error && !dismissed}
|
||||
titleI18nKey={'motd.title'}
|
||||
{...cypressId('motd-modal')}>
|
||||
<Modal.Body>
|
||||
<EditorToRendererCommunicatorContextProvider>
|
||||
<RendererIframe
|
||||
frameClasses={'w-100'}
|
||||
rendererType={RendererType.SIMPLE}
|
||||
markdownContentLines={lines as string[]}
|
||||
adaptFrameHeightToContent={true}
|
||||
showWaitSpinner={true}
|
||||
/>
|
||||
</EditorToRendererCommunicatorContextProvider>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant={'success'} onClick={dismiss} {...testId('motd-dismiss')} {...cypressId('motd-dismiss')}>
|
||||
<Trans i18nKey={'common.dismiss'} />
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</CommonModal>
|
||||
)
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue