Skip to content

Commit

Permalink
test: simplify the AddContentContainer tests
Browse files Browse the repository at this point in the history
  • Loading branch information
bradenmacdonald committed Sep 11, 2024
1 parent 19fb9e2 commit 7fddc77
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 93 deletions.
40 changes: 40 additions & 0 deletions src/generic/data/api.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* istanbul ignore file */
import * as api from './api';

/**
* Mock for `getClipboard()` that simulates an empty clipboard
*/
export async function mockClipboardEmpty(): Promise<api.ClipboardStatus> {
return {
content: null,
sourceUsageKey: '',
sourceContextTitle: '',
sourceEditUrl: '',
};
}
mockClipboardEmpty.applyMock = () => jest.spyOn(api, 'getClipboard').mockImplementation(mockClipboardEmpty);
mockClipboardEmpty.applyMockOnce = () => jest.spyOn(api, 'getClipboard').mockImplementationOnce(mockClipboardEmpty);

/**
* Mock for `getClipboard()` that simulates a copied HTML component
*/
export async function mockClipboardHtml(): Promise<api.ClipboardStatus> {
return {
content: {
id: 69,
userId: 3,
created: '2024-01-16T13:33:21.314439Z',
purpose: 'clipboard',
status: 'ready',
blockType: 'html',
blockTypeDisplay: 'Text',
olxUrl: 'http://localhost:18010/api/content-staging/v1/staged-content/69/olx',
displayName: 'Blank HTML Page',
},
sourceUsageKey: 'block-v1:edX+DemoX+Demo_Course+type@html+block@html1',
sourceContextTitle: 'Demonstration Course',
sourceEditUrl: 'http://localhost:18010/container/block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical1',
};
}
mockClipboardHtml.applyMock = () => jest.spyOn(api, 'getClipboard').mockImplementation(mockClipboardHtml);
mockClipboardHtml.applyMockOnce = () => jest.spyOn(api, 'getClipboard').mockImplementationOnce(mockClipboardHtml);
21 changes: 19 additions & 2 deletions src/generic/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,27 @@ export async function createOrRerunCourse(courseData: Object): Promise<unknown>
return camelCaseObject(data);
}

export interface ClipboardStatus {
content: {
id: number;
userId: number;
created: string; // e.g. '2024-08-28T19:02:08.272192Z'
purpose: 'clipboard';
status: 'ready' | 'loading' | 'expired' | 'error';
blockType: string;
blockTypeDisplay: string;
olxUrl: string;
displayName: string;
} | null;
sourceUsageKey: string; // May be an empty string
sourceContextTitle: string; // May be an empty string
sourceEditUrl: string; // May be an empty string
}

/**
* Retrieves user's clipboard.
*/
export async function getClipboard(): Promise<unknown> {
export async function getClipboard(): Promise<ClipboardStatus> {
const { data } = await getAuthenticatedHttpClient()
.get(getClipboardUrl());

Expand All @@ -60,7 +77,7 @@ export async function getClipboard(): Promise<unknown> {
/**
* Updates user's clipboard.
*/
export async function updateClipboard(usageKey: string): Promise<unknown> {
export async function updateClipboard(usageKey: string): Promise<ClipboardStatus> {
const { data } = await getAuthenticatedHttpClient()
.post(getClipboardUrl(), { usage_key: usageKey });

Expand Down
138 changes: 47 additions & 91 deletions src/library-authoring/add-content/AddContentContainer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,16 @@
import React from 'react';
import { initializeMockApp } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
render, screen, fireEvent, waitFor,
} from '@testing-library/react';
import { AppProvider } from '@edx/frontend-platform/react';
import MockAdapter from 'axios-mock-adapter';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import AddContentContainer from './AddContentContainer';
import initializeStore from '../../store';
fireEvent,
render,
waitFor,
initializeMocks,
} from '../../testUtils';
import { mockContentLibrary } from '../data/api.mocks';
import { getCreateLibraryBlockUrl, getLibraryPasteClipboardUrl } from '../data/api';
import { getClipboardUrl } from '../../generic/data/api';

import { clipboardXBlock } from '../../__mocks__';

const mockUseParams = jest.fn();
let axiosMock;

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'), // use actual for all non-hook parts
useParams: () => mockUseParams(),
}));

const libraryId = '1';
let store;
import { mockClipboardEmpty, mockClipboardHtml } from '../../generic/data/api.mock';
import AddContentContainer from './AddContentContainer';

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
const { libraryId } = mockContentLibrary;
const renderOpts = { path: '/library/:libraryId/*', params: { libraryId } };

const clipboardBroadcastChannelMock = {
postMessage: jest.fn(),
Expand All @@ -41,101 +19,79 @@ const clipboardBroadcastChannelMock = {

(global as any).BroadcastChannel = jest.fn(() => clipboardBroadcastChannelMock);

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<QueryClientProvider client={queryClient}>
<AddContentContainer />
</QueryClientProvider>
</IntlProvider>
</AppProvider>
);

describe('<AddContentContainer />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
mockUseParams.mockReturnValue({ libraryId });
});

afterEach(() => {
jest.clearAllMocks();
});

it('should render content buttons', () => {
render(<RootWrapper />);
expect(screen.getByRole('button', { name: /collection/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /text/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /problem/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /open reponse/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /drag drop/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /video/i })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument();
expect(screen.queryByRole('button', { name: /copy from clipboard/i })).not.toBeInTheDocument();
initializeMocks();
mockClipboardEmpty.applyMock();
const doc = render(<AddContentContainer />);
expect(doc.getByRole('button', { name: /collection/i })).toBeInTheDocument();
expect(doc.getByRole('button', { name: /text/i })).toBeInTheDocument();
expect(doc.getByRole('button', { name: /problem/i })).toBeInTheDocument();
expect(doc.getByRole('button', { name: /open reponse/i })).toBeInTheDocument();
expect(doc.getByRole('button', { name: /drag drop/i })).toBeInTheDocument();
expect(doc.getByRole('button', { name: /video/i })).toBeInTheDocument();
expect(doc.getByRole('button', { name: /advanced \/ other/i })).toBeInTheDocument();
expect(doc.queryByRole('button', { name: /copy from clipboard/i })).not.toBeInTheDocument();
});

it('should create a content', async () => {
const { axiosMock } = initializeMocks();
mockClipboardEmpty.applyMock();
const url = getCreateLibraryBlockUrl(libraryId);
axiosMock.onPost(url).reply(200);

render(<RootWrapper />);
const doc = render(<AddContentContainer />, renderOpts);

const textButton = screen.getByRole('button', { name: /text/i });
const textButton = doc.getByRole('button', { name: /text/i });
fireEvent.click(textButton);

await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(url));
});

it('should render paste button if clipboard contains pastable xblock', async () => {
const url = getClipboardUrl();
axiosMock.onGet(url).reply(200, clipboardXBlock);

render(<RootWrapper />);

await waitFor(() => expect(axiosMock.history.get[0].url).toEqual(url));

expect(screen.getByRole('button', { name: /paste from clipboard/i })).toBeInTheDocument();
initializeMocks();
// Simulate having an HTML block in the clipboard:
const getClipboardSpy = mockClipboardHtml.applyMock();
const doc = render(<AddContentContainer />, renderOpts);
expect(getClipboardSpy).toHaveBeenCalled(); // Hmm, this is getting called three times! Refactor to use react-query.
await waitFor(() => expect(doc.queryByRole('button', { name: /paste from clipboard/i })).toBeInTheDocument());
});

it('should paste content', async () => {
const clipboardUrl = getClipboardUrl();
axiosMock.onGet(clipboardUrl).reply(200, clipboardXBlock);
const { axiosMock } = initializeMocks();
// Simulate having an HTML block in the clipboard:
const getClipboardSpy = mockClipboardHtml.applyMock();

const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
axiosMock.onPost(pasteUrl).reply(200);

render(<RootWrapper />);
const doc = render(<AddContentContainer />, renderOpts);

await waitFor(() => expect(axiosMock.history.get[0].url).toEqual(clipboardUrl));
expect(getClipboardSpy).toHaveBeenCalled(); // Hmm, this is getting called four times! Refactor to use react-query.

const pasteButton = screen.getByRole('button', { name: /paste from clipboard/i });
await waitFor(() => expect(doc.queryByRole('button', { name: /paste from clipboard/i })).toBeInTheDocument());
const pasteButton = doc.getByRole('button', { name: /paste from clipboard/i });
fireEvent.click(pasteButton);

await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));
});

it('should fail pasting content', async () => {
const clipboardUrl = getClipboardUrl();
axiosMock.onGet(clipboardUrl).reply(200, clipboardXBlock);
it('should handle failure to paste content', async () => {
const { axiosMock } = initializeMocks();
// Simulate having an HTML block in the clipboard:
mockClipboardHtml.applyMock();

const pasteUrl = getLibraryPasteClipboardUrl(libraryId);
axiosMock.onPost(pasteUrl).reply(400);

render(<RootWrapper />);
const doc = render(<AddContentContainer />, renderOpts);

await waitFor(() => expect(axiosMock.history.get[0].url).toEqual(clipboardUrl));

const pasteButton = screen.getByRole('button', { name: /paste from clipboard/i });
await waitFor(() => expect(doc.queryByRole('button', { name: /paste from clipboard/i })).toBeInTheDocument());
const pasteButton = doc.getByRole('button', { name: /paste from clipboard/i });
fireEvent.click(pasteButton);

await waitFor(() => expect(axiosMock.history.post[0].url).toEqual(pasteUrl));

// TODO: check that an actual error message is shown?!
});
});

0 comments on commit 7fddc77

Please sign in to comment.