Skip to content

Commit

Permalink
Added tests for SSR with inferno-router and fix bug in <Switch> (reba…
Browse files Browse the repository at this point in the history
…sed on master) (#1658)

* Added tests for inferno-router SSR and <Switch> and fixed a bug. Also added support for subclassing Route

* simple lint fixes

* Second set of lint-fixes
  • Loading branch information
jhsware authored Dec 11, 2023
1 parent aa11f19 commit d05dc2d
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 40 deletions.
40 changes: 40 additions & 0 deletions packages/inferno-router/__tests__/loaderOnRoute.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -693,4 +693,44 @@ describe('Resolve loaders during server side rendering', () => {
const result = await resolveLoaders(loaderEntries);
expect(result).toEqual(initialData);
});


it('Can resolve with sub classed Route', async () => {
class MyRoute extends Route {
constructor(props, context) {
super(props, context);
}
}
const TEXT = 'bubblegum';
const Component = (props) => {
const res = useLoaderData(props);
return <h1>{res?.message}</h1>;
};

const loaderFuncNoHit = async () => {
return { message: 'no' };
};
const loaderFunc = async () => {
return { message: TEXT };
};

const initialData = {
'/flowers': { res: await loaderFunc() },
'/flowers/birds': { res: await loaderFunc() }
};

const app = (
<StaticRouter context={{}} location="/flowers/birds">
<MyRoute path="/flowers" render={Component} loader={loaderFunc}>
<MyRoute path="/flowers/birds" render={Component} loader={loaderFunc} />
<MyRoute path="/flowers/bees" render={Component} loader={loaderFuncNoHit} />
{null}
</MyRoute>
</StaticRouter>
);

const loaderEntries = traverseLoaders('/flowers/birds', app);
const result = await resolveLoaders(loaderEntries);
expect(result).toEqual(initialData);
});
});
122 changes: 107 additions & 15 deletions packages/inferno-router/__tests__/loaderWithSwitch.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { render, rerender } from 'inferno';
import {
MemoryRouter,
Route,
Switch,
NavLink,
useLoaderData,
useLoaderError,
} from 'inferno-router';
import { MemoryRouter, StaticRouter, Route, Switch, NavLink, useLoaderData, useLoaderError, resolveLoaders, traverseLoaders } from 'inferno-router';
// Cherry picked relative import so we don't get node-stuff from inferno-server in browser test
import { createEventGuard } from './testUtils';

Expand Down Expand Up @@ -44,7 +37,7 @@ describe('A <Route> with loader in a MemoryRouter', () => {
loader={loaderFunc}
/>
</MemoryRouter>,
container,
container
);

// Wait until async loader has completed
Expand Down Expand Up @@ -73,7 +66,7 @@ describe('A <Route> with loader in a MemoryRouter', () => {
loader={loaderFunc}
/>
</MemoryRouter>,
container,
container
);

// Wait until async loader has completed
Expand All @@ -92,14 +85,14 @@ describe('A <Route> with loader in a MemoryRouter', () => {
return { message: TEXT };
};
const initialData = {
'/flowers': { res: await loaderFunc(), err: undefined },
'/flowers': { res: await loaderFunc(), err: undefined }
};

render(
<MemoryRouter initialEntries={['/flowers']} initialData={initialData}>
<Route path="/flowers" render={Component} loader={loaderFunc} />
</MemoryRouter>,
container,
container
);

expect(container.innerHTML).toContain(TEXT);
Expand Down Expand Up @@ -178,14 +171,14 @@ describe('A <Route> with loader in a MemoryRouter', () => {
return { message: TEXT };
};
const initialData = {
'/flowers': { res: await loaderFunc(), err: undefined },
'/flowers': { res: await loaderFunc(), err: undefined }
};

render(
<MemoryRouter initialEntries={['/flowers']} initialData={initialData}>
<Route path="/flowers" render={Component} loader={loaderFunc} />
</MemoryRouter>,
container,
container
);

expect(container.innerHTML).toContain(TEXT);
Expand Down Expand Up @@ -283,7 +276,7 @@ describe('A <Route> with loader in a MemoryRouter', () => {
/>
</Switch>
</MemoryRouter>,
container,
container
);

// Check that we are starting in the right place
Expand Down Expand Up @@ -371,3 +364,102 @@ describe('A <Route> with loader in a MemoryRouter', () => {
expect(container.querySelector('#publish')).toBeNull();
});
});

describe('Resolve loaders during server side rendering', () => {
it('Can resolve with single route', async () => {
const TEXT = 'bubblegum';
const Component = (props) => {
const res = useLoaderData(props);
return <h1>{res?.message}</h1>;
};

const loaderFunc = async () => {
return { message: TEXT };
};

const initialData = {
'/flowers': { res: await loaderFunc() }
};

const app = (
<StaticRouter context={{}} location="/flowers">
<Switch>
<Route path="/flowers" render={Component} loader={loaderFunc} />
</Switch>
</StaticRouter>
);

const loaderEntries = traverseLoaders('/flowers', app);
const result = await resolveLoaders(loaderEntries);
expect(result).toEqual(initialData);
});

it('Can resolve with multiple routes', async () => {
const TEXT = 'bubblegum';
const Component = (props) => {
const res = useLoaderData(props);
return <h1>{res?.message}</h1>;
};

const loaderFuncNoHit = async () => {
return { message: 'no' };
};
const loaderFunc = async () => {
return { message: TEXT };
};

const initialData = {
'/birds': { res: await loaderFunc() }
};

const app = (
<StaticRouter context={{}} location="/birds">
<Switch>
<Route path="/flowers" render={Component} loader={loaderFuncNoHit} />
<Route path="/birds" render={Component} loader={loaderFunc} />
<Route path="/birds" render={Component} loader={loaderFuncNoHit} />
</Switch>
</StaticRouter>
);

const loaderEntries = traverseLoaders('/birds', app);
const result = await resolveLoaders(loaderEntries);
expect(result).toEqual(initialData);
});

it('Can resolve with nested routes', async () => {
const TEXT = 'bubblegum';
const Component = (props) => {
const res = useLoaderData(props);
return <h1>{res?.message}</h1>;
};

const loaderFuncNoHit = async () => {
return { message: 'no' };
};
const loaderFunc = async () => {
return { message: TEXT };
};

const initialData = {
'/flowers': { res: await loaderFunc() },
'/flowers/birds': { res: await loaderFunc() }
};

const app = (
<StaticRouter context={{}} location="/flowers/birds">
<Route path="/flowers" render={Component} loader={loaderFunc}>
<Switch>
<Route path="/flowers/birds" render={Component} loader={loaderFunc} />
<Route path="/flowers/bees" render={Component} loader={loaderFuncNoHit} />
{null}
</Switch>
</Route>
</StaticRouter>
);

const loaderEntries = traverseLoaders('/flowers/birds', app);
const result = await resolveLoaders(loaderEntries);
expect(result).toEqual(initialData);
});
});
52 changes: 27 additions & 25 deletions packages/inferno-router/src/resolveLoaders.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { isNullOrUndef, isUndefined } from 'inferno-shared';
import { matchPath } from './matchPath';
import type { TLoaderData } from './Router';
import type { TLoaderData, TLoaderProps } from './Router';
import { Switch } from './Switch';
import { Route } from './Route';

export async function resolveLoaders(
loaderEntries: TLoaderEntry[],
Expand All @@ -21,7 +22,7 @@ interface TLoaderEntry {
params: Record<string, any>;
request: Request;
controller: AbortController;
loader: (TLoaderProps) => Promise<TLoaderEntry>;
loader: (props: TLoaderProps<any>) => Promise<TLoaderEntry>;
}

export function traverseLoaders(
Expand All @@ -32,6 +33,19 @@ export function traverseLoaders(
return _traverseLoaders(location, tree, base, false);
}

function _isSwitch(node: any): boolean {
// Using the same patterns as for _isRoute, but I don't have a test where
// I pass a Switch via an array, but it is better to be consistent.
return node?.type?.prototype instanceof Switch || node?.type === Switch;
}

function _isRoute(node: any): boolean {
// So the === check is needed if routes are passed in an array,
// the instanceof test if routes are passed as children to a Component
// This feels inconsistent, but at least it works.
return node?.type?.prototype instanceof Route || node?.type === Route;
}

// Optionally pass base param during SSR to get fully qualified request URI passed to loader in request param
function _traverseLoaders(
location: string,
Expand All @@ -47,12 +61,7 @@ function _traverseLoaders(
const entriesOfArr = tree.reduce((res, node) => {
if (parentIsSwitch && hasMatch) return res;

const outpArr = _traverseLoaders(
location,
node,
base,
node?.type?.prototype instanceof Switch,
);
const outpArr = _traverseLoaders(location, node, base, _isSwitch(node));
if (parentIsSwitch && outpArr.length > 0) {
hasMatch = true;
}
Expand All @@ -62,9 +71,7 @@ function _traverseLoaders(
}

const outp: TLoaderEntry[] = [];
let isRouteButNotMatch = false;
if (tree.props) {
// TODO: If we traverse a switch, only the first match should be returned
if (_isRoute(tree) && tree.props) {
// TODO: Should we check if we are in Router? It is defensive and could save a bit of time, but is it worth it?
const {
path,
Expand All @@ -80,10 +87,10 @@ function _traverseLoaders(
});

// So we can bail out of recursion it this was a Route which didn't match
isRouteButNotMatch = !match;

// Add any loader on this node (but only on the VNode)
if (match && !tree.context && tree.props?.loader && tree.props?.path) {
if (!match) {
return outp;
} else if (!tree.context && tree.props?.loader && tree.props?.path) {
// Add any loader on this node (but only on the VNode)
const { params } = match;
const controller = new AbortController();
const request = createClientSideRequest(
Expand All @@ -102,16 +109,11 @@ function _traverseLoaders(
}
}

// Traverse ends here
if (isRouteButNotMatch) return outp;

// Traverse children
const entries = _traverseLoaders(
location,
tree.children || tree.props?.children,
base,
tree.type?.prototype instanceof Switch,
);
const children = tree.children ?? tree.props?.children;
if (isNullOrUndef(children)) return outp;

const entries = _traverseLoaders(location, children, base, _isSwitch(tree));
return [...outp, ...entries];
}

Expand Down Expand Up @@ -214,7 +216,7 @@ function createClientSideRequest(

// Request is undefined when running tests
if (process.env.NODE_ENV === 'test' && typeof Request === 'undefined') {
// @ts-expect-error global req
// @ts-expect-error minimum to fix tests
global.Request = class Request {
public url;
public signal;
Expand Down

0 comments on commit d05dc2d

Please sign in to comment.