Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 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
17 changes: 17 additions & 0 deletions website/src/__mocks__/moduleList.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[
{ "moduleCode": "ACC2002", "title": "Managerial Accounting", "semesters": [1, 2] },
{
"moduleCode": "BFS1001",
"title": "Personal Development & Career Management",
"semesters": [1, 2]
},
{ "moduleCode": "CS1010S", "title": "Programming Methodology", "semesters": [1, 2] },
{
"moduleCode": "CS3216",
"title": "Software Product Engineering for Digital Markets",
"semesters": [1]
},
{ "moduleCode": "CS4243", "title": "Computer Vision and Pattern Recognition", "semesters": [1] },
{ "moduleCode": "GER1000", "title": "Quantitative Reasoning", "semesters": [1, 2] },
{ "moduleCode": "GES1021", "title": "Natural Heritage of Singapore", "semesters": [1, 2] }
]
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;
});
14 changes: 7 additions & 7 deletions website/src/actions/export.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ 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 { getSemesterTimetable } from 'selectors/timetables';
import { captureException } from 'utils/error';
import retryImport from 'utils/retryImport';
import { getSemesterTimetableLessons } from 'selectors/timetables';
import { SET_EXPORTED_DATA } from './constants';

function downloadUrl(blob: Blob, filename: string) {
Expand All @@ -28,11 +29,10 @@ export function downloadAsIcal(semester: Semester) {
retryImport(() => import(/* webpackChunkName: "export" */ 'utils/ical')),
])
.then(([ical, icalUtils]) => {
const {
moduleBank: { modules },
timetables,
} = getState();
const { timetable } = getSemesterTimetable(semester, timetables);
const state = getState();
const { modules } = state.moduleBank;

const timetable = getSemesterTimetableLessons(state)(semester);
const timetableWithLessons = hydrateSemTimetableWithLessons(timetable, modules, semester);

const events = icalUtils.default(semester, timetableWithLessons, modules);
Expand Down
2 changes: 1 addition & 1 deletion website/src/actions/timetables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ export function validateTimetable(semester: Semester) {
export function fetchTimetableModules(timetables: SemTimetableConfig[]) {
return (dispatch: Dispatch, getState: GetState) => {
const moduleCodes = new Set(flatMap(timetables, Object.keys));
const validateModule = getModuleCondensed(getState().moduleBank);
const validateModule = getModuleCondensed(getState());

return Promise.all(
Array.from(moduleCodes)
Expand Down
8 changes: 4 additions & 4 deletions website/src/reducers/moduleBank.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import produce, { Draft } from 'immer';
import { keyBy, omit, size, zipObject } from 'lodash';

import type { Actions } from 'types/actions';
import type { Module } from 'types/modules';
import type { ModuleBank, ModuleList } from 'types/reducers';

import {
FETCH_ARCHIVE_MODULE,
FETCH_MODULE,
Expand All @@ -10,12 +14,8 @@ import {
SET_EXPORTED_DATA,
} from 'actions/constants';
import { createMigrate, REHYDRATE } from 'redux-persist';
import { Module } from 'types/modules';
import { ModuleBank, ModuleList } from 'types/reducers';
import { SUCCESS_KEY } from 'middlewares/requests-middleware';

import { Actions } from 'types/actions';

const defaultModuleBankState: ModuleBank = {
moduleList: [], // List of basic modules data (module code, name, semester)
modules: {}, // Object of Module code -> Module details
Expand Down
6 changes: 4 additions & 2 deletions website/src/selectors/moduleBank.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ import { getModuleCondensed } from 'selectors/moduleBank';
describe(getModuleCondensed, () => {
test('should return a function that determines if the given module code is valid', () => {
const state: any = {
moduleCodes: {
CS1010S: {},
moduleBank: {
moduleCodes: {
CS1010S: {},
},
},
};

Expand Down
13 changes: 6 additions & 7 deletions website/src/selectors/moduleBank.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
import { createSelector } from 'reselect';

import { ModuleCode, ModuleCondensed, Semester } from 'types/modules';
import { ModuleBank, ModuleCodeMap, ModuleSelectListItem } from 'types/reducers';
import { SemTimetableConfig } from 'types/timetables';
import type { ModuleCode, ModuleCondensed, Semester } from 'types/modules';
import type { ModuleCodeMap, ModuleSelectListItem } from 'types/reducers';
import type { SemTimetableConfig } from 'types/timetables';
import type { State } from 'types/state';

import { notNull } from 'types/utils';
import { State } from 'types/state';
import { isOngoing } from './requests';
import { getRequestModuleCode } from '../actions/constants';

const moduleCodesSelector = (state: ModuleBank) => state.moduleCodes;

// Returns a getter that returns module condensed given a module code
export type ModuleCondensedGetter = (moduleCode: ModuleCode) => ModuleCondensed | undefined;
export const getModuleCondensed = createSelector(
moduleCodesSelector,
({ moduleBank }: State) => moduleBank.moduleCodes,
(moduleCodes: ModuleCodeMap): ModuleCondensedGetter => (moduleCode: ModuleCode) =>
moduleCodes[moduleCode],
);
Expand Down
39 changes: 24 additions & 15 deletions website/src/selectors/timetables.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { ModuleCode, Semester } from 'types/modules';
import { createSelector } from 'reselect';

import type { ModuleCode, Semester } from 'types/modules';
import type { State } from 'types/state';

import { fetchArchiveRequest } from 'actions/constants';
import config from 'config';
import { isOngoing, isSuccess } from 'selectors/requests';
import { State } from 'types/state';
import { fetchArchiveRequest } from 'actions/constants';
import { ColorMapping, TimetablesState } from 'types/reducers';
import { SemTimetableConfig } from 'types/timetables';

export function isArchiveLoading(state: State, moduleCode: ModuleCode) {
return config.archiveYears.some((year) =>
Expand All @@ -18,14 +19,22 @@ export function availableArchive(state: State, moduleCode: ModuleCode): string[]
);
}

// Extract sem timetable and colors for a specific semester from TimetablesState
const EMPTY_OBJECT = {};
export function getSemesterTimetable(
semester: Semester,
state: TimetablesState,
): { timetable: SemTimetableConfig; colors: ColorMapping } {
return {
timetable: state.lessons[semester] || EMPTY_OBJECT,
colors: state.colors[semester] || EMPTY_OBJECT,
};
}

/**
* Extract semester timetable lessons for a specific semester.
*/
export const getSemesterTimetableLessons = createSelector(
({ timetables }: State) => timetables.lessons,
(lessons) => (semester: Semester | null) =>
semester === null ? EMPTY_OBJECT : lessons[semester] ?? EMPTY_OBJECT,
);

/**
* Extract semester timetable colors for a specific semester.
*/
export const getSemesterTimetableColors = createSelector(
({ timetables }: State) => timetables.colors,
(colors) => (semester: Semester | null) =>
semester === null ? EMPTY_OBJECT : colors[semester] ?? EMPTY_OBJECT,
);
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
32 changes: 32 additions & 0 deletions website/src/test-utils/renderWithRouterMatch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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,
}: {
path?: string;
location?: Parameters<typeof createHistory>[0];
},
) {
const { history } = createHistory(location);
const view = render(
<Router history={history}>
<Route path={path}>{children}</Route>
</Router>,
);
return {
history,
view,
};
}
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,
);
}
4 changes: 2 additions & 2 deletions website/src/utils/export.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Semester } from 'types/modules';
import { ExportData } from 'types/export';
import { getSemesterTimetable } from 'selectors/timetables';
import { getSemesterTimetableColors } from 'selectors/timetables';
import { State } from 'types/state';
import { SemTimetableConfig } from 'types/timetables';

Expand All @@ -9,7 +9,7 @@ export function extractStateForExport(
timetable: SemTimetableConfig,
state: State,
): ExportData {
const { colors } = getSemesterTimetable(semester, state.timetables);
const colors = getSemesterTimetableColors(state)(semester);
const hidden = state.timetables.hidden[semester] || [];

return {
Expand Down
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,
);
}
10 changes: 5 additions & 5 deletions website/src/utils/timetables.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import _ from 'lodash';
import { getModuleSemesterData, getModuleTimetable } from 'utils/modules';

import { CS1010S, CS3216, CS4243, PC1222 } from '__mocks__/modules';
import modulesListJSON from '__mocks__/module-list.json';
import moduleCodeMapJSON from '__mocks__/module-code-map.json';
import timetable from '__mocks__/sem-timetable.json';
import lessonsArray from '__mocks__/lessons-array.json';

Expand Down Expand Up @@ -54,7 +54,7 @@ import {
} from './timetables';

// TODO: Fix this later
const modulesList = modulesListJSON as any;
const moduleCodeMap = moduleCodeMapJSON as any;

describe(isValidSemester, () => {
test('semesters 1-4 are valid', () => {
Expand Down Expand Up @@ -471,14 +471,14 @@ test('isSameTimetableConfig', () => {

describe(validateTimetableModules, () => {
test('should leave valid modules untouched', () => {
expect(validateTimetableModules({}, modulesList)).toEqual([{}, []]);
expect(validateTimetableModules({}, moduleCodeMap)).toEqual([{}, []]);
expect(
validateTimetableModules(
{
CS1010S: {},
CS2100: {},
},
modulesList,
moduleCodeMap,
),
).toEqual([{ CS1010S: {}, CS2100: {} }, []]);
});
Expand All @@ -490,7 +490,7 @@ describe(validateTimetableModules, () => {
DEADBEEF: {},
CS2100: {},
},
modulesList,
moduleCodeMap,
),
).toEqual([{ CS2100: {} }, ['DEADBEEF']]);
});
Expand Down
4 changes: 3 additions & 1 deletion website/src/views/components/LinkModuleCodes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ describe(LinkModuleCodesComponent, () => {

function create(content: string, moduleCodes: ModuleCodeMap = {}) {
const getModule = getModuleCondensed({
moduleCodes,
moduleBank: {
moduleCodes,
},
} as any);

return mount(
Expand Down
2 changes: 1 addition & 1 deletion website/src/views/components/LinkModuleCodes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const LinkModuleCodesComponent: React.FC<Props> = (props) => {
};

const mapStateToProps = connect((state: State) => ({
getModuleCondensed: getModuleCondensed(state.moduleBank),
getModuleCondensed: getModuleCondensed(state),
}));

export default mapStateToProps(LinkModuleCodesComponent);
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;
4 changes: 2 additions & 2 deletions website/src/views/components/module-info/ModuleExamClash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Module, ModuleCode, Semester } from 'types/modules';
import { AlertTriangle } from 'react-feather';
import { getModuleSemesterData } from 'utils/modules';
import { getSemesterModules } from 'utils/timetables';
import { getSemesterTimetable } from 'selectors/timetables';
import { getSemesterTimetableLessons } from 'selectors/timetables';
import LinkModuleCodes from 'views/components/LinkModuleCodes';
import { State } from 'types/state';

Expand Down Expand Up @@ -60,7 +60,7 @@ export const ModuleExamClashComponent: React.FC<Props> = ({
};

export default connect((state: State, ownProps: OwnProps) => {
const { timetable } = getSemesterTimetable(ownProps.semester, state.timetables);
const timetable = getSemesterTimetableLessons(state)(ownProps.semester);
const modulesMap = state.moduleBank.modules;
return { modules: getSemesterModules(timetable, modulesMap) };
})(React.memo(ModuleExamClashComponent));
Loading