From bf1881eb547555291508aa2096380f6b4662e587 Mon Sep 17 00:00:00 2001 From: Tilman Vatteroth Date: Sun, 3 Apr 2022 19:31:28 +0200 Subject: [PATCH] Move copy button test from e2e to unit test Signed-off-by: Tilman Vatteroth --- .../integration/highlightedCodeBlock.spec.ts | 17 ---- .../copy-to-clipboard-button.test.tsx.snap | 88 +++++++++++++++++++ .../copy-to-clipboard-button.test.tsx | 69 +++++++++++++++ .../copyable/hooks/use-copy-overlay.tsx | 43 +++++---- 4 files changed, 185 insertions(+), 32 deletions(-) create mode 100644 src/components/common/copyable/copy-to-clipboard-button/__snapshots__/copy-to-clipboard-button.test.tsx.snap create mode 100644 src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.test.tsx diff --git a/cypress/integration/highlightedCodeBlock.spec.ts b/cypress/integration/highlightedCodeBlock.spec.ts index c160b84ce..2353962bf 100644 --- a/cypress/integration/highlightedCodeBlock.spec.ts +++ b/cypress/integration/highlightedCodeBlock.spec.ts @@ -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) => { - 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') - }) }) diff --git a/src/components/common/copyable/copy-to-clipboard-button/__snapshots__/copy-to-clipboard-button.test.tsx.snap b/src/components/common/copyable/copy-to-clipboard-button/__snapshots__/copy-to-clipboard-button.test.tsx.snap new file mode 100644 index 000000000..04d88a7a4 --- /dev/null +++ b/src/components/common/copyable/copy-to-clipboard-button/__snapshots__/copy-to-clipboard-button.test.tsx.snap @@ -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`] = ` +
+ +
+`; + +exports[`Copy to clipboard button show an error text if clipboard api isn't available 2`] = ` +
+ +
+`; + +exports[`Copy to clipboard button shows an error text if writing failed 1`] = ` +
+ +
+`; + +exports[`Copy to clipboard button shows an error text if writing failed 2`] = ` +
+ +
+`; + +exports[`Copy to clipboard button shows an success text if writing succeeded 1`] = ` +
+ +
+`; + +exports[`Copy to clipboard button shows an success text if writing succeeded 2`] = ` +
+ +
+`; diff --git a/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.test.tsx b/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.test.tsx new file mode 100644 index 000000000..2c45a9624 --- /dev/null +++ b/src/components/common/copyable/copy-to-clipboard-button/copy-to-clipboard-button.test.tsx @@ -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 = + const uuidMock = '35a35a31-c259-48c4-b75a-8da99859dcdb' // https://xkcd.com/221/ + const overlayId = `copied_${uuidMock}` + + const mockClipboard = async (copyIsSuccessful: boolean, testFunction: () => Promise) => { + 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)) +}) diff --git a/src/components/common/copyable/hooks/use-copy-overlay.tsx b/src/components/common/copyable/hooks/use-copy-overlay.tsx index 0e9ff7490..b064bc918 100644 --- a/src/components/common/copyable/hooks/use-copy-overlay.tsx +++ b/src/components/common/copyable/hooks/use-copy-overlay.tsx @@ -5,16 +5,23 @@ */ 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 { Trans, useTranslation } from 'react-i18next' import { v4 as uuid } from 'uuid' import { ShowIf } from '../../show-if/show-if' import { Logger } from '../../../../utils/logger' import { isClientSideRendering } from '../../../../utils/is-client-side-rendering' +import { useTimeoutFn } from 'react-use' 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. * @@ -27,48 +34,54 @@ export const useCopyOverlay = ( content: string ): [copyToCliphoard: () => void, overlayElement: ReactElement] => { useTranslation() - const [showCopiedTooltip, setShowCopiedTooltip] = useState(false) - const [error, setError] = useState(false) + const [showState, setShowState] = useState(SHOW_STATE.HIDDEN) const [tooltipId] = useState(() => uuid()) + const [, , reset] = useTimeoutFn(() => setShowState(SHOW_STATE.HIDDEN), 2000) + + useEffect(() => { + if (showState !== SHOW_STATE.HIDDEN) { + reset() + } + }, [reset, showState]) + const copyToClipboard = useCallback(() => { if (!isClientSideRendering()) { + setShowState(SHOW_STATE.ERROR) log.error('Clipboard not available in server side rendering') return } + if (typeof navigator.clipboard === 'undefined') { + setShowState(SHOW_STATE.ERROR) + return + } navigator.clipboard .writeText(content) .then(() => { - setError(false) + setShowState(SHOW_STATE.SUCCESS) }) .catch((error: Error) => { - setError(true) + setShowState(SHOW_STATE.ERROR) log.error('Copy failed', error) }) - .finally(() => { - setShowCopiedTooltip(true) - setTimeout(() => { - setShowCopiedTooltip(false) - }, 2000) - }) }, [content]) const overlayElement = useMemo( () => ( - + {(props) => ( - + - + )} ), - [clickComponent, error, showCopiedTooltip, tooltipId] + [clickComponent, showState, tooltipId] ) return [copyToClipboard, overlayElement]