Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: Convert more Taxonomy code to TypeScript (2) #1536

Merged
merged 2 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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