mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-26 11:43:59 -05:00
Move copy button test from e2e to unit test
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
aca9856766
commit
bf1881eb54
4 changed files with 185 additions and 32 deletions
|
@ -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')
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -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>
|
||||||
|
`;
|
|
@ -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))
|
||||||
|
})
|
|
@ -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]
|
||||||
|
|
Loading…
Reference in a new issue