From c4ecb979b3401316442e30a7574b3a97e2674a38 Mon Sep 17 00:00:00 2001 From: William Chargin Date: Wed, 15 Aug 2018 16:44:28 -0700 Subject: [PATCH] Extract `Assets` from router with `withAssets` (#674) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/withAssets.js | 31 +++++++++++ src/app/withAssets.test.js | 111 +++++++++++++++++++++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/app/withAssets.js create mode 100644 src/app/withAssets.test.js diff --git a/src/app/withAssets.js b/src/app/withAssets.js new file mode 100644 index 0000000..deb3443 --- /dev/null +++ b/src/app/withAssets.js @@ -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( + C: ComponentType +): ComponentType<{...$Diff, router: Router}> { + const result = class WithAssets extends React.Component<{ + ...$Diff, + 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 ; + } + }; + result.displayName = `withAssets(${C.displayName || C.name || "Component"})`; + return result; +} diff --git a/src/app/withAssets.test.js b/src/app/withAssets.test.js new file mode 100644 index 0000000..c5463d6 --- /dev/null +++ b/src/app/withAssets.test.js @@ -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 ( +
+ favicon +
+ ); + } + } + + class CaptionedFaviconRenderer extends React.Component<{| + +assets: Assets, + +children: ReactNode, + |}> { + render() { + const {assets, children} = this.props; + return ( +
+ favicon +
{children}
+
+ ); + } + } + + it("enhances a component with no extra props", () => { + const {relativeHistory} = createHistory("/foo/", "/foo/bar/"); + const component = ( + + + + ); + 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 our favicon; + } + } + const component = ( + + + + + + ); + 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 click here; + } + } + class Caption extends React.Component<{||}> { + render() { + return our favicon; + } + } + const component = ( + + + + + + + ); + 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"); + }); +});