fix: Copy to clipboard would seemingly fail even if it worked
This commit is contained in:
committed by
Braden MacDonald
parent
a522c48045
commit
681854209a
@@ -68,13 +68,6 @@ const courseId = '123';
|
||||
|
||||
window.HTMLElement.prototype.scrollIntoView = jest.fn();
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useLocation: jest.fn(),
|
||||
|
||||
@@ -21,13 +21,6 @@ jest.mock('react-router-dom', () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const unit = {
|
||||
id: 'unit-1',
|
||||
};
|
||||
|
||||
@@ -47,13 +47,6 @@ const unit = {
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const renderComponent = (props) => render(
|
||||
<AppProvider store={store}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
|
||||
@@ -93,13 +93,6 @@ jest.mock('react-router-dom', () => ({
|
||||
useNavigate: () => mockedUsedNavigate,
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
/**
|
||||
* Simulates receiving a post message event for testing purposes.
|
||||
* This can be used to mimic events like deletion or other actions
|
||||
|
||||
@@ -22,13 +22,6 @@ let axiosMock;
|
||||
let queryClient;
|
||||
const courseId = '123';
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
global.BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const renderComponent = (props = {}) => render(
|
||||
<AppProvider store={store}>
|
||||
<IntlProvider locale="en">
|
||||
|
||||
@@ -6,7 +6,6 @@ import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { Provider } from 'react-redux';
|
||||
|
||||
import { messageTypes } from '../../../constants';
|
||||
import { mockBroadcastChannel } from '../../../../generic/data/api.mock';
|
||||
import initializeStore from '../../../../store';
|
||||
import { useMessageHandlers } from '..';
|
||||
|
||||
@@ -20,8 +19,6 @@ jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
|
||||
mockBroadcastChannel();
|
||||
|
||||
describe('useMessageHandlers', () => {
|
||||
let handlers;
|
||||
let result;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from '../../../__mocks__';
|
||||
import { initializeMocks, makeWrapper } from '../../../testUtils';
|
||||
import { getClipboardUrl } from '../../data/api';
|
||||
import useClipboard from './useClipboard';
|
||||
import useClipboard, { _testingOverrideBroadcastChannel } from './useClipboard';
|
||||
|
||||
initializeMocks();
|
||||
|
||||
@@ -16,13 +16,14 @@ let mockShowToast: jest.Mock;
|
||||
|
||||
const unitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc';
|
||||
const xblockId = 'block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4';
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
onmessage: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
let broadcastMockListener: (x: unknown) => void | undefined;
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: (message: unknown) => { broadcastMockListener(message); },
|
||||
addEventListener: (_eventName: string, handler: typeof broadcastMockListener) => { broadcastMockListener = handler; },
|
||||
removeEventListener: jest.fn(),
|
||||
};
|
||||
_testingOverrideBroadcastChannel(clipboardBroadcastChannelMock as any);
|
||||
|
||||
describe('useClipboard', () => {
|
||||
beforeEach(async () => {
|
||||
@@ -88,14 +89,14 @@ describe('useClipboard', () => {
|
||||
describe('broadcast channel message handling', () => {
|
||||
it('updates states correctly on receiving a broadcast message', async () => {
|
||||
const { result, rerender } = renderHook(() => useClipboard(true), { wrapper: makeWrapper() });
|
||||
clipboardBroadcastChannelMock.onmessage({ data: clipboardUnit });
|
||||
clipboardBroadcastChannelMock.postMessage({ data: clipboardUnit });
|
||||
|
||||
rerender();
|
||||
|
||||
expect(result.current.showPasteUnit).toBe(true);
|
||||
expect(result.current.showPasteXBlock).toBe(false);
|
||||
|
||||
clipboardBroadcastChannelMock.onmessage({ data: clipboardXBlock });
|
||||
clipboardBroadcastChannelMock.postMessage({ data: clipboardXBlock });
|
||||
rerender();
|
||||
|
||||
expect(result.current.showPasteUnit).toBe(false);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { useCallback, useContext, useEffect } from 'react';
|
||||
|
||||
import { getClipboard, updateClipboard } from '../../data/api';
|
||||
import {
|
||||
@@ -11,6 +11,14 @@ import {
|
||||
import { ToastContext } from '../../toast-context';
|
||||
import messages from './messages';
|
||||
|
||||
// Global, shared broadcast channel for the clipboard. Disabled by default in test environment where it's not defined.
|
||||
let clipboardBroadcastChannel = (
|
||||
typeof BroadcastChannel !== 'undefined' ? new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL) : null
|
||||
);
|
||||
/** To allow mocking the broadcast channel for testing */
|
||||
// eslint-disable-next-line
|
||||
export const _testingOverrideBroadcastChannel = (x: BroadcastChannel) => { clipboardBroadcastChannel = x; };
|
||||
|
||||
/**
|
||||
* Custom React hook for managing clipboard functionality.
|
||||
*
|
||||
@@ -23,7 +31,6 @@ import messages from './messages';
|
||||
*/
|
||||
const useClipboard = (canEdit: boolean = true) => {
|
||||
const intl = useIntl();
|
||||
const [clipboardBroadcastChannel] = useState(() => new BroadcastChannel(STUDIO_CLIPBOARD_CHANNEL));
|
||||
const { data: clipboardData } = useQuery({
|
||||
queryKey: ['clipboard'],
|
||||
queryFn: getClipboard,
|
||||
@@ -33,37 +40,48 @@ const useClipboard = (canEdit: boolean = true) => {
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const copyToClipboard = async (usageKey: string) => {
|
||||
const copyToClipboard = useCallback(async (usageKey: string) => {
|
||||
// This code is synchronous for now, but it could be made asynchronous in the future.
|
||||
// In that case, the `done` message should be shown after the asynchronous operation completes.
|
||||
showToast(intl.formatMessage(messages.copying));
|
||||
let newData;
|
||||
try {
|
||||
const newData = await updateClipboard(usageKey);
|
||||
clipboardBroadcastChannel.postMessage(newData);
|
||||
newData = await updateClipboard(usageKey);
|
||||
queryClient.setQueryData(['clipboard'], newData);
|
||||
showToast(intl.formatMessage(messages.done));
|
||||
} catch (error) {
|
||||
showToast(intl.formatMessage(messages.error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Update the clipboard state across all other open browser tabs too:
|
||||
try {
|
||||
clipboardBroadcastChannel?.postMessage(newData);
|
||||
} catch (error) {
|
||||
// Log the error but no need to show it to the user.
|
||||
// istanbul ignore next
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Unable to sync clipboard state across other open tabs:', error);
|
||||
}
|
||||
showToast(intl.formatMessage(messages.done));
|
||||
}, [showToast, intl, queryClient]);
|
||||
|
||||
const handleBroadcastMessage = useCallback((event: MessageEvent) => {
|
||||
// Note: if this useClipboard() hook is used many times on one page,
|
||||
// this will result in many separate calls to setQueryData() whenever
|
||||
// the clipboard contents change, but that is fine and shouldn't actually
|
||||
// cause any issues. If it did, we could refactor this into a
|
||||
// <ClipboardContextProvider> that manages a single clipboardBroadcastChannel
|
||||
// rather than having a separate channel per useClipboard hook.
|
||||
queryClient.setQueryData(['clipboard'], event.data);
|
||||
}, [queryClient]);
|
||||
|
||||
useEffect(() => {
|
||||
// Handle messages from the broadcast channel
|
||||
clipboardBroadcastChannel.onmessage = (event) => {
|
||||
// Note: if this useClipboard() hook is used many times on one page,
|
||||
// this will result in many separate calls to setQueryData() whenever
|
||||
// the clipboard contents change, but that is fine and shouldn't actually
|
||||
// cause any issues. If it did, we could refactor this into a
|
||||
// <ClipboardContextProvider> that manages a single clipboardBroadcastChannel
|
||||
// rather than having a separate channel per useClipboard hook.
|
||||
queryClient.setQueryData(['clipboard'], event.data);
|
||||
};
|
||||
|
||||
clipboardBroadcastChannel?.addEventListener('message', handleBroadcastMessage);
|
||||
// Cleanup function for the BroadcastChannel when the hook is unmounted
|
||||
return () => {
|
||||
clipboardBroadcastChannel.close();
|
||||
clipboardBroadcastChannel?.removeEventListener('message', handleBroadcastMessage);
|
||||
};
|
||||
}, [clipboardBroadcastChannel]);
|
||||
}, []);
|
||||
|
||||
const isPasteable = canEdit && clipboardData?.content?.status !== CLIPBOARD_STATUS.expired;
|
||||
const showPasteUnit = isPasteable && clipboardData?.content?.blockType === 'vertical';
|
||||
|
||||
@@ -38,12 +38,3 @@ export async function mockClipboardHtml(blockType?: string): Promise<api.Clipboa
|
||||
}
|
||||
mockClipboardHtml.applyMock = (blockType?: string) => jest.spyOn(api, 'getClipboard').mockImplementation(() => mockClipboardHtml(blockType));
|
||||
mockClipboardHtml.applyMockOnce = () => jest.spyOn(api, 'getClipboard').mockImplementationOnce(mockClipboardHtml);
|
||||
|
||||
/** Mock the DOM `BroadcastChannel` API which the clipboard code uses */
|
||||
export function mockBroadcastChannel() {
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import { getConfig } from '@edx/frontend-platform';
|
||||
import { act, renderHook } from '@testing-library/react';
|
||||
import { useKeyedState } from '@edx/react-unit-test-utils';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
import { mockBroadcastChannel } from '../../data/api.mock';
|
||||
import { iframeMessageTypes, iframeStateKeys } from '../../../constants';
|
||||
import { useIframeBehavior } from '../useIframeBehavior';
|
||||
import { useLoadBearingHook } from '../useLoadBearingHook';
|
||||
@@ -18,8 +17,6 @@ jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
|
||||
mockBroadcastChannel();
|
||||
|
||||
describe('useIframeBehavior', () => {
|
||||
const id = 'test-id';
|
||||
const iframeUrl = 'http://example.com';
|
||||
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
import { mockContentSearchConfig } from '../search-manager/data/api.mock';
|
||||
import { studioHomeMock } from '../studio-home/__mocks__';
|
||||
import { getStudioHomeApiUrl } from '../studio-home/data/api';
|
||||
import { mockBroadcastChannel } from '../generic/data/api.mock';
|
||||
import { LibraryLayout } from '.';
|
||||
import { getLibraryCollectionsApiUrl, getLibraryContainersApiUrl } from './data/api';
|
||||
|
||||
@@ -34,7 +33,6 @@ mockContentSearchConfig.applyMock();
|
||||
mockContentLibrary.applyMock();
|
||||
mockGetLibraryTeam.applyMock();
|
||||
mockXBlockFields.applyMock();
|
||||
mockBroadcastChannel();
|
||||
|
||||
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
|
||||
|
||||
|
||||
@@ -47,13 +47,6 @@ jest.mock('../search-manager', () => ({
|
||||
useSearchContext: () => mockUseSearchContext(),
|
||||
}));
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const withLibraryId = (libraryId: string) => ({
|
||||
extraWrapper: ({ children }: { children: React.ReactNode }) => (
|
||||
<LibraryProvider libraryId={libraryId}>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
getContentLibraryApiUrl, getCreateLibraryBlockUrl, getLibraryCollectionComponentApiUrl, getLibraryPasteClipboardUrl,
|
||||
getXBlockFieldsApiUrl, getLibraryContainerChildrenApiUrl,
|
||||
} from '../data/api';
|
||||
import { mockBroadcastChannel, mockClipboardEmpty, mockClipboardHtml } from '../../generic/data/api.mock';
|
||||
import { mockClipboardEmpty, mockClipboardHtml } from '../../generic/data/api.mock';
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
import AddContent from './AddContent';
|
||||
import { ComponentEditorModal } from '../components/ComponentEditorModal';
|
||||
@@ -23,7 +23,6 @@ import editorCmsApi from '../../editors/data/services/cms/api';
|
||||
import { ToastActionData } from '../../generic/toast-context';
|
||||
import * as textEditorHooks from '../../editors/containers/TextEditor/hooks';
|
||||
|
||||
mockBroadcastChannel();
|
||||
// mockCreateLibraryBlock.applyMock();
|
||||
|
||||
// Mocks for ComponentEditorModal to work in tests.
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
mockCreateLibraryBlock,
|
||||
mockXBlockFields,
|
||||
} from '../data/api.mocks';
|
||||
import { mockBroadcastChannel, mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import { mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import { mockContentSearchConfig, mockSearchResult } from '../../search-manager/data/api.mock';
|
||||
import { studioHomeMock } from '../../studio-home/__mocks__';
|
||||
import { getStudioHomeApiUrl } from '../../studio-home/data/api';
|
||||
@@ -25,7 +25,6 @@ import LibraryLayout from '../LibraryLayout';
|
||||
|
||||
mockContentSearchConfig.applyMock();
|
||||
mockClipboardEmpty.applyMock();
|
||||
mockBroadcastChannel();
|
||||
mockContentLibrary.applyMock();
|
||||
mockCreateLibraryBlock.applyMock();
|
||||
mockSearchResult(mockResult);
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
mockGetCollectionMetadata,
|
||||
} from '../data/api.mocks';
|
||||
import { mockContentSearchConfig, mockGetBlockTypes } from '../../search-manager/data/api.mock';
|
||||
import { mockBroadcastChannel, mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import { mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import { LibraryLayout } from '..';
|
||||
import { ContentTagsDrawer } from '../../content-tags-drawer';
|
||||
import { getLibraryCollectionComponentApiUrl } from '../data/api';
|
||||
@@ -31,7 +31,6 @@ mockContentSearchConfig.applyMock();
|
||||
mockGetBlockTypes.applyMock();
|
||||
mockContentLibrary.applyMock();
|
||||
mockXBlockFields.applyMock();
|
||||
mockBroadcastChannel();
|
||||
|
||||
const searchEndpoint = 'http://mock.meilisearch.local/multi-search';
|
||||
const path = '/library/:libraryId/*';
|
||||
|
||||
@@ -10,14 +10,12 @@ import {
|
||||
mockGetUnpaginatedEntityLinks,
|
||||
} from '../data/api.mocks';
|
||||
import { mockContentSearchConfig, mockFetchIndexDocuments } from '../../search-manager/data/api.mock';
|
||||
import { mockBroadcastChannel } from '../../generic/data/api.mock';
|
||||
import { LibraryProvider } from '../common/context/LibraryContext';
|
||||
import { SidebarBodyComponentId, SidebarProvider } from '../common/context/SidebarContext';
|
||||
import ComponentInfo from './ComponentInfo';
|
||||
import { getXBlockPublishApiUrl } from '../data/api';
|
||||
|
||||
mockContentSearchConfig.applyMock();
|
||||
mockBroadcastChannel();
|
||||
mockContentLibrary.applyMock();
|
||||
mockLibraryBlockMetadata.applyMock();
|
||||
mockGetUnpaginatedEntityLinks.applyMock();
|
||||
|
||||
@@ -39,13 +39,6 @@ const contentHit: ContentHit = {
|
||||
publishStatus: PublishStatus.Published,
|
||||
};
|
||||
|
||||
const clipboardBroadcastChannelMock = {
|
||||
postMessage: jest.fn(),
|
||||
close: jest.fn(),
|
||||
};
|
||||
|
||||
(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);
|
||||
|
||||
const libraryId = 'lib:org1:Demo_Course';
|
||||
const render = () => baseRender(<ComponentCard hit={contentHit} />, {
|
||||
extraWrapper: ({ children }) => (
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
mockLibraryBlockMetadata,
|
||||
} from '../data/api.mocks';
|
||||
import { mockContentSearchConfig, mockGetBlockTypes } from '../../search-manager/data/api.mock';
|
||||
import { mockBroadcastChannel, mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import { mockClipboardEmpty } from '../../generic/data/api.mock';
|
||||
import LibraryLayout from '../LibraryLayout';
|
||||
|
||||
const path = '/library/:libraryId/*';
|
||||
@@ -28,7 +28,6 @@ mockGetBlockTypes.applyMock();
|
||||
mockContentLibrary.applyMock();
|
||||
mockXBlockFields.applyMock();
|
||||
mockLibraryBlockMetadata.applyMock();
|
||||
mockBroadcastChannel();
|
||||
|
||||
describe('<LibraryUnitPage />', () => {
|
||||
beforeEach(() => {
|
||||
|
||||
Reference in New Issue
Block a user