Skip to content

Commit

Permalink
add router unit tests
Browse files Browse the repository at this point in the history
  • Loading branch information
no-chris committed Feb 3, 2024
1 parent 87dc146 commit e4e4fad
Show file tree
Hide file tree
Showing 7 changed files with 200 additions and 31 deletions.
2 changes: 1 addition & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ module.exports = {
],
'no-unsanitized/method': ['error'],

'react/prop-types': ['error'],
'react/prop-types': ['off'],

'react-hooks/rules-of-hooks': ['error'],
'react-hooks/exhaustive-deps': ['warn'],
Expand Down
2 changes: 1 addition & 1 deletion packages/chord-chart-studio/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function run() {
? window.location.pathname + window.location.search
: '/';

router.initRouter(allRoutes);
router.init(allRoutes);

return navigateTo(currentPathname);
}
54 changes: 33 additions & 21 deletions packages/chord-chart-studio/src/core/router.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import qs from 'qs';

import renderController from '../renderController';

let universalRouter;
let router;
let getUrl;

export default {
initRouter(allRoutes) {
init(allRoutes) {
const allRoutesWithWrappedActions = allRoutes.map((route) => {
return {
...route,
Expand All @@ -18,42 +18,54 @@ export default {
}),
};
});
universalRouter = new UniversalRouter(allRoutesWithWrappedActions, {
router = new UniversalRouter(allRoutesWithWrappedActions, {
errorHandler(error, context) {
console.error('======== Error');
console.error(error);
console.info(context);
console.error(
`Error: Cannot find route for path: ${context.pathname}`
);
},
});
getUrl = generateUrls(universalRouter);
getUrl = generateUrls(router, {
stringifyQueryParams: qs.stringify,
});
},
};

export function navigateTo(url, shouldPushState = true) {
const parsedUrl = new URL(url, window.location.origin);

return universalRouter
return router
.resolve(parsedUrl.pathname)
.then(({ Controller, params }) => {
if (shouldPushState) {
pushState(url);
.then(({ Controller, params } = {}) => {
if (Controller) {
if (shouldPushState) {
pushState(url);
}
const queryParams = qs.parse(parsedUrl.search, {
ignoreQueryPrefix: true,
});
renderController(Controller, {
...params,
...queryParams,
});
}
const queryParams = qs.parse(parsedUrl.search, {
ignoreQueryPrefix: true,
});
renderController(Controller, {
...params,
...queryParams,
});
});
}

function pushState(stateUrl) {
window.history.pushState({}, null, stateUrl);
export function getLink(routeName, params) {
try {
return getUrl(routeName, params);
} catch (e) {
console.error(e.toString());
}
}

function pushState(url) {
window.history.pushState({ url }, null, url);
}

/* istanbul ignore next */
window.addEventListener('popstate', () => {
console.log('popstate');
const path = window.location.pathname + window.location.search;
navigateTo(path, false);
});
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { useSelector } from 'react-redux';

import { getAllTitles } from '../../../db/files/selectors';
import { navigateTo } from '../../../core/router';
import { navigateTo, getLink } from '../../../core/router';

export default function Library() {
const allTitles = useSelector(getAllTitles);
Expand All @@ -22,8 +22,7 @@ export default function Library() {
const SongEntry = ({ song }) => {
const handleClick = (e) => {
e.preventDefault();
console.log('going to', `/songView/${song.id}`);
navigateTo(`/songView/${song.id}`);
navigateTo(getLink('songView', { songId: song.id }));
};
return (
<li>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import React from 'react';
import { useSelector } from 'react-redux';

import { navigateTo } from '../../../core/router';
import { navigateTo, getLink } from '../../../core/router';
import { getOne } from '../../../db/files/selectors';
//import router from '../router';

export default function SongView({ songId }) {
const song = useSelector((state) => getOne(state, songId));

const handleClick = (e) => {
e.preventDefault();
navigateTo('/library');
navigateTo(getLink('library'));
};

return (
Expand Down
8 changes: 6 additions & 2 deletions packages/chord-chart-studio/src/renderController.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ import { getStore } from './state/store';

import ErrorBoundary from './ui/_components/ErrorBoundary';

const container = document.getElementById('app');
const root = createRoot(container);
let root;

export default function renderController(Controller, params) {
const container = document.getElementById('app');
if (!root) {
root = createRoot(container);
}

root.render(
<Provider store={getStore()}>
<React.StrictMode>
Expand Down
155 changes: 155 additions & 0 deletions packages/chord-chart-studio/tests/unit/core/router.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
jest.mock('../../../src/renderController');
import renderController from '../../../src/renderController';

import router, { navigateTo, getLink } from '../../../src/core/router';

describe('router', () => {
test('Module', () => {
expect(router).toBeInstanceOf(Object);
expect(router.init).toBeInstanceOf(Function);
expect(navigateTo).toBeInstanceOf(Function);
expect(getLink).toBeInstanceOf(Function);
});
});

const logSpy = jest.spyOn(console, 'error');

beforeEach(() => {
renderController.mockClear();
logSpy.mockClear();
});

const allRoutes = [
{
name: 'test1',
path: '/test',
action: 'myAction1',
},
{
name: 'test2',
path: '/test/:artistId/:songId',
action: 'myAction2',
},
];

describe('navigateTo', () => {
test('should render the controller attached to a given route', async () => {
router.init(allRoutes);
await navigateTo('/test');

expect(renderController).toHaveBeenCalledTimes(1);
expect(renderController).toHaveBeenCalledWith('myAction1', {});
});

test('should pass the pathname parameters to the controller', async () => {
router.init(allRoutes);
await navigateTo('/test/bowie/changes');

expect(renderController).toHaveBeenCalledTimes(1);
expect(renderController).toHaveBeenCalledWith('myAction2', {
artistId: 'bowie',
songId: 'changes',
});
});

test('should pass the query strings params to the controller', async () => {
router.init(allRoutes);
await navigateTo('/test?artistId=bowie&songId=changes');

expect(renderController).toHaveBeenCalledTimes(1);
expect(renderController).toHaveBeenCalledWith('myAction1', {
artistId: 'bowie',
songId: 'changes',
});
});

test('should pass pathname and query strings params to the controller, query strings have the final word', async () => {
router.init(allRoutes);
await navigateTo('/test/bowie/changes?print=true&songId=ashes2ashes');

expect(renderController).toHaveBeenCalledTimes(1);
expect(renderController).toHaveBeenCalledWith('myAction2', {
artistId: 'bowie',
songId: 'ashes2ashes',
print: 'true',
});
});

test('should add an history entry by default', async () => {
await navigateTo('/test/bowie/lifeonmars');

expect(history.state.url).toBe('/test/bowie/lifeonmars');
});

test('should explicitly add an history entry if `shouldPushState` is true', async () => {
await navigateTo('/test/bowie/themotel', true);

expect(history.state.url).toBe('/test/bowie/themotel');
});

test('should not add an history entry if `shouldPushState` is false', async () => {
await navigateTo('/test/bowie/starman', false);

expect(history.state.url).not.toBe('/test/bowie/starman');
});

test('should log a error if route does not exists', async () => {
await navigateTo('/unknown?params=should&be=discarded');

expect(logSpy).toHaveBeenCalledWith(
'Error: Cannot find route for path: /unknown'
);
});

test('should log a error if route is undefined', async () => {
await navigateTo(undefined);

expect(logSpy).toHaveBeenCalledWith(
'Error: Cannot find route for path: /undefined'
);
});
});

describe('getLink', () => {
test('should get the link for a given route name', async () => {
router.init(allRoutes);

expect(getLink('test1')).toBe('/test');
expect(getLink('test2', { artistId: 'bowie', songId: 'changes' })).toBe(
'/test/bowie/changes'
);
});

test('should happen extra params as query string parameters', async () => {
router.init(allRoutes);

expect(getLink('test1', { my: 'param', myOther: 'param' })).toBe(
'/test?my=param&myOther=param'
);
expect(
getLink('test2', {
artistId: 'bowie',
songId: 'changes',
printMode: true,
})
).toBe('/test/bowie/changes?printMode=true');
});

test('should log a error if route name does not exists', () => {
router.init(allRoutes);

getLink('unknown');

expect(logSpy).toHaveBeenCalledWith(`Error: Route "unknown" not found`);
});

test('should return undefined in case of missing params', () => {
router.init(allRoutes);

expect(
getLink('test2', {
artistId: 'bowie',
})
).toBeUndefined();
});
});

0 comments on commit e4e4fad

Please sign in to comment.