diff --git a/client/package.json b/client/package.json index 9c22801..a4664a9 100644 --- a/client/package.json +++ b/client/package.json @@ -15,7 +15,6 @@ "dependencies": { "bootstrap": "^4.6.2", "classnames": "^2.3.2", - "clipboard": "^2.0.11", "jquery": "3", "moment": "^2.29.4", "nanoid": "^4.0.0", @@ -29,6 +28,7 @@ "react-router": "^6.4.4", "react-router-dom": "^6.4.4", "react-simple-dropdown": "^3.2.3", + "react-tooltip": "^5.2.0", "redux": "^4.2.0", "redux-thunk": "^2.4.2", "sanitize-html": "^2.7.3", diff --git a/client/src/components/Home/__snapshots__/index.test.jsx.snap b/client/src/components/Home/__snapshots__/index.test.jsx.snap index 1128732..3762e12 100644 --- a/client/src/components/Home/__snapshots__/index.test.jsx.snap +++ b/client/src/components/Home/__snapshots__/index.test.jsx.snap @@ -62,10 +62,7 @@ exports[`Connected Home component > should display 1`] = ` /> / @@ -74,9 +71,8 @@ exports[`Connected Home component > should display 1`] = ` > should display 1`] = ` > { }; }); -vi.useFakeTimers(); +const mockClipboardWriteTest = vi.fn(); const mockTranslations = { newRoomButton: 'new room', @@ -43,6 +43,8 @@ const mockTranslations = { aboutButton: 'about', }; +vi.useFakeTimers(); + test('Nav component is displaying', async () => { const { asFragment } = render( { ); expect(asFragment()).toMatchSnapshot(); - - expect(mock$).toHaveBeenCalledWith('.room-id'); - expect(mock$).toHaveBeenLastCalledWith('.lock-room'); - expect(mockTooltip).toHaveBeenLastCalledWith({ trigger: 'manual' }); }); test('Nav component is displaying with another configuration and can rerender', async () => { @@ -103,11 +101,11 @@ test('Nav component is displaying with another configuration and can rerender', }); test('Can copy room url', async () => { - document.execCommand = vi.fn(() => true); + navigator.clipboard = { writeText: mockClipboardWriteTest }; const toggleLockRoom = vi.fn(); - const { getByText } = render( + const { getByText, queryByText } = render( { toggleLockRoom={toggleLockRoom} openModal={() => {}} iAmOwner={false} - translations={{}} + translations={{ copyButtonTooltip: 'Copied' }} />, ); - fireEvent.click(getByText(`/testRoom`)); + await act(async () => { + await fireEvent.click(getByText('/testRoom')); + }); - expect(document.execCommand).toHaveBeenLastCalledWith('copy'); - expect(mock$).toHaveBeenCalledTimes(12); - expect(mockTooltip).toHaveBeenLastCalledWith('show'); + expect(mockClipboardWriteTest).toHaveBeenLastCalledWith('http://localhost:3000/testRoom'); + + await getByText('Copied'); // Wait tooltip closing - vi.runAllTimers(); + await act(() => vi.runAllTimers()); - expect(mock$).toHaveBeenCalledTimes(15); - expect(mockTooltip).toHaveBeenLastCalledWith('hide'); + expect(queryByText('Copied')).not.toBeInTheDocument(); }); test('Can lock/unlock room is room owner only', async () => { const toggleLockRoom = vi.fn(); - const { rerender, getByTitle } = render( + const { rerender, getByTestId, getByText, queryByText } = render( { />, ); - const toggleLockRoomButton = getByTitle('You must be the owner to lock or unlock the room'); + const toggleLockRoomButton = getByTestId('lock-room-button'); - fireEvent.click(toggleLockRoomButton); + await fireEvent.click(toggleLockRoomButton); expect(toggleLockRoom).toHaveBeenCalledWith(); - fireEvent.click(toggleLockRoomButton); + await fireEvent.click(toggleLockRoomButton); expect(toggleLockRoom).toHaveBeenCalledTimes(2); @@ -182,11 +181,16 @@ test('Can lock/unlock room is room owner only', async () => { />, ); - fireEvent.click(toggleLockRoomButton); + await fireEvent.click(toggleLockRoomButton); expect(toggleLockRoom).toHaveBeenCalledTimes(2); - expect(mock$).toHaveBeenLastCalledWith('.lock-room'); - expect(mockTooltip).toHaveBeenLastCalledWith('show'); + + await getByText('You must be the owner to lock or unlock the room'); + + // Wait tooltip closing + await act(() => vi.runAllTimers()); + + expect(queryByText('You must be the owner to lock or unlock the room')).not.toBeInTheDocument(); }); test('Can show user list', async () => { @@ -226,8 +230,10 @@ test('Can show user list', async () => { translations={{}} />, ); + await waitFor(() => expect(getByText('alan')).toBeInTheDocument()); await waitFor(() => expect(getByText('dan')).toBeInTheDocument()); + expect(queryByTitle('Owner')).not.toBeInTheDocument(); expect(queryByTitle('Me')).not.toBeInTheDocument(); }); diff --git a/client/src/components/Nav/__snapshots__/Nav.test.jsx.snap b/client/src/components/Nav/__snapshots__/Nav.test.jsx.snap index 000d2a9..43fce07 100644 --- a/client/src/components/Nav/__snapshots__/Nav.test.jsx.snap +++ b/client/src/components/Nav/__snapshots__/Nav.test.jsx.snap @@ -15,9 +15,7 @@ exports[`Nav component is displaying 1`] = ` /> /testRoom @@ -26,9 +24,8 @@ exports[`Nav component is displaying 1`] = ` > /testRoom_2 @@ -268,9 +263,8 @@ exports[`Nav component is displaying with another configuration and can rerender > /testRoom_3 @@ -556,9 +546,8 @@ exports[`Nav component is displaying with another configuration and can rerender > { + const [showCopyTooltip, setShowCopyTooltip] = React.useState(false); + const [showLockedTooltip, setShowLockedTooltip] = React.useState(false); + const mountedRef = React.useRef(true); + const roomUrl = `${window.location.origin}/${roomId}`; + React.useEffect(() => { - const clip = new Clipboard('.clipboard-trigger'); - - clip.on('success', () => { - $('.room-id').tooltip('show'); - setTimeout(() => { - $('.room-id').tooltip('hide'); - }, 3000); - }); - - $(() => { - $('.room-id').tooltip({ - trigger: 'manual', - }); - $('.lock-room').tooltip({ - trigger: 'manual', - }); - }); + mountedRef.current = true; + return () => { + mountedRef.current = false; + }; }, []); const newRoom = () => { @@ -47,39 +39,67 @@ const Nav = ({ members, roomId, userId, roomLocked, toggleLockRoom, openModal, i const handleToggleLock = () => { if (!iAmOwner) { - $('.lock-room').tooltip('show'); - setTimeout(() => $('.lock-room').tooltip('hide'), 3000); + setShowLockedTooltip(true); + setTimeout(() => { + if (mountedRef.current) { + setShowLockedTooltip(false); + } + }, 2000); return; } toggleLockRoom(); }; + const handleCopy = async () => { + await navigator.clipboard.writeText(roomUrl); + setShowCopyTooltip(true); + setTimeout(() => { + if (mountedRef.current) { + setShowCopyTooltip(false); + } + }, 2000); + }; + return ( {`/${roomId}`} - + {showCopyTooltip && ( + + )} {roomLocked && } {!roomLocked && } + {showLockedTooltip && ( + + )} @@ -96,13 +116,13 @@ const Nav = ({ members, roomId, userId, roomLocked, toggleLockRoom, openModal, i {member.id === userId && ( - + )} {member.isOwner && ( - - + + )} diff --git a/client/src/components/RoomLink/RoomLink.test.jsx b/client/src/components/RoomLink/RoomLink.test.jsx index f7a6b02..bd44c83 100644 --- a/client/src/components/RoomLink/RoomLink.test.jsx +++ b/client/src/components/RoomLink/RoomLink.test.jsx @@ -1,8 +1,7 @@ -import React from 'react'; import { render, fireEvent } from '@testing-library/react'; import RoomLink from '.'; -import mock$ from 'jquery'; -import { describe, it, expect, vi, afterEach } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; +import { act } from 'react-dom/test-utils'; const mockTooltip = vi.fn().mockImplementation(param => {}); @@ -26,43 +25,31 @@ const mockTranslations = { }; describe('RoomLink', () => { - afterEach(() => { - mock$.mockClear(); - }); - it('should display', async () => { - const { asFragment, unmount } = render(); + const { asFragment } = render(); expect(asFragment()).toMatchSnapshot(); - - expect(mock$).toHaveBeenLastCalledWith('.copy-room'); - expect(mockTooltip).toHaveBeenLastCalledWith({ trigger: 'manual' }); - mock$.mockClear(); - - unmount(); - - expect(mock$).toHaveBeenLastCalledWith('.copy-room'); - expect(mockTooltip).toHaveBeenLastCalledWith('hide'); }); it('should copy link', async () => { - // Mock execCommand for paste - document.execCommand = vi.fn(() => true); + const mockClipboardWriteTest = vi.fn(); + navigator.clipboard = { writeText: mockClipboardWriteTest }; - const { getByTitle } = render(); + const { getByTestId, queryByText, getByText } = render( + , + ); - await fireEvent.click(getByTitle(mockTranslations.copyButtonTooltip)); + await act(async () => { + await fireEvent.click(getByTestId('copy-room-button')); + }); - expect(document.execCommand).toHaveBeenLastCalledWith('copy'); - expect(mock$).toHaveBeenCalledTimes(4); - expect(mock$).toHaveBeenLastCalledWith('.copy-room'); - expect(mockTooltip).toHaveBeenLastCalledWith('show'); + expect(mockClipboardWriteTest).toHaveBeenLastCalledWith('http://localhost:3000/roomId'); - // Wait for tooltip to close - vi.runAllTimers(); + await getByText(mockTranslations.copyButtonTooltip); - expect(mock$).toHaveBeenCalledTimes(6); - expect(mock$).toHaveBeenLastCalledWith('.copy-room'); - expect(mockTooltip).toHaveBeenLastCalledWith('hide'); + // Wait tooltip closing + await act(() => vi.runAllTimers()); + + expect(queryByText('Copied')).not.toBeInTheDocument(); }); }); diff --git a/client/src/components/RoomLink/__snapshots__/RoomLink.test.jsx.snap b/client/src/components/RoomLink/__snapshots__/RoomLink.test.jsx.snap index 09d8e08..0fd8521 100644 --- a/client/src/components/RoomLink/__snapshots__/RoomLink.test.jsx.snap +++ b/client/src/components/RoomLink/__snapshots__/RoomLink.test.jsx.snap @@ -21,10 +21,8 @@ exports[`RoomLink > should display 1`] = ` > { + const [showTooltip, setShowTooltip] = React.useState(false); + const mountedRef = React.useRef(true); + const roomUrl = `${window.location.origin}/${roomId}`; React.useEffect(() => { - const clip = new Clipboard('.copy-room'); - - clip.on('success', () => { - $('.copy-room').tooltip('show'); - setTimeout(() => { - $('.copy-room').tooltip('hide'); - }, 3000); - }); - - $(() => { - $('.copy-room').tooltip({ - trigger: 'manual', - }); - }); - + mountedRef.current = true; return () => { - if ($('.copy-room').tooltip) $('.copy-room').tooltip('hide'); + mountedRef.current = false; }; }, []); + const handleClick = async () => { + await navigator.clipboard.writeText(roomUrl); + setShowTooltip(true); + setTimeout(() => { + if (mountedRef.current) { + setShowTooltip(false); + } + }, 2000); + }; + return ( @@ -35,16 +33,24 @@ const RoomLink = ({ roomId, translations }) => { + {showTooltip && ( + + )} diff --git a/client/src/main.tsx b/client/src/main.tsx index 04b3806..f07db91 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -8,6 +8,7 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import 'react-simple-dropdown/styles/Dropdown.css'; import './stylesheets/app.sass'; import 'bootstrap/dist/js/bootstrap.bundle.min.js'; +import 'react-tooltip/dist/react-tooltip.css'; import configureStore from '@/store/'; import Home from '@/components/Home/'; diff --git a/client/yarn.lock b/client/yarn.lock index bb6c762..f74e0c6 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -345,6 +345,18 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@floating-ui/core@^1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.4.tgz#03066eaea8e9b2a2cd3f5aaa60f1e0f580ebe88e" + integrity sha512-FPFLbg2b06MIw1dqk2SOEMAMX3xlrreGjcui5OTxfBDtaKTmh0kioOVjT8gcfl58juawL/yF+S+gnq8aUYQx/Q== + +"@floating-ui/dom@^1.0.4": + version "1.0.12" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.12.tgz#07c870a05d9b825a6d7657524f48fe6761722800" + integrity sha512-HeG/wHoa2laUHlDX3xkzqlUqliAfa+zqV04LaKIwNCmCNaW2p0fQi4/Kd0LB4GdFoJ2UllLFq5gWnXAd67lg7w== + dependencies: + "@floating-ui/core" "^1.0.4" + "@humanwhocodes/config-array@^0.11.6": version "0.11.7" resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.7.tgz#38aec044c6c828f6ed51d5d7ae3d9b9faf6dbb0f" @@ -1083,15 +1095,6 @@ classnames@^2.1.2, classnames@^2.3.2: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== -clipboard@^2.0.11: - version "2.0.11" - resolved "https://registry.yarnpkg.com/clipboard/-/clipboard-2.0.11.tgz#62180360b97dd668b6b3a84ec226975762a70be5" - integrity sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw== - dependencies: - good-listener "^1.2.2" - select "^1.1.2" - tiny-emitter "^2.0.0" - color-convert@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" @@ -1248,11 +1251,6 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== -delegate@^3.1.2: - version "3.2.0" - resolved "https://registry.yarnpkg.com/delegate/-/delegate-3.2.0.tgz#b66b71c3158522e8ab5744f720d8ca0c2af59166" - integrity sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw== - diff-sequences@^29.3.1: version "29.3.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.3.1.tgz#104b5b95fe725932421a9c6e5b4bef84c3f2249e" @@ -1846,13 +1844,6 @@ globby@^11.1.0: merge2 "^1.4.1" slash "^3.0.0" -good-listener@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/good-listener/-/good-listener-1.2.2.tgz#d53b30cdf9313dffb7dc9a0d477096aa6d145c50" - integrity sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw== - dependencies: - delegate "^3.1.2" - gopd@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" @@ -2962,6 +2953,14 @@ react-simple-dropdown@^3.2.3: classnames "^2.1.2" prop-types "^15.5.8" +react-tooltip@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/react-tooltip/-/react-tooltip-5.2.0.tgz#e10e7de2385e8fe6bf3438739c574558b455de3b" + integrity sha512-EH6XIg2MDbMTEElSAZQVXMVeFoOhTgQuea2or0iwyzsr9v8rJf3ImMhOtq7Xe/BPlougxC+PmOibazodLdaRoA== + dependencies: + "@floating-ui/dom" "^1.0.4" + classnames "^2.3.2" + react@^18.2.0: version "18.2.0" resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" @@ -3118,11 +3117,6 @@ scheduler@^0.23.0: dependencies: loose-envify "^1.1.0" -select@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/select/-/select-1.1.2.tgz#0e7350acdec80b1108528786ec1d4418d11b396d" - integrity sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA== - semver@^6.0.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" @@ -3299,11 +3293,6 @@ text-table@^0.2.0: resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== -tiny-emitter@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/tiny-emitter/-/tiny-emitter-2.1.0.tgz#1d1a56edfc51c43e863cbb5382a72330e3555423" - integrity sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q== - tinybench@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.3.1.tgz#14f64e6b77d7ef0b1f6ab850c7a808c6760b414d"