Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d9c8cd7
Enzyme -> RTL Navtabs.test.tsx
taneliang Dec 2, 2020
310e56d
Functionalize Navtabs
taneliang Dec 2, 2020
6ee3f7b
Fix nit in reducer types
taneliang Dec 2, 2020
86a6e08
WIP Enzyme -> RTL ModuleArchiveContainer.test.tsx
taneliang Dec 2, 2020
e887abd
Fix ModuleArchiveContainer tests and split retryImport into its own file
taneliang Dec 2, 2020
4b2255d
Mock RandomKawaii to reduce noise in test failure messages
taneliang Dec 2, 2020
3ee467b
Reduce test output noise by mocking react-feather in tests
taneliang Dec 3, 2020
b17bc2c
yarn upgrade @types/react-router-dom
taneliang Dec 4, 2020
e4f5ae1
Functionalize ModuleArchiveContainer
taneliang Dec 4, 2020
723242a
Enzyme -> RTL ModulePageContainer.test.tsx
taneliang Dec 4, 2020
8522944
Functionalize ModulePageContainer
taneliang Dec 4, 2020
d9a32cb
Rename module-list.json -> module-code-map.jsonn
taneliang Dec 4, 2020
2330d57
Overhaul TimetableContainer.test.tsx with RTL
taneliang Dec 5, 2020
c771035
Change getModuleCondensed selector to operate on global Redux state
taneliang Dec 5, 2020
ee0f59f
Replace getSemesterTimetable with getSemesterTimetable(Colors|Lessons…
taneliang Dec 5, 2020
6eeb745
Functionalize TimetableContainer
taneliang Dec 5, 2020
e9c3cea
Merge branch 'master' into eliang/even-more-fc
taneliang Dec 5, 2020
c7b9b35
Remove withRouter and matchBreakpoint hocs from VenueDetails
taneliang Dec 5, 2020
d464848
import * as React -> import type { FC } in NoFooter
taneliang Dec 5, 2020
b7c0613
Clarify VenueDetails useMediaQuery return variable name
taneliang Dec 6, 2020
34e4c87
Merge branch 'master' into eliang/even-more-fc
taneliang Dec 6, 2020
829e6d5
yarn lint:code --fix
taneliang Dec 6, 2020
ef77b04
Fix noob mistakes in TimetableContainer RTL tests
taneliang Dec 6, 2020
03c03e3
Add mock Axios response in TimetableContainer
taneliang Dec 6, 2020
9dd20e1
Fix all noob RTL mistakes in this PR
taneliang Dec 6, 2020
e0aefaa
Rename renderResult -> view
taneliang Dec 6, 2020
5cd113c
Increase renderWithRouterMatch param flexibility
taneliang Dec 6, 2020
ed658e8
Merge branch 'master' into eliang/even-more-fc
taneliang Dec 7, 2020
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
2 changes: 1 addition & 1 deletion website/src/__mocks__/modules/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Module } from 'types/modules';
import type { Module } from 'types/modules';

import ACC2002_JSON from './ACC2002.json';
import BFS1001_JSON from './BFS1001.json';
Expand Down
9 changes: 9 additions & 0 deletions website/src/__mocks__/react-feather.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { mapValues } from 'lodash';
import type { ComponentType } from 'react';
import * as feather from 'react-feather';

module.exports = mapValues(feather, (_component, name) => {
const MockComponent = jest.fn(() => <div data-testid={`react-feather ${name} icon`} />);
(MockComponent as ComponentType).displayName = name;
return MockComponent;
});
3 changes: 2 additions & 1 deletion website/src/actions/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import type { Module, Semester } from 'types/modules';
import type { ExportData } from 'types/export';
import type { Dispatch, GetState } from 'types/redux';
import { hydrateSemTimetableWithLessons } from 'utils/timetables';
import { captureException, retryImport } from 'utils/error';
import { captureException } from 'utils/error';
import retryImport from 'utils/retryImport';
import { getSemesterTimetable } from 'selectors/timetables';
import { SET_EXPORTED_DATA } from './constants';

Expand Down
8 changes: 2 additions & 6 deletions website/src/test-utils/createHistory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,13 @@ type MatchShape = {
isExact?: boolean;
};

// This can also be Location, but no test case use that for now so we leave it
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createMemoryHistory's initialEntries can only be a string[], so I think this comment is outdated.

// out for simplicity
type HistoryEntry = string;

// eslint-disable-next-line @typescript-eslint/ban-types
export default function createHistory<T = {}>(
initialEntries: HistoryEntry | Readonly<HistoryEntry[]> = '/',
initialEntries: string | string[] = '/',
matchParams: MatchShape = {},
): RouteComponentProps<T> {
const entries = _.castArray(initialEntries);
const history = createMemoryHistory({ initialEntries: entries as any });
const history = createMemoryHistory({ initialEntries: entries });
const { params = {}, isExact = true } = matchParams;

const match: Match<T> = {
Expand Down
23 changes: 23 additions & 0 deletions website/src/test-utils/renderWithRouterMatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { render } from '@testing-library/react';
import type { ReactNode } from 'react';
import { Route, Router } from 'react-router-dom';
import createHistory from './createHistory';

/**
* `render` `children` in a `Router` and `Route` so that `children` have
* populated route matches when using React Router.
*
* Inspiration: https://spectrum.chat/testing-library/help-react/attempting-to-test-react-router-match~b0550426-f54a-4b76-b402-c7b32204b55e?m=MTU2OTM1MzY4NjUwNw==
*/
export default function renderWithRouterMatch(children: ReactNode, { path = '/', location = '/' }) {
const { history } = createHistory([location]);
const renderResult = render(
<Router history={history}>
<Route path={path}>{children}</Route>
</Router>,
);
return {
history,
renderResult,
};
}
2 changes: 1 addition & 1 deletion website/src/types/reducers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ export type ModuleCodeMap = { [moduleCode: string]: ModuleCondensed };
export type ModuleArchive = {
[moduleCode: string]: {
// Mapping acad year to module info
[key: string]: Module;
[acadYear: string]: Module;
};
};

Expand Down
1 change: 0 additions & 1 deletion website/src/utils/__mocks__/error.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export const captureException = jest.fn();
export const getScriptErrorHandler = jest.fn().mockReturnValue(() => jest.fn());
export const retryImport = jest.fn().mockResolvedValue(undefined);
13 changes: 0 additions & 13 deletions website/src/utils/error.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as Sentry from '@sentry/browser';
import { each, size } from 'lodash';
import { retry } from 'utils/promise';

export function captureException(error: Error, extra: { [key: string]: unknown } = {}) {
Sentry.withScope((scope) => {
Expand Down Expand Up @@ -30,15 +29,3 @@ export function getScriptErrorHandler(scriptName: string) {
}
};
}

/**
* Wrap an async import() so that it automatically retries in case of a chunk
* load error and when the user is online
*/
export function retryImport<T>(importFactory: () => Promise<T>, retries = 3) {
return retry(
retries,
importFactory,
(error) => error.message.includes('Loading chunk ') && window.navigator.onLine,
);
}
13 changes: 13 additions & 0 deletions website/src/utils/retryImport.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { retry } from 'utils/promise';

/**
* Wrap an async import() so that it automatically retries in case of a chunk
* load error and when the user is online
*/
export default function retryImport<T>(importFactory: () => Promise<T>, retries = 3) {
return retry(
retries,
importFactory,
(error) => error.message.includes('Loading chunk ') && window.navigator.onLine,
);
}
2 changes: 1 addition & 1 deletion website/src/views/components/Tooltip/index.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';

import { retryImport } from 'utils/error';
import retryImport from 'utils/retryImport';

import { Props, TooltipGroupProps } from './Tooltip';

Expand Down
4 changes: 4 additions & 0 deletions website/src/views/components/__mocks__/RandomKawaii.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type { FC } from 'react';

const MockRandomKawaii: FC = jest.fn(() => <div data-testid="RandomKawaii component" />);
export default MockRandomKawaii;
2 changes: 1 addition & 1 deletion website/src/views/contribute/ContributeContainer/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Loadable, { LoadingComponentProps } from 'react-loadable';

import LoadingSpinner from 'views/components/LoadingSpinner';
import ApiError from 'views/errors/ApiError';
import { retryImport } from 'utils/error';
import retryImport from 'utils/retryImport';

const AsyncContributeContainer = Loadable({
loader: () =>
Expand Down
76 changes: 67 additions & 9 deletions website/src/views/layout/Navtabs.test.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,71 @@
import { shallow } from 'enzyme';
import { render } from '@testing-library/react';
import configureStore from 'bootstrapping/configure-store';
import produce from 'immer';
import React from 'react';
import { Provider } from 'react-redux';
import { MemoryRouter } from 'react-router-dom';
import reducers from 'reducers';
import { initAction } from 'test-utils/redux';

import createHistory from 'test-utils/createHistory';
import { NavtabsComponent } from './Navtabs';
import Navtabs from './Navtabs';

describe(NavtabsComponent, () => {
test('renders into nav element', () => {
const navtabs = shallow(
<NavtabsComponent activeSemester={1} beta={false} {...createHistory()} />,
);
expect(navtabs).toMatchSnapshot();
const relevantStoreContents = {
app: { activeSemester: 1 },
settings: { beta: false },
};

const initialState = reducers(undefined, initAction());

function make(storeOverrides: Partial<typeof relevantStoreContents> = {}) {
const { store } = configureStore(
produce(initialState, (draft) => {
draft.app.activeSemester =
storeOverrides.app?.activeSemester ?? relevantStoreContents.app.activeSemester;
draft.settings.beta = storeOverrides.settings?.beta ?? relevantStoreContents.settings.beta;
}),
);
return render(
<MemoryRouter>
<Provider store={store}>
<Navtabs />,
</Provider>
</MemoryRouter>,
);
}

describe(Navtabs, () => {
test('should render into nav element', () => {
const { container } = make();
expect(Array.from(container.getElementsByTagName('a')).map((elem) => elem.textContent))
.toMatchInlineSnapshot(`
Array [
"Today",
"Timetable",
"Modules",
"Venues",
"Settings",
"Contribute",
"Whispers",
]
`);
expect(container).toMatchSnapshot();
Comment thread
taneliang marked this conversation as resolved.
Outdated
});

test('should show beta tabs if beta is true', () => {
const { container } = make({ settings: { beta: true } });
expect(Array.from(container.getElementsByTagName('a')).map((elem) => elem.textContent))
.toMatchInlineSnapshot(`
Array [
"Today",
"Timetable",
"Modules",
"Venues",
"Planner",
"Settings",
"Contribute",
"Whispers",
]
`);
expect(container).toMatchSnapshot();
});
});
28 changes: 10 additions & 18 deletions website/src/views/layout/Navtabs.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,24 @@
import * as React from 'react';
import { connect } from 'react-redux';
import { NavLink, RouteComponentProps, withRouter } from 'react-router-dom';
import type { FC } from 'react';
import { useSelector } from 'react-redux';
import { NavLink } from 'react-router-dom';
import classnames from 'classnames';
import { BookOpen, Calendar, Clock, Heart, Map, Settings, Star, Trello } from 'react-feather';

import { Semester } from 'types/modules';
import ExternalLink from 'views/components/ExternalLink';
import { timetablePage } from 'views/routes/paths';
import { preload as preloadToday } from 'views/today/TodayContainer';
import { preload as preloadVenues } from 'views/venues/VenuesContainer';
import { preload as preloadContribute } from 'views/contribute/ContributeContainer';
import { State } from 'types/state';
import type { State } from 'types/state';

import styles from './Navtabs.scss';

export const NAVTAB_HEIGHT = 48;

type Props = RouteComponentProps & {
activeSemester: Semester;
beta: boolean;
};
const Navtabs: FC = () => {
const activeSemester = useSelector(({ app }: State) => app.activeSemester);
const beta = useSelector(({ settings }: State) => settings.beta);

export const NavtabsComponent: React.FC<Props> = (props) => {
const tabProps = {
className: styles.link,
activeClassName: styles.linkActive,
Expand All @@ -33,7 +30,7 @@ export const NavtabsComponent: React.FC<Props> = (props) => {
<Clock />
<span className={styles.title}>Today</span>
</NavLink>
<NavLink {...tabProps} to={timetablePage(props.activeSemester)}>
<NavLink {...tabProps} to={timetablePage(activeSemester)}>
<Calendar />
<span className={styles.title}>Timetable</span>
</NavLink>
Expand All @@ -48,7 +45,7 @@ export const NavtabsComponent: React.FC<Props> = (props) => {
<Map />
<span className={styles.title}>Venues</span>
</NavLink>
{props.beta && (
{beta && (
<NavLink
{...tabProps}
className={classnames(tabProps.className, styles.hiddenOnMobile)}
Expand Down Expand Up @@ -84,9 +81,4 @@ export const NavtabsComponent: React.FC<Props> = (props) => {
);
};

const connectedNavtabs = connect((state: State) => ({
activeSemester: state.app.activeSemester,
beta: !!state.settings.beta,
}))(NavtabsComponent);

export default withRouter(connectedNavtabs);
export default Navtabs;
Loading