Extract `Assets` from router with `withAssets` (#674)
Summary: This is the last piece of major infrastructure for #643. It will enable components like `Page` and `CredExplorerApp` to receive `Assets` as a prop. A previous iteration of the same functionality used the new Context API in React v16.3. This did a good job of solving the problem in production code, and was convenient. However, it is currently intractable to test with the current state of Enzyme. It’s plausible that this might improve in the future, so if threading down the props becomes to onerous, we might check in to see how our testing libraries are doing. I expect that the threading should not be too bad, given that we do the same thing with `localStore`, which has worked (as far as I’m aware) without a hitch. Test Plan: Unit tests added; `yarn test` suffices. wchargin-branch: withAssets
This commit is contained in:
parent
4bbbfeebdb
commit
c4ecb979b3
|
@ -0,0 +1,31 @@
|
|||
// @flow
|
||||
|
||||
import React, {type ComponentType} from "react";
|
||||
import type {Router} from "react-router";
|
||||
|
||||
import {Assets} from "./assets";
|
||||
|
||||
// Higher-order component to serve as an adapter between React Router
|
||||
// and `Assets`.
|
||||
export default function withAssets<Props: {}>(
|
||||
C: ComponentType<Props>
|
||||
): ComponentType<{...$Diff<Props, {assets: Assets | void}>, router: Router}> {
|
||||
const result = class WithAssets extends React.Component<{
|
||||
...$Diff<Props, {assets: Assets | void}>,
|
||||
router: Router,
|
||||
}> {
|
||||
_assets: ?Assets;
|
||||
render() {
|
||||
const assets: Assets = new Assets(this.props.router.createHref("/"));
|
||||
if (
|
||||
this._assets == null ||
|
||||
this._assets.resolve("") !== assets.resolve("")
|
||||
) {
|
||||
this._assets = assets;
|
||||
}
|
||||
return <C {...this.props} assets={assets} />;
|
||||
}
|
||||
};
|
||||
result.displayName = `withAssets(${C.displayName || C.name || "Component"})`;
|
||||
return result;
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// @flow
|
||||
|
||||
import React, {type Node as ReactNode} from "react";
|
||||
import {IndexRoute, Link, Router, Route} from "react-router";
|
||||
import {mount, render} from "enzyme";
|
||||
|
||||
import {Assets} from "./assets";
|
||||
import withAssets from "./withAssets";
|
||||
|
||||
import createMemoryHistory from "history/lib/createMemoryHistory";
|
||||
import createRelativeHistory from "./createRelativeHistory";
|
||||
|
||||
require("./testUtil").configureEnzyme();
|
||||
|
||||
describe("app/withAssets", () => {
|
||||
function createHistory(basename, path) {
|
||||
const memoryHistory = createMemoryHistory(path);
|
||||
const relativeHistory = createRelativeHistory(memoryHistory, basename);
|
||||
return {memoryHistory, relativeHistory};
|
||||
}
|
||||
|
||||
class FaviconRenderer extends React.Component<{|+assets: Assets|}> {
|
||||
render() {
|
||||
const {assets} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<img alt="favicon" src={assets.resolve("/favicon.png")} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CaptionedFaviconRenderer extends React.Component<{|
|
||||
+assets: Assets,
|
||||
+children: ReactNode,
|
||||
|}> {
|
||||
render() {
|
||||
const {assets, children} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<img alt="favicon" src={assets.resolve("/favicon.png")} />
|
||||
<figcaption>{children}</figcaption>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
it("enhances a component with no extra props", () => {
|
||||
const {relativeHistory} = createHistory("/foo/", "/foo/bar/");
|
||||
const component = (
|
||||
<Router history={relativeHistory}>
|
||||
<Route path="/bar/" component={withAssets(FaviconRenderer)} />
|
||||
</Router>
|
||||
);
|
||||
const e = render(component);
|
||||
expect(e.find("img").attr("src")).toEqual("../favicon.png");
|
||||
});
|
||||
|
||||
it("enhances a component with children", () => {
|
||||
const {relativeHistory} = createHistory("/foo/", "/foo/bar/baz/");
|
||||
class Caption extends React.Component<{||}> {
|
||||
render() {
|
||||
return <span>our favicon</span>;
|
||||
}
|
||||
}
|
||||
const component = (
|
||||
<Router history={relativeHistory}>
|
||||
<Route path="/bar/" component={withAssets(CaptionedFaviconRenderer)}>
|
||||
<Route path="/bar/baz/" component={Caption} />
|
||||
</Route>
|
||||
</Router>
|
||||
);
|
||||
const e = render(component);
|
||||
expect(e.find("img").attr("src")).toEqual("../../favicon.png");
|
||||
expect(e.find("figcaption").text()).toEqual("our favicon");
|
||||
});
|
||||
|
||||
it("updates on page change", () => {
|
||||
const {relativeHistory} = createHistory("/foo/", "/foo/bar/");
|
||||
class LinkToCaption extends React.Component<{||}> {
|
||||
render() {
|
||||
return <Link to="/bar/captioned/">click here</Link>;
|
||||
}
|
||||
}
|
||||
class Caption extends React.Component<{||}> {
|
||||
render() {
|
||||
return <span>our favicon</span>;
|
||||
}
|
||||
}
|
||||
const component = (
|
||||
<Router history={relativeHistory}>
|
||||
<Route path="/bar/" component={withAssets(CaptionedFaviconRenderer)}>
|
||||
<IndexRoute component={LinkToCaption} />
|
||||
<Route path="/bar/captioned/" component={Caption} />
|
||||
</Route>
|
||||
</Router>
|
||||
);
|
||||
const e = mount(component);
|
||||
expect(e.find("img").prop("src")).toEqual("../favicon.png");
|
||||
expect(e.find("figcaption").text()).toEqual("click here");
|
||||
function click(link) {
|
||||
// React Router only transitions if the event appears to be from
|
||||
// a left-click (button index 0) event on a mouse.
|
||||
const event = {button: 0};
|
||||
link.simulate("click", event);
|
||||
}
|
||||
click(e.find("a"));
|
||||
expect(e.find("img").prop("src")).toEqual("../../favicon.png");
|
||||
expect(e.find("figcaption").text()).toEqual("our favicon");
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue