Move copy button test from e2e to unit test

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-04-03 19:31:28 +02:00
parent aca9856766
commit bf1881eb54
4 changed files with 185 additions and 32 deletions

View file

@ -91,21 +91,4 @@ describe('Code', () => {
}) })
}) })
}) })
it('has a working copy button', () => {
cy.setCodemirrorContent('```javascript \nlet x = 0\n```')
cy.getByCypressId('documentIframe').then((element: JQuery<HTMLElement>) => {
const frame = element.get(0) as HTMLIFrameElement
if (frame === null || frame.contentWindow === null) {
return cy.wrap(null)
}
cy.spy(frame.contentWindow.navigator.clipboard, 'writeText').as('copy')
})
cy.getIframeBody().findByCypressId('copy-code-button').click()
cy.get('@copy').should('be.calledWithExactly', 'let x = 0\n')
})
}) })

View file

@ -0,0 +1,88 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Copy to clipboard button show an error text if clipboard api isn't available 1`] = `
<div>
<button
class="btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode"
type="button"
>
<i
class="fa fa-files-o "
/>
</button>
</div>
`;
exports[`Copy to clipboard button show an error text if clipboard api isn't available 2`] = `
<div>
<button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
class="btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode"
type="button"
>
<i
class="fa fa-files-o "
/>
</button>
</div>
`;
exports[`Copy to clipboard button shows an error text if writing failed 1`] = `
<div>
<button
class="btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode"
type="button"
>
<i
class="fa fa-files-o "
/>
</button>
</div>
`;
exports[`Copy to clipboard button shows an error text if writing failed 2`] = `
<div>
<button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
class="btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode"
type="button"
>
<i
class="fa fa-files-o "
/>
</button>
</div>
`;
exports[`Copy to clipboard button shows an success text if writing succeeded 1`] = `
<div>
<button
class="btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode"
type="button"
>
<i
class="fa fa-files-o "
/>
</button>
</div>
`;
exports[`Copy to clipboard button shows an success text if writing succeeded 2`] = `
<div>
<button
aria-describedby="copied_35a35a31-c259-48c4-b75a-8da99859dcdb"
class="btn btn-dark btn-sm"
title="renderer.highlightCode.copyCode"
type="button"
>
<i
class="fa fa-files-o "
/>
</button>
</div>
`;

View file

@ -0,0 +1,69 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { mockI18n } from '../../../markdown-renderer/test-utils/mock-i18n'
import { CopyToClipboardButton } from './copy-to-clipboard-button'
import { render, screen } from '@testing-library/react'
import * as uuidModule from 'uuid'
jest.mock('uuid')
describe('Copy to clipboard button', () => {
const copyContent = 'Copy McCopy Content. Electric Copyloo'
const copyToClipboardButton = <CopyToClipboardButton content={copyContent} />
const uuidMock = '35a35a31-c259-48c4-b75a-8da99859dcdb' // https://xkcd.com/221/
const overlayId = `copied_${uuidMock}`
const mockClipboard = async (copyIsSuccessful: boolean, testFunction: () => Promise<void>) => {
const originalClipboard = window.navigator.clipboard
const writeTextMock = jest.fn().mockImplementation(() => (copyIsSuccessful ? Promise.resolve() : Promise.reject()))
Object.assign(global.navigator, {
clipboard: {
writeText: writeTextMock
}
})
try {
await testFunction()
expect(writeTextMock).toHaveBeenCalledWith(copyContent)
} finally {
Object.assign(global.navigator, {
clipboard: originalClipboard
})
}
}
const testButton = async (expectSuccess: boolean) => {
const view = render(copyToClipboardButton)
expect(view.container).toMatchSnapshot()
const button = await screen.findByTitle('renderer.highlightCode.copyCode')
button.click()
const tooltip = await screen.findByRole('tooltip')
expect(tooltip).toHaveTextContent(expectSuccess ? 'copyOverlay.success' : 'copyOverlay.error')
expect(tooltip).toHaveAttribute('id', overlayId)
expect(view.container).toMatchSnapshot()
}
beforeAll(async () => {
await mockI18n()
jest.spyOn(uuidModule, 'v4').mockReturnValue(uuidMock)
})
afterAll(() => {
jest.resetAllMocks()
jest.resetModules()
})
it('shows an success text if writing succeeded', () => mockClipboard(true, () => testButton(true)))
it('shows an error text if writing failed', () => mockClipboard(false, () => testButton(false)))
it("show an error text if clipboard api isn't available", () => testButton(false))
})

View file

@ -5,16 +5,23 @@
*/ */
import type { ReactElement, RefObject } from 'react' import type { ReactElement, RefObject } from 'react'
import React, { useCallback, useMemo, useState } from 'react' import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Overlay, Tooltip } from 'react-bootstrap' import { Overlay, Tooltip } from 'react-bootstrap'
import { Trans, useTranslation } from 'react-i18next' import { Trans, useTranslation } from 'react-i18next'
import { v4 as uuid } from 'uuid' import { v4 as uuid } from 'uuid'
import { ShowIf } from '../../show-if/show-if' import { ShowIf } from '../../show-if/show-if'
import { Logger } from '../../../../utils/logger' import { Logger } from '../../../../utils/logger'
import { isClientSideRendering } from '../../../../utils/is-client-side-rendering' import { isClientSideRendering } from '../../../../utils/is-client-side-rendering'
import { useTimeoutFn } from 'react-use'
const log = new Logger('useCopyOverlay') const log = new Logger('useCopyOverlay')
enum SHOW_STATE {
SUCCESS,
ERROR,
HIDDEN
}
/** /**
* Provides a function that writes the given text into the browser clipboard and an {@link Overlay overlay} that is shown when the copy action was successful. * Provides a function that writes the given text into the browser clipboard and an {@link Overlay overlay} that is shown when the copy action was successful.
* *
@ -27,48 +34,54 @@ export const useCopyOverlay = (
content: string content: string
): [copyToCliphoard: () => void, overlayElement: ReactElement] => { ): [copyToCliphoard: () => void, overlayElement: ReactElement] => {
useTranslation() useTranslation()
const [showCopiedTooltip, setShowCopiedTooltip] = useState(false) const [showState, setShowState] = useState<SHOW_STATE>(SHOW_STATE.HIDDEN)
const [error, setError] = useState(false)
const [tooltipId] = useState<string>(() => uuid()) const [tooltipId] = useState<string>(() => uuid())
const [, , reset] = useTimeoutFn(() => setShowState(SHOW_STATE.HIDDEN), 2000)
useEffect(() => {
if (showState !== SHOW_STATE.HIDDEN) {
reset()
}
}, [reset, showState])
const copyToClipboard = useCallback(() => { const copyToClipboard = useCallback(() => {
if (!isClientSideRendering()) { if (!isClientSideRendering()) {
setShowState(SHOW_STATE.ERROR)
log.error('Clipboard not available in server side rendering') log.error('Clipboard not available in server side rendering')
return return
} }
if (typeof navigator.clipboard === 'undefined') {
setShowState(SHOW_STATE.ERROR)
return
}
navigator.clipboard navigator.clipboard
.writeText(content) .writeText(content)
.then(() => { .then(() => {
setError(false) setShowState(SHOW_STATE.SUCCESS)
}) })
.catch((error: Error) => { .catch((error: Error) => {
setError(true) setShowState(SHOW_STATE.ERROR)
log.error('Copy failed', error) log.error('Copy failed', error)
}) })
.finally(() => {
setShowCopiedTooltip(true)
setTimeout(() => {
setShowCopiedTooltip(false)
}, 2000)
})
}, [content]) }, [content])
const overlayElement = useMemo( const overlayElement = useMemo(
() => ( () => (
<Overlay target={clickComponent} show={showCopiedTooltip} placement='top'> <Overlay target={clickComponent} show={showState !== SHOW_STATE.HIDDEN} placement='top'>
{(props) => ( {(props) => (
<Tooltip id={`copied_${tooltipId}`} {...props}> <Tooltip id={`copied_${tooltipId}`} {...props}>
<ShowIf condition={error}> <ShowIf condition={showState === SHOW_STATE.ERROR}>
<Trans i18nKey={'copyOverlay.error'} /> <Trans i18nKey={'copyOverlay.error'} />
</ShowIf> </ShowIf>
<ShowIf condition={!error}> <ShowIf condition={showState === SHOW_STATE.SUCCESS}>
<Trans i18nKey={'copyOverlay.success'} /> <Trans i18nKey={'copyOverlay.success'} />
</ShowIf> </ShowIf>
</Tooltip> </Tooltip>
)} )}
</Overlay> </Overlay>
), ),
[clickComponent, error, showCopiedTooltip, tooltipId] [clickComponent, showState, tooltipId]
) )
return [copyToClipboard, overlayElement] return [copyToClipboard, overlayElement]