Skip to content

Commit

Permalink
refactor: Convert more Taxonomy code to TypeScript (2) (#1536)
Browse files Browse the repository at this point in the history
* Converts some files from .js or .mjs to .ts
* Refactors some tests to use the new initializeMocks helper
* Cleans up and improves some type definitions
  • Loading branch information
bradenmacdonald authored Dec 2, 2024
1 parent abe68ac commit 6e53e37
Show file tree
Hide file tree
Showing 25 changed files with 141 additions and 278 deletions.
Original file line number Diff line number Diff line change
@@ -1,14 +1,9 @@
import React, { useContext } from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { render } from '@testing-library/react';

import initializeStore from '../store';
import { initializeMocks, render } from '../testUtils';
import { TaxonomyContext } from './common/context';
import TaxonomyLayout from './TaxonomyLayout';
import { TaxonomyLayout } from './TaxonomyLayout';

let store;
const toastMessage = 'Hello, this is a toast!';
const alertErrorTitle = 'Error title';
const alertErrorDescription = 'Error description';
Expand All @@ -20,14 +15,14 @@ const MockChildComponent = () => {
<div data-testid="mock-content">
<button
type="button"
onClick={() => setToastMessage(toastMessage)}
onClick={() => setToastMessage!(toastMessage)}
data-testid="taxonomy-show-toast"
>
Show Toast
</button>
<button
type="button"
onClick={() => setAlertProps({ title: alertErrorTitle, description: alertErrorDescription })}
onClick={() => setAlertProps!({ title: alertErrorTitle, description: alertErrorDescription })}
data-testid="taxonomy-show-alert"
>
Show Alert
Expand All @@ -46,36 +41,20 @@ jest.mock('react-router-dom', () => ({
ScrollRestoration: jest.fn(() => <div />),
}));

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<TaxonomyLayout />
</IntlProvider>
</AppProvider>
);

describe('<TaxonomyLayout />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
initializeMocks();
});

it('should render page correctly', () => {
const { getByTestId } = render(<RootWrapper />);
const { getByTestId } = render(<TaxonomyLayout />);
expect(getByTestId('mock-header')).toBeInTheDocument();
expect(getByTestId('mock-content')).toBeInTheDocument();
expect(getByTestId('mock-footer')).toBeInTheDocument();
});

it('should show toast', () => {
const { getByTestId, getByText } = render(<RootWrapper />);
const { getByTestId, getByText } = render(<TaxonomyLayout />);
const button = getByTestId('taxonomy-show-toast');
button.click();
expect(getByTestId('taxonomy-toast')).toBeInTheDocument();
Expand All @@ -88,7 +67,7 @@ describe('<TaxonomyLayout />', () => {
getByText,
getByRole,
queryByTestId,
} = render(<RootWrapper />);
} = render(<TaxonomyLayout />);

const button = getByTestId('taxonomy-show-alert');
button.click();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @ts-check
import React, { useMemo, useState } from 'react';
import { StudioFooter } from '@edx/frontend-component-footer';
import { useIntl } from '@edx/frontend-platform/i18n';
Expand All @@ -7,15 +6,15 @@ import { Toast } from '@openedx/paragon';

import AlertMessage from '../generic/alert-message';
import Header from '../header';
import { TaxonomyContext } from './common/context';
import { type AlertProps, TaxonomyContext } from './common/context';
import messages from './messages';

const TaxonomyLayout = () => {
export const TaxonomyLayout = () => {
const intl = useIntl();
// Use `setToastMessage` to show the toast.
const [toastMessage, setToastMessage] = useState(/** @type{null|string} */ (null));
const [toastMessage, setToastMessage] = useState<string | null>(null);
// Use `setToastMessage` to show the alert.
const [alertProps, setAlertProps] = useState(/** @type {null|import('./common/context').AlertProps} */ (null));
const [alertProps, setAlertProps] = useState<AlertProps | null>(null);

const context = useMemo(() => ({
toastMessage, setToastMessage, alertProps, setAlertProps,
Expand Down Expand Up @@ -51,5 +50,3 @@ const TaxonomyLayout = () => {
</TaxonomyContext.Provider>
);
};

export default TaxonomyLayout;
Original file line number Diff line number Diff line change
@@ -1,25 +1,15 @@
import React from 'react';
import { IntlProvider, injectIntl } from '@edx/frontend-platform/i18n';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import type MockAdapter from 'axios-mock-adapter';
import {
act,
fireEvent,
render,
initializeMocks,
render as baseRender,
waitFor,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
} from '../testUtils';

import initializeStore from '../store';
import { apiUrls } from './data/api';
import TaxonomyListPage from './TaxonomyListPage';
import { TaxonomyListPage } from './TaxonomyListPage';
import { TaxonomyContext } from './common/context';

let store;
let axiosMock;

const taxonomies = [{
id: 1,
name: 'Taxonomy',
Expand All @@ -39,81 +29,61 @@ const organizations = ['Org 1', 'Org 2'];
const context = {
toastMessage: null,
setToastMessage: jest.fn(),
alertProps: null,
setAlertProps: jest.fn(),
};
const queryClient = new QueryClient();

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<QueryClientProvider client={queryClient}>
<TaxonomyContext.Provider value={context}>
<TaxonomyListPage intl={injectIntl} />
</TaxonomyContext.Provider>
</QueryClientProvider>
</IntlProvider>
</AppProvider>
);

const render = (ui: React.ReactElement) => baseRender(ui, {
extraWrapper: ({ children }) => (
<TaxonomyContext.Provider value={context}> { children } </TaxonomyContext.Provider>
),
});
let axiosMock: MockAdapter;

describe('<TaxonomyListPage />', () => {
beforeEach(async () => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();
axiosMock = new MockAdapter(getAuthenticatedHttpClient());
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
axiosMock.onGet(organizationsListUrl).reply(200, organizations);
});

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

it('should render page and page title correctly', () => {
const { getByText } = render(<RootWrapper />);
const { getByText } = render(<TaxonomyListPage />);
expect(getByText('Taxonomies')).toBeInTheDocument();
});

it('shows the spinner before the query is complete', async () => {
// Simulate an API request that times out:
axiosMock.onGet(listTaxonomiesUrl).reply(new Promise(() => {}));
await act(async () => {
const { getByRole } = render(<RootWrapper />);
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading');
});
axiosMock.onGet(listTaxonomiesUrl).reply(200, new Promise(() => {}));
const { getByRole } = render(<TaxonomyListPage />);
const spinner = getByRole('status');
expect(spinner.textContent).toEqual('Loading');
});

it('shows the data table after the query is complete', async () => {
axiosMock.onGet(listTaxonomiesUrl).reply(200, { results: taxonomies, canAddTaxonomy: false });
await act(async () => {
const { getByTestId, queryByText } = render(<RootWrapper />);
await waitFor(() => { expect(queryByText('Loading')).toEqual(null); });
expect(getByTestId('taxonomy-card-1')).toBeInTheDocument();
});
const { getByTestId, queryByText } = render(<TaxonomyListPage />);
await waitFor(() => { expect(queryByText('Loading')).toEqual(null); });
expect(getByTestId('taxonomy-card-1')).toBeInTheDocument();
});

it.each(['CSV', 'JSON'])('downloads the taxonomy template %s', async (fileFormat) => {
it.each(['csv', 'json'] as const)('downloads the taxonomy template %s', async (fileFormat) => {
axiosMock.onGet(listTaxonomiesUrl).reply(200, { results: taxonomies, canAddTaxonomy: false });
const { findByRole, queryByText } = render(<RootWrapper />);
const { findByRole, queryByText } = render(<TaxonomyListPage />);
// Wait until data has been loaded and rendered:
await waitFor(() => { expect(queryByText('Loading')).toEqual(null); });
const templateMenu = await findByRole('button', { name: 'Download template' });
fireEvent.click(templateMenu);
const templateButton = await findByRole('link', { name: `${fileFormat} template` });
const templateButton = await findByRole('link', { name: `${fileFormat.toUpperCase()} template` });
fireEvent.click(templateButton);

expect(templateButton.href).toBe(apiUrls.taxonomyTemplate(fileFormat.toLowerCase()));
expect((templateButton as HTMLAnchorElement).href).toBe(apiUrls.taxonomyTemplate(fileFormat));
});

it('disables the import taxonomy button if not permitted', async () => {
axiosMock.onGet(listTaxonomiesUrl).reply(200, { results: [], canAddTaxonomy: false });

const { queryByText, getByRole } = render(<RootWrapper />);
const { queryByText, getByRole } = render(<TaxonomyListPage />);
// Wait until data has been loaded and rendered:
await waitFor(() => { expect(queryByText('Loading')).toEqual(null); });
const importButton = getByRole('button', { name: 'Import' });
Expand All @@ -123,7 +93,7 @@ describe('<TaxonomyListPage />', () => {
it('opens the import dialog modal when the import button is clicked', async () => {
axiosMock.onGet(listTaxonomiesUrl).reply(200, { results: [], canAddTaxonomy: true });

const { getByRole, getByText } = render(<RootWrapper />);
const { getByRole, getByText } = render(<TaxonomyListPage />);
const importButton = getByRole('button', { name: 'Import' });
// Once the API response is received and rendered, the Import button should be enabled:
await waitFor(() => { expect(importButton).not.toBeDisabled(); });
Expand Down Expand Up @@ -152,7 +122,7 @@ describe('<TaxonomyListPage />', () => {
getByRole,
getAllByText,
queryByText,
} = render(<RootWrapper />);
} = render(<TaxonomyListPage />);
// Wait until data has been loaded and rendered:
await waitFor(() => { expect(queryByText('Loading')).toEqual(null); });

Expand Down Expand Up @@ -197,14 +167,19 @@ describe('<TaxonomyListPage />', () => {
results: [{ name: 'Org2 Taxonomy C', ...defaults }],
});

const { getByRole, getByText, queryByText } = render(<RootWrapper />);
const {
getByRole,
getByText,
queryByText,
findByRole,
} = render(<TaxonomyListPage />);

// Open the taxonomies org filter select menu
const taxonomiesFilterSelectMenu = await getByRole('button', { name: 'All taxonomies' });
fireEvent.click(taxonomiesFilterSelectMenu);

// Check that the 'Unassigned' option is correctly called
fireEvent.click(getByRole('link', { name: 'Unassigned' }));
fireEvent.click(await findByRole('link', { name: 'Unassigned' }));
await waitFor(() => {
expect(getByText('Unassigned Taxonomy A')).toBeInTheDocument();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
// @ts-check
import React, { useState } from 'react';
import PropTypes from 'prop-types';
import {
Button,
CardView,
Expand Down Expand Up @@ -31,7 +29,7 @@ import { ImportTagsWizard } from './import-tags';
import messages from './messages';
import TaxonomyCard from './taxonomy-card';

const TaxonomyListHeaderButtons = ({ canAddTaxonomy }) => {
const TaxonomyListHeaderButtons = (props: { canAddTaxonomy: boolean }) => {
const intl = useIntl();

const [isImportModalOpen, importModalOpen, importModalClose] = useToggle(false);
Expand Down Expand Up @@ -80,7 +78,7 @@ const TaxonomyListHeaderButtons = ({ canAddTaxonomy }) => {
iconBefore={Add}
onClick={importModalOpen}
data-testid="taxonomy-import-button"
disabled={!canAddTaxonomy}
disabled={!props.canAddTaxonomy}
>
{intl.formatMessage(messages.importButtonLabel)}
</Button>
Expand All @@ -93,6 +91,11 @@ const OrganizationFilterSelector = ({
organizationListData,
selectedOrgFilter,
setSelectedOrgFilter,
}: {
isOrganizationListLoaded: boolean;
organizationListData?: string[];
selectedOrgFilter: string;
setSelectedOrgFilter: (org: string) => void,
}) => {
const intl = useIntl();
const isOrgSelected = (value) => (value === selectedOrgFilter ? <Check /> : null);
Expand Down Expand Up @@ -152,9 +155,9 @@ const OrganizationFilterSelector = ({
);
};

const TaxonomyListPage = () => {
export const TaxonomyListPage = () => {
const intl = useIntl();
const [selectedOrgFilter, setSelectedOrgFilter] = useState(ALL_TAXONOMIES);
const [selectedOrgFilter, setSelectedOrgFilter] = useState<string>(ALL_TAXONOMIES);

const {
data: organizationListData,
Expand Down Expand Up @@ -242,22 +245,3 @@ const TaxonomyListPage = () => {
</>
);
};

TaxonomyListHeaderButtons.propTypes = {
canAddTaxonomy: PropTypes.bool.isRequired,
};

OrganizationFilterSelector.propTypes = {
isOrganizationListLoaded: PropTypes.bool.isRequired,
organizationListData: PropTypes.arrayOf(PropTypes.string),
selectedOrgFilter: PropTypes.string.isRequired,
setSelectedOrgFilter: PropTypes.func.isRequired,
};

OrganizationFilterSelector.defaultProps = {
organizationListData: null,
};

TaxonomyListPage.propTypes = {};

export default TaxonomyListPage;
Loading

0 comments on commit 6e53e37

Please sign in to comment.