mirror of
https://github.com/status-im/sourcecred.git
synced 2025-02-23 09:48:14 +00:00
Setup React Admin frontend (#1888)
* Setup React Admin frontend
This configures ReactAdmin to render as the frontend with a simple "fake data provider".
SSR issues need to be addressed before the UI will be visible
TestPlan: Ensure the frontend builds when running yarn start
* Fix failing react-admin build
This mocks out the testing-library modules which were causing issues in the frontend build and adds temporary
patches on postinstall for the other problematic react-admin libraries. We can remove the patches once they merge
the PR that fixes this upstream: https://github.com/marmelab/react-admin/pull/4970
Test Plan: Run yarn start --instance <instance location> to ensure the frontend builds without errors
* Update history to compatible version
Updates the history package to a version compatible with the React Router v4
Test Plan: Run yarn start --instance <instance location> and ensure the
React Admin UI renders and navigation works
* Fix flow errors importing from history package
v4 of the history package updated the import format, updated our usage to match.
More info: 845d690c55/CHANGES.md (v400-0)
TestPlan: Ensure flow check passes
* Add flow types for History and React-Router
Better type safety is always nice to have :)
TestPlan: Ensure versions for typings match installed versions of history and react-router
* remove withAssets and createRelativeHistory
withAssets is no longer needed since we have the Docusaurus site to fulfill those purposes and
our homepage is no longer being generated by this UI.
RelativeHistroy is no longer needed with Router V5 because this version
of the router accepts a `basename` parameter
TestPlan: Ensure that nothing is still using functionality from these two modules
* Update Link component to react-router v5
React router v5 has moved its Link component into a separate package: react-router-dom. This PR
just updates the place we import the Link component for our own internal wrapper around it.
TestPlan: Ensure that our Link component is still functional
* Remove react-admin patches
React admin merged my PR that fixes the issues with window (https://github.com/marmelab/react-admin/pull/4970),
so we no longer need these patches since the issue is addressed upstream now.
TestPlan: Ensure that the site builds and runs without "window is not defined" errors
Co-authored-by: Kevin Siegler <kevinsiegler54@gmail.com>
This commit is contained in:
parent
aab5c4ab9b
commit
8140738da3
@ -10,6 +10,7 @@ import type {
|
||||
const path = require("path");
|
||||
const fs = require("fs-extra");
|
||||
const webpack = require("webpack");
|
||||
|
||||
const RemoveBuildDirectoryPlugin = require("./RemoveBuildDirectoryPlugin");
|
||||
const CopyPlugin = require("copy-webpack-plugin");
|
||||
const ManifestPlugin = require("webpack-manifest-plugin");
|
||||
@ -163,6 +164,10 @@ async function makeConfig(
|
||||
// match the requirements. When no loader matches it will fall
|
||||
// back to the "file" loader at the end of the loader list.
|
||||
oneOf: [
|
||||
{
|
||||
test: [/@testing-library\/dom/, /@testing-library\/react/],
|
||||
use: "null-loader",
|
||||
},
|
||||
// "url" loader works just like "file" loader but it also embeds
|
||||
// assets smaller than specified size as data URLs to avoid requests.
|
||||
{
|
||||
|
175
flow-typed/npm/history_v4.x.x.js
vendored
Normal file
175
flow-typed/npm/history_v4.x.x.js
vendored
Normal file
@ -0,0 +1,175 @@
|
||||
// flow-typed signature: fa6e6f9d8810d431fc95f1b20933c827
|
||||
// flow-typed version: 45d63d67fa/history_v4.x.x/flow_>=v0.104.x
|
||||
|
||||
declare module "history/createBrowserHistory" {
|
||||
declare function Unblock(): void;
|
||||
|
||||
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type BrowserLocation = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
// Browser and Memory specific
|
||||
state: {...},
|
||||
key: string,
|
||||
...
|
||||
};
|
||||
|
||||
declare export interface BrowserHistory {
|
||||
length: number,
|
||||
location: $Shape<BrowserLocation>,
|
||||
action: Action,
|
||||
push(path: string, state?: {...}): void,
|
||||
push(location: $Shape<BrowserLocation>): void,
|
||||
replace(path: string, state?: {...}): void,
|
||||
replace(location: $Shape<BrowserLocation>): void,
|
||||
go(n: number): void,
|
||||
goBack(): void,
|
||||
goForward(): void,
|
||||
listen: Function,
|
||||
block(message: string): typeof Unblock,
|
||||
block((location: BrowserLocation, action: Action) => string): typeof Unblock,
|
||||
}
|
||||
|
||||
declare type BrowserHistoryOpts = {
|
||||
basename?: string,
|
||||
forceRefresh?: boolean,
|
||||
getUserConfirmation?: (
|
||||
message: string,
|
||||
callback: (willContinue: boolean) => void,
|
||||
) => void,
|
||||
...
|
||||
};
|
||||
|
||||
declare export default (opts?: BrowserHistoryOpts) => BrowserHistory;
|
||||
}
|
||||
|
||||
declare module "history/createMemoryHistory" {
|
||||
declare function Unblock(): void;
|
||||
|
||||
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type MemoryLocation = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
// Browser and Memory specific
|
||||
state: {...},
|
||||
key: string,
|
||||
...
|
||||
};
|
||||
|
||||
declare export interface MemoryHistory {
|
||||
length: number,
|
||||
location: $Shape<MemoryLocation>,
|
||||
action: Action,
|
||||
index: number,
|
||||
entries: Array<MemoryLocation | string>,
|
||||
push(path: string, state?: {...}): void,
|
||||
push(location: $Shape<MemoryLocation>): void,
|
||||
replace(path: string, state?: {...}): void,
|
||||
replace(location: $Shape<MemoryLocation>): void,
|
||||
go(n: number): void,
|
||||
goBack(): void,
|
||||
goForward(): void,
|
||||
// Memory only
|
||||
canGo?: (n: number) => boolean,
|
||||
listen: Function,
|
||||
block(message: string): typeof Unblock,
|
||||
block((location: MemoryLocation, action: Action) => ?string): typeof Unblock,
|
||||
}
|
||||
|
||||
declare type MemoryHistoryOpts = {
|
||||
initialEntries?: Array<MemoryLocation | string>,
|
||||
initialIndex?: number,
|
||||
keyLength?: number,
|
||||
getUserConfirmation?: (
|
||||
message: string,
|
||||
callback: (willContinue: boolean) => void,
|
||||
) => void,
|
||||
...
|
||||
};
|
||||
|
||||
declare export default (opts?: MemoryHistoryOpts) => MemoryHistory;
|
||||
}
|
||||
|
||||
declare module "history/createHashHistory" {
|
||||
declare function Unblock(): void;
|
||||
|
||||
declare export type Action = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type HashLocation = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
...
|
||||
};
|
||||
|
||||
declare export interface HashHistory {
|
||||
length: number,
|
||||
location: $Shape<HashLocation>,
|
||||
action: Action,
|
||||
push(path: string, state?: {...}): void,
|
||||
push(location: $Shape<HashLocation>): void,
|
||||
replace(path: string, state?: {...}): void,
|
||||
replace(location: $Shape<HashLocation>): void,
|
||||
go(n: number): void,
|
||||
goBack(): void,
|
||||
goForward(): void,
|
||||
listen: Function,
|
||||
block(message: string): typeof Unblock,
|
||||
block((location: HashLocation, action: Action) => string): typeof Unblock,
|
||||
push(path: string): void,
|
||||
}
|
||||
|
||||
declare type HashHistoryOpts = {
|
||||
basename?: string,
|
||||
hashType: "slash" | "noslash" | "hashbang",
|
||||
getUserConfirmation?: (
|
||||
message: string,
|
||||
callback: (willContinue: boolean) => void,
|
||||
) => void,
|
||||
...
|
||||
};
|
||||
|
||||
declare export default (opts?: HashHistoryOpts) => HashHistory;
|
||||
}
|
||||
|
||||
declare module 'history' {
|
||||
import typeof CreateMemoryHistory from 'history/createMemoryHistory';
|
||||
import typeof CreateHashHistory from 'history/createHashHistory';
|
||||
import typeof CreateBrowserHistory from 'history/createBrowserHistory';
|
||||
|
||||
import type {
|
||||
MemoryHistory,
|
||||
MemoryLocation,
|
||||
MemoryHistoryOpts
|
||||
} from 'history/createMemoryHistory';
|
||||
import type {
|
||||
HashHistory,
|
||||
HashLocation,
|
||||
HashHistoryOpts
|
||||
} from 'history/createHashHistory';
|
||||
import type {
|
||||
BrowserHistory,
|
||||
BrowserLocation,
|
||||
BrowserHistoryOpts
|
||||
} from 'history/createBrowserHistory';
|
||||
|
||||
declare module.exports: {|
|
||||
createMemoryHistory: CreateMemoryHistory,
|
||||
createHashHistory: CreateHashHistory,
|
||||
createBrowserHistory: CreateBrowserHistory,
|
||||
HashHistory: HashHistory,
|
||||
HashLocation: HashHistory,
|
||||
HashHistoryOpts: HashHistory,
|
||||
MemoryHistory: MemoryHistory,
|
||||
MemoryLocation: MemoryHistory,
|
||||
MemoryHistoryOpts: MemoryHistory,
|
||||
BrowserHistory: BrowserHistory,
|
||||
BrowserLocation: BrowserHistory,
|
||||
BrowserHistoryOpts: BrowserHistory,
|
||||
Action: 'PUSH' | 'REPLACE' | 'POP'
|
||||
|}
|
||||
}
|
156
flow-typed/npm/react-router_v5.x.x.js
vendored
Normal file
156
flow-typed/npm/react-router_v5.x.x.js
vendored
Normal file
@ -0,0 +1,156 @@
|
||||
// flow-typed signature: 38d16d2099bb076f9f375a333a268246
|
||||
// flow-typed version: 45d63d67fa/react-router_v5.x.x/flow_>=v0.104.x
|
||||
|
||||
declare module "react-router" {
|
||||
// NOTE: many of these are re-exported by react-router-dom and
|
||||
// react-router-native, so when making changes, please be sure to update those
|
||||
// as well.
|
||||
declare export type Location = {
|
||||
pathname: string,
|
||||
search: string,
|
||||
hash: string,
|
||||
...
|
||||
};
|
||||
|
||||
declare export type LocationShape = {
|
||||
pathname?: string,
|
||||
search?: string,
|
||||
hash?: string,
|
||||
...
|
||||
};
|
||||
|
||||
declare export type HistoryAction = "PUSH" | "REPLACE" | "POP";
|
||||
|
||||
declare export type RouterHistory = {
|
||||
length: number,
|
||||
location: Location,
|
||||
action: HistoryAction,
|
||||
listen(
|
||||
callback: (location: Location, action: HistoryAction) => void
|
||||
): () => void,
|
||||
push(path: string | LocationShape, state?: any): void,
|
||||
replace(path: string | LocationShape, state?: any): void,
|
||||
go(n: number): void,
|
||||
goBack(): void,
|
||||
goForward(): void,
|
||||
canGo?: (n: number) => boolean,
|
||||
block(
|
||||
callback: string | (location: Location, action: HistoryAction) => ?string
|
||||
): () => void,
|
||||
...
|
||||
};
|
||||
|
||||
declare export type Match = {
|
||||
params: { [key: string]: ?string, ... },
|
||||
isExact: boolean,
|
||||
path: string,
|
||||
url: string,
|
||||
...
|
||||
};
|
||||
|
||||
declare export type ContextRouter = {|
|
||||
history: RouterHistory,
|
||||
location: Location,
|
||||
match: Match,
|
||||
staticContext?: StaticRouterContext
|
||||
|};
|
||||
|
||||
declare export type GetUserConfirmation = (
|
||||
message: string,
|
||||
callback: (confirmed: boolean) => void
|
||||
) => void;
|
||||
|
||||
declare type StaticRouterContext = { url?: string, ... };
|
||||
|
||||
declare export class StaticRouter extends React$Component<{
|
||||
basename?: string,
|
||||
location?: string | Location,
|
||||
context: StaticRouterContext,
|
||||
children?: React$Node,
|
||||
...
|
||||
}> {}
|
||||
|
||||
declare export class MemoryRouter extends React$Component<{
|
||||
initialEntries?: Array<LocationShape | string>,
|
||||
initialIndex?: number,
|
||||
getUserConfirmation?: GetUserConfirmation,
|
||||
keyLength?: number,
|
||||
children?: React$Node,
|
||||
...
|
||||
}> {}
|
||||
|
||||
declare export class Router extends React$Component<{
|
||||
history: RouterHistory,
|
||||
children?: React$Node,
|
||||
...
|
||||
}> {}
|
||||
|
||||
declare export class Prompt extends React$Component<{
|
||||
message: string | ((location: Location) => string | true),
|
||||
when?: boolean,
|
||||
...
|
||||
}> {}
|
||||
|
||||
declare export class Redirect extends React$Component<{|
|
||||
to: string | LocationShape,
|
||||
push?: boolean,
|
||||
from?: string,
|
||||
exact?: boolean,
|
||||
strict?: boolean
|
||||
|}> {}
|
||||
|
||||
|
||||
declare export class Route extends React$Component<{|
|
||||
component?: React$ComponentType<*>,
|
||||
render?: (router: ContextRouter) => React$Node,
|
||||
children?: React$ComponentType<ContextRouter> | React$Node,
|
||||
path?: string | Array<string>,
|
||||
exact?: boolean,
|
||||
strict?: boolean,
|
||||
location?: LocationShape,
|
||||
sensitive?: boolean
|
||||
|}> {}
|
||||
|
||||
declare export class Switch extends React$Component<{|
|
||||
children?: React$Node,
|
||||
location?: Location
|
||||
|}> {}
|
||||
|
||||
declare export function withRouter<P>(
|
||||
Component: React$ComponentType<{| ...ContextRouter, ...P |}>
|
||||
): React$ComponentType<P>;
|
||||
|
||||
declare type MatchPathOptions = {
|
||||
path?: string | string[],
|
||||
exact?: boolean,
|
||||
strict?: boolean,
|
||||
sensitive?: boolean,
|
||||
...
|
||||
};
|
||||
|
||||
declare export function matchPath(
|
||||
pathname: string,
|
||||
options?: MatchPathOptions | string | string[]
|
||||
): null | Match;
|
||||
|
||||
declare export function useHistory(): $PropertyType<ContextRouter, 'history'>;
|
||||
declare export function useLocation(): $PropertyType<ContextRouter, 'location'>;
|
||||
declare export function useParams(): $PropertyType<$PropertyType<ContextRouter, 'match'>, 'params'>;
|
||||
declare export function useRouteMatch(path?: MatchPathOptions | string | string[]): $PropertyType<ContextRouter, 'match'>;
|
||||
|
||||
declare export function generatePath(pattern?: string, params?: {...}): string;
|
||||
|
||||
declare export default {
|
||||
StaticRouter: typeof StaticRouter,
|
||||
MemoryRouter: typeof MemoryRouter,
|
||||
Router: typeof Router,
|
||||
Prompt: typeof Prompt,
|
||||
Redirect: typeof Redirect,
|
||||
Route: typeof Route,
|
||||
Switch: typeof Switch,
|
||||
withRouter: typeof withRouter,
|
||||
matchPath: typeof matchPath,
|
||||
generatePath: typeof generatePath,
|
||||
...
|
||||
};
|
||||
}
|
10
package.json
10
package.json
@ -24,7 +24,7 @@
|
||||
"express": "^4.17.1",
|
||||
"fs-extra": "^9.0.0",
|
||||
"globby": "^11.0.0",
|
||||
"history": "^3.3.0",
|
||||
"history": "^4.10.1",
|
||||
"htmlparser2": "^4.1.0",
|
||||
"isomorphic-fetch": "^2.2.1",
|
||||
"isomorphic-webcrypto": "^2.3.6",
|
||||
@ -35,11 +35,15 @@
|
||||
"object-assign": "^4.1.1",
|
||||
"pako": "^1.0.11",
|
||||
"promise": "^8.1.0",
|
||||
"ra-data-fakerest": "^3.6.2",
|
||||
"react": "^16.13.0",
|
||||
"react-admin": "^3.6.2",
|
||||
"react-dom": "^16.13.0",
|
||||
"react-icons": "^3.9.0",
|
||||
"react-markdown": "^4.3.1",
|
||||
"react-router": "^3.2.6",
|
||||
"react-router": "^5.2.0",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"recharts": "^1.8.5",
|
||||
"remove-markdown": "^0.3.0",
|
||||
"retry": "^0.12.0",
|
||||
"rimraf": "^3.0.2",
|
||||
@ -74,6 +78,8 @@
|
||||
"jest": "^26.0.1",
|
||||
"jest-fetch-mock": "^3.0.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"null-loader": "^4.0.0",
|
||||
"postinstall-postinstall": "^2.1.0",
|
||||
"prettier": "^2.0.1",
|
||||
"raf": "^3.4.1",
|
||||
"react-dev-utils": "^5.0.3",
|
||||
|
@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
|
||||
import App from "./App";
|
||||
|
||||
const target = document.getElementById("root");
|
||||
|
@ -3,6 +3,7 @@
|
||||
import {StyleSheetServer} from "aphrodite/no-important";
|
||||
import React from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import {StaticRouter} from "react-router";
|
||||
|
||||
import dedent from "../util/dedent";
|
||||
import {Assets, rootFromPath} from "../webutil/assets";
|
||||
@ -15,13 +16,18 @@ export default function render(
|
||||
const path = locals.path;
|
||||
const root = rootFromPath(path);
|
||||
const assets = new Assets(root);
|
||||
const context = {};
|
||||
|
||||
return renderStandardRoute();
|
||||
|
||||
function renderStandardRoute() {
|
||||
const bundlePath = locals.assets["main"];
|
||||
const component = <App />;
|
||||
const {html, css} = StyleSheetServer.renderStatic(() =>
|
||||
ReactDOMServer.renderToString(component)
|
||||
ReactDOMServer.renderToString(
|
||||
<StaticRouter location="/" context={context}>
|
||||
<App />
|
||||
</StaticRouter>
|
||||
)
|
||||
);
|
||||
const page = dedent`\
|
||||
<!DOCTYPE html>
|
||||
|
@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
import React, {Component} from "react";
|
||||
import {Link as RouterLink} from "react-router";
|
||||
import {Link as RouterLink} from "react-router-dom";
|
||||
import {StyleSheet, css} from "aphrodite/no-important";
|
||||
|
||||
import Colors from "./Colors";
|
||||
|
@ -2,7 +2,7 @@
|
||||
import {StyleSheet} from "aphrodite/no-important";
|
||||
import {shallow} from "enzyme";
|
||||
import React from "react";
|
||||
import {Link as RouterLink} from "react-router";
|
||||
import {Link as RouterLink} from "react-router-dom";
|
||||
|
||||
import Link from "./Link";
|
||||
|
||||
|
@ -1,229 +0,0 @@
|
||||
// @flow
|
||||
|
||||
/*
|
||||
* This module mediates between three representations of paths:
|
||||
*
|
||||
* - In React-space, paths are absolute, rooted semantically with
|
||||
* respect to the application.
|
||||
* - In browser-space, paths are absolute, rooted with respect to the
|
||||
* actual host.
|
||||
* - In DOM-space, paths are relative.
|
||||
*
|
||||
* For instance, suppose that an application is being served from
|
||||
* http://example.com/gateway/. Suppose that we are on the "about us"
|
||||
* page, with route "/about-us/". Then the route for the "contact us"
|
||||
* page has the following three representations:
|
||||
*
|
||||
* - in React-space: "/contact-us/";
|
||||
* - in browser-space: "/gateway/contact-us/";
|
||||
* - in DOM-space: "../contact-us/".
|
||||
*
|
||||
* These different spaces interact as follows:
|
||||
*
|
||||
* - Actual interaction with the `window.history` API uses
|
||||
* browser-space. This is necessary/convenient because
|
||||
* `window.location` is always represented in browser-space.
|
||||
*
|
||||
* - Interactions with React Router are in React-space. In particular,
|
||||
* the result of `getCurrentLocation()` is in React-space, and the
|
||||
* argument to `createHref` is in React-space. This is
|
||||
* necessary/convenient because it is an assumption of React Router
|
||||
* (e.g., actual route data must be specified thus).
|
||||
*
|
||||
* - The result of `createHref` is in DOM-space. This is
|
||||
* necessary/convenient because an `a` element must have a relative
|
||||
* href, because the gateway is not known at the time that the
|
||||
* static site is generated.
|
||||
*
|
||||
* Use `createRelativeHistory` to get a history object that provides the
|
||||
* right interface to each client.
|
||||
*/
|
||||
|
||||
import type {History /* actually `any` */} from "history";
|
||||
|
||||
/**
|
||||
* Given a history implementation that operates in browser-space with
|
||||
* the provided basename, create a history implementation that operates
|
||||
* in React-space, except for `createHref`, which provides results in
|
||||
* DOM-space.
|
||||
*
|
||||
* In a server-side rendering context, the basename should be "/". On
|
||||
* the client, the basename depends on the particular gateway from which
|
||||
* the page is served, which is known only at runtime and must be
|
||||
* computed from `window.location.pathname`.
|
||||
*
|
||||
* For instance, if `window.location.pathname` is "/foo/bar/about-us/",
|
||||
* and we are rendering what is semantically the "/about-us/" route,
|
||||
* then `basename` should be "/foo/bar/".
|
||||
*
|
||||
* The basename must begin and end with a slash. (These may be the same
|
||||
* slash.)
|
||||
*
|
||||
* See module docstring for more details.
|
||||
*/
|
||||
export default function createRelativeHistory(
|
||||
delegate: History,
|
||||
basename: string
|
||||
): History {
|
||||
if (!delegate.getCurrentLocation) {
|
||||
// (The `Router` component of `react-router` uses the same check.)
|
||||
throw new Error(
|
||||
"delegate: expected history@3 implementation, got: " + String(delegate)
|
||||
);
|
||||
}
|
||||
if (typeof basename !== "string") {
|
||||
throw new Error("basename: expected string, got: " + basename);
|
||||
}
|
||||
if (!basename.startsWith("/")) {
|
||||
throw new Error("basename: must be absolute: " + basename);
|
||||
}
|
||||
if (!basename.endsWith("/")) {
|
||||
throw new Error("basename: must end in slash: " + basename);
|
||||
}
|
||||
verifyBasename(delegate.getCurrentLocation().pathname);
|
||||
|
||||
interface Lens {
|
||||
(pathname: string): string;
|
||||
<T: {+pathname: string}>(location: T): T;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a function that transforms a pathname, return a function
|
||||
* that:
|
||||
* - transforms strings by interpreting them as pathnames;
|
||||
* - transforms location objects by transforming their pathnames;
|
||||
* - passes through `null` and `undefined` unchanged, with warning.
|
||||
*/
|
||||
function lens(transformPathname: (string) => string): Lens {
|
||||
return (value) => {
|
||||
// istanbul ignore if
|
||||
if (value == null) {
|
||||
console.warn("unexpected lens argument: " + String(value));
|
||||
// Pass through unchanged.
|
||||
return value;
|
||||
} else if (typeof value === "string") {
|
||||
return (transformPathname(value): any);
|
||||
} else {
|
||||
const pathname = transformPathname(value.pathname);
|
||||
return ({...value, pathname}: any);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/*
|
||||
* Check that the provided browser-space path does indeed begin with
|
||||
* the expected basename. If it doesn't, this means that we somehow
|
||||
* navigated out of our "sandbox" (maybe someone manually called
|
||||
* `window.history.pushState`). All bets are off in that case.
|
||||
*/
|
||||
function verifyBasename(browserPath) {
|
||||
if (!browserPath.startsWith(basename)) {
|
||||
const p = JSON.stringify(browserPath);
|
||||
const b = JSON.stringify(basename);
|
||||
throw new Error(`basename violation: ${b} is not a prefix of ${p}`);
|
||||
}
|
||||
}
|
||||
|
||||
const reactToBrowser = lens((path) => basename + path.replace(/^\//, ""));
|
||||
const browserToReact = lens((path) => {
|
||||
verifyBasename(path);
|
||||
return "/" + path.slice(basename.length);
|
||||
});
|
||||
const browserToDom = lens((path) => {
|
||||
verifyBasename(path);
|
||||
const current = delegate.getCurrentLocation().pathname;
|
||||
verifyBasename(current);
|
||||
const relativeRoot = current
|
||||
.slice(basename.length)
|
||||
// Strip any file component in the current directory.
|
||||
.replace(/\/[^/]*$/, "/")
|
||||
// Traverse back up any intermediate directory.
|
||||
.replace(/[^/]+/g, "..");
|
||||
return relativeRoot + path.slice(basename.length);
|
||||
});
|
||||
|
||||
function getCurrentLocation() {
|
||||
return browserToReact(delegate.getCurrentLocation());
|
||||
}
|
||||
function listenBefore(listener) {
|
||||
// Result is a function `unlisten: () => void`; no need to
|
||||
// transform.
|
||||
return delegate.listenBefore((currentLocation) => {
|
||||
return listener(browserToReact(currentLocation));
|
||||
});
|
||||
}
|
||||
function listen(listener) {
|
||||
// Result is a function `unlisten: () => void`; no need to
|
||||
// transform.
|
||||
return delegate.listen((currentLocation) => {
|
||||
return listener(browserToReact(currentLocation));
|
||||
});
|
||||
}
|
||||
function transitionTo(location) {
|
||||
// Result is `undefined`; no need to transform.
|
||||
return delegate.transitionTo(reactToBrowser(location));
|
||||
}
|
||||
function push(location) {
|
||||
// Result is `undefined`; no need to transform.
|
||||
return delegate.push(reactToBrowser(location));
|
||||
}
|
||||
function replace(location) {
|
||||
// Result is `undefined`; no need to transform.
|
||||
return delegate.replace(reactToBrowser(location));
|
||||
}
|
||||
function go(n) {
|
||||
// Result is `undefined`; no need to transform.
|
||||
// `n` is an integer; no need to transform.
|
||||
return delegate.go(n);
|
||||
}
|
||||
function goBack() {
|
||||
// Result is `undefined`; no need to transform.
|
||||
return delegate.goBack();
|
||||
}
|
||||
function goForward() {
|
||||
// Result is `undefined`; no need to transform.
|
||||
return delegate.goForward();
|
||||
}
|
||||
function createKey() {
|
||||
// Result is not a path; no need to transform.
|
||||
return delegate.createKey();
|
||||
}
|
||||
function createPath(_unused_location) {
|
||||
// It is not clear whether this function is part of the public
|
||||
// API. If it is, it is not clear what kind of URL (which
|
||||
// representation space) it is supposed to return. This is because
|
||||
// the `history` module does not actually have any API docs. This
|
||||
// function is not called by React Router v3, so, given that we do
|
||||
// not know what the semantics should be, we refrain from
|
||||
// implementing it.
|
||||
//
|
||||
// If this ever throws, maybe we'll have a better idea of what to
|
||||
// do.
|
||||
throw new Error("createPath is not part of the public API");
|
||||
}
|
||||
function createHref(location) {
|
||||
return browserToDom(delegate.createHref(reactToBrowser(location)));
|
||||
}
|
||||
function createLocation(location, action) {
|
||||
// `action` is an enum constant ("POP", "PUSH", or "REPLACE"); no
|
||||
// need to transform it.
|
||||
return browserToReact(
|
||||
delegate.createLocation(reactToBrowser(location), action)
|
||||
);
|
||||
}
|
||||
return {
|
||||
getCurrentLocation,
|
||||
listenBefore,
|
||||
listen,
|
||||
transitionTo,
|
||||
push,
|
||||
replace,
|
||||
go,
|
||||
goBack,
|
||||
goForward,
|
||||
createKey,
|
||||
createPath,
|
||||
createHref,
|
||||
createLocation,
|
||||
};
|
||||
}
|
@ -1,704 +0,0 @@
|
||||
// @flow
|
||||
|
||||
import React, {type Node as ReactNode} from "react";
|
||||
import {Router, Route, Link} from "react-router";
|
||||
import {mount, render} from "enzyme";
|
||||
|
||||
import normalize from "../util/pathNormalize";
|
||||
import type {History /* actually `any` */} from "history";
|
||||
import createMemoryHistory from "history/lib/createMemoryHistory";
|
||||
import createRelativeHistory from "./createRelativeHistory";
|
||||
|
||||
require("./testUtil").configureEnzyme();
|
||||
|
||||
describe("webutil/createRelativeHistory", () => {
|
||||
function createHistory(basename: string, path: string) {
|
||||
const memoryHistory = createMemoryHistory(path);
|
||||
const relativeHistory = createRelativeHistory(memoryHistory, basename);
|
||||
return {memoryHistory, relativeHistory};
|
||||
}
|
||||
|
||||
describe("by direct interaction", () => {
|
||||
describe("construction", () => {
|
||||
it("should require a valid `history` implementation", () => {
|
||||
const historyV4Object = {
|
||||
length: 1,
|
||||
action: "POP",
|
||||
location: {
|
||||
pathname: "/foo/",
|
||||
search: "",
|
||||
hash: "",
|
||||
key: "123456",
|
||||
state: undefined,
|
||||
},
|
||||
createHref: () => "wat",
|
||||
push: () => undefined,
|
||||
replace: () => undefined,
|
||||
go: () => undefined,
|
||||
goBack: () => undefined,
|
||||
goForward: () => undefined,
|
||||
canGo: () => true,
|
||||
block: () => undefined,
|
||||
listen: () => undefined,
|
||||
};
|
||||
expect(() => createRelativeHistory(historyV4Object, "/")).toThrow(
|
||||
"delegate: expected history@3 implementation, got: [object Object]"
|
||||
);
|
||||
});
|
||||
it("should require a basename", () => {
|
||||
expect(() =>
|
||||
createHistory(
|
||||
// $FlowExpectedError
|
||||
undefined,
|
||||
"undefined/"
|
||||
)
|
||||
).toThrow("basename: expected string, got: undefined");
|
||||
});
|
||||
it("should reject a basename that does not start with a slash", () => {
|
||||
expect(() =>
|
||||
createHistory("not-a-slash/", "not-a-slash/thing")
|
||||
).toThrow("basename: must be absolute: not-a-slash/");
|
||||
});
|
||||
it("should reject a basename that does not end with a slash", () => {
|
||||
expect(() => createHistory("/not-a-dir", "/not-a-dir/thing")).toThrow(
|
||||
"basename: must end in slash: /not-a-dir"
|
||||
);
|
||||
});
|
||||
it("should reject a basename that is not a prefix of the location", () => {
|
||||
expect(() => createHistory("/foo/bar/", "/not/foo/bar/")).toThrow(
|
||||
'basename violation: "/foo/bar/" is not a prefix of "/not/foo/bar/"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// We perform some minimal testing with a root basename. Most of the
|
||||
// interesting cases can only be usefully covered with a non-root
|
||||
// basename, and are unlikely to break only for a root basename, so
|
||||
// there's no need to duplicate the tests.
|
||||
describe('with a root basename ("/")', () => {
|
||||
it("should return React-space from `getCurrentLocation`", () => {
|
||||
const {memoryHistory, relativeHistory} = createHistory(
|
||||
"/",
|
||||
"/foo/bar/"
|
||||
);
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
"/foo/bar/"
|
||||
);
|
||||
memoryHistory.push("/baz/quux/");
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
"/baz/quux/"
|
||||
);
|
||||
});
|
||||
it("should return DOM-space from `createHref` at root", () => {
|
||||
expect(
|
||||
createHistory("/", "/").relativeHistory.createHref("/favicon.png")
|
||||
).toEqual("favicon.png");
|
||||
});
|
||||
it("should return DOM-space from `createHref` at non-root", () => {
|
||||
expect(
|
||||
createHistory("/", "/foo/bar/").relativeHistory.createHref(
|
||||
"/favicon.png"
|
||||
)
|
||||
).toEqual("../../favicon.png");
|
||||
});
|
||||
it("should accept a location string for `push`", () => {
|
||||
const {memoryHistory, relativeHistory} = createHistory(
|
||||
"/",
|
||||
"/foo/bar/"
|
||||
);
|
||||
relativeHistory.push("/baz/quux/#browns");
|
||||
expect(memoryHistory.getCurrentLocation()).toEqual(
|
||||
expect.objectContaining({
|
||||
pathname: "/baz/quux/",
|
||||
search: "",
|
||||
hash: "#browns",
|
||||
state: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
it("should accept a location object for `push`", () => {
|
||||
const {memoryHistory, relativeHistory} = createHistory(
|
||||
"/",
|
||||
"/foo/bar/"
|
||||
);
|
||||
relativeHistory.push({pathname: "/baz/quux/", hash: "#browns"});
|
||||
expect(memoryHistory.getCurrentLocation()).toEqual(
|
||||
expect.objectContaining({
|
||||
pathname: "/baz/quux/",
|
||||
search: "",
|
||||
hash: "#browns",
|
||||
state: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with a non-root basename ("/my/gateway/")', () => {
|
||||
const createStandardHistory = () =>
|
||||
createHistory("/my/gateway/", "/my/gateway/foo/bar/");
|
||||
|
||||
describe("getCurrentLocation", () => {
|
||||
it("should return the initial location, in React-space", () => {
|
||||
const {relativeHistory} = createStandardHistory();
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
"/foo/bar/"
|
||||
);
|
||||
});
|
||||
it("should accommodate changes in the delegate location", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
memoryHistory.push("/my/gateway/baz/quux/");
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
"/baz/quux/"
|
||||
);
|
||||
});
|
||||
it("should throw if the delegate moves out of basename scope", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
"/foo/bar/"
|
||||
);
|
||||
memoryHistory.push("/not/my/gateway/baz/quux/");
|
||||
expect(() => relativeHistory.getCurrentLocation()).toThrow(
|
||||
'basename violation: "/my/gateway/" is not ' +
|
||||
'a prefix of "/not/my/gateway/baz/quux/"'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listenBefore", () => {
|
||||
function testListener(target: "RELATIVE" | "MEMORY") {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
const listener = jest.fn();
|
||||
relativeHistory.listenBefore(listener);
|
||||
expect(listener).toHaveBeenCalledTimes(0);
|
||||
listener.mockImplementationOnce((newLocation) => {
|
||||
// We should _not_ already have transitioned. (Strictly,
|
||||
// this doesn't mean that the pathnames must not be
|
||||
// equal---an event could be fired if, say, only the hash
|
||||
// changes---but it suffices for our test cases.)
|
||||
expect(relativeHistory.getCurrentLocation().pathname).not.toEqual(
|
||||
newLocation.pathname
|
||||
);
|
||||
expect(newLocation.pathname).toEqual("/baz/quux/");
|
||||
expect(newLocation.hash).toEqual("#browns");
|
||||
expect(newLocation.search).toEqual("");
|
||||
});
|
||||
if (target === "RELATIVE") {
|
||||
relativeHistory.push("/baz/quux/#browns");
|
||||
} else if (target === "MEMORY") {
|
||||
memoryHistory.push("/my/gateway/baz/quux/#browns");
|
||||
} else {
|
||||
throw new Error((target: empty));
|
||||
}
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
it("should handle events fired on the relative history", () => {
|
||||
testListener("RELATIVE");
|
||||
});
|
||||
|
||||
it("should handle events fired on the delegate history", () => {
|
||||
testListener("MEMORY");
|
||||
});
|
||||
|
||||
it("should unlisten when asked", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
const listener = jest.fn();
|
||||
const unlisten = relativeHistory.listenBefore(listener);
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(0);
|
||||
memoryHistory.push("/my/gateway/baz/quux/#browns");
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
|
||||
unlisten();
|
||||
memoryHistory.push("/my/gateway/some/thing/else/");
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("listen", () => {
|
||||
function testListener(target: "RELATIVE" | "MEMORY") {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
const listener = jest.fn();
|
||||
relativeHistory.listen(listener);
|
||||
expect(listener).toHaveBeenCalledTimes(0);
|
||||
listener.mockImplementationOnce((newLocation) => {
|
||||
// We should already have transitioned.
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
newLocation.pathname
|
||||
);
|
||||
expect(newLocation.pathname).toEqual("/baz/quux/");
|
||||
expect(newLocation.hash).toEqual("#browns");
|
||||
expect(newLocation.search).toEqual("");
|
||||
});
|
||||
if (target === "RELATIVE") {
|
||||
relativeHistory.push("/baz/quux/#browns");
|
||||
} else if (target === "MEMORY") {
|
||||
memoryHistory.push("/my/gateway/baz/quux/#browns");
|
||||
} else {
|
||||
throw new Error((target: empty));
|
||||
}
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
}
|
||||
|
||||
it("should handle events fired on the relative history", () => {
|
||||
testListener("RELATIVE");
|
||||
});
|
||||
|
||||
it("should handle events fired on the delegate history", () => {
|
||||
testListener("MEMORY");
|
||||
});
|
||||
|
||||
it("should unlisten when asked", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
const listener = jest.fn();
|
||||
const unlisten = relativeHistory.listen(listener);
|
||||
|
||||
expect(listener).toHaveBeenCalledTimes(0);
|
||||
memoryHistory.push("/my/gateway/baz/quux/#browns");
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
|
||||
unlisten();
|
||||
memoryHistory.push("/my/gateway/some/thing/else/");
|
||||
expect(listener).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
// I have no idea what `transitionTo` is supposed to do. One would
|
||||
// think that it effects a transition, but one would be wrong:
|
||||
//
|
||||
// > var mh = require("history/lib/createMemoryHistory").default();
|
||||
// > mh.transitionTo("/foo/");
|
||||
// > mh.getCurrentLocation().pathname;
|
||||
// '/'
|
||||
//
|
||||
// The best that I can think of to do is to verify that the
|
||||
// appropriate argument is passed along.
|
||||
describe("transitionTo", () => {
|
||||
it("forwards a browser-space string", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
const spy = jest.spyOn(memoryHistory, "transitionTo");
|
||||
relativeHistory.transitionTo("/baz/quux/");
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith("/my/gateway/baz/quux/");
|
||||
});
|
||||
it("forwards a browser-space location object", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
const spy = jest.spyOn(memoryHistory, "transitionTo");
|
||||
relativeHistory.transitionTo({
|
||||
pathname: "/baz/quux/",
|
||||
hash: "#browns",
|
||||
state: "california",
|
||||
});
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toHaveBeenCalledWith({
|
||||
pathname: "/my/gateway/baz/quux/",
|
||||
hash: "#browns",
|
||||
state: "california",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// For some reason, the `memoryHistory` delegate seems to treat
|
||||
// `push`, and `replace` identically: in particular, the action
|
||||
// assigned tot he resulting location is, in all cases, "POP".
|
||||
// I don't know what the difference is supposed to be.
|
||||
function testTransitionFunction(method: "push" | "replace") {
|
||||
it("should accept a location string", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
relativeHistory[method].call(relativeHistory, "/baz/quux/#browns");
|
||||
expect(memoryHistory.getCurrentLocation()).toEqual(
|
||||
expect.objectContaining({
|
||||
pathname: "/my/gateway/baz/quux/",
|
||||
search: "",
|
||||
hash: "#browns",
|
||||
state: undefined,
|
||||
action: "POP",
|
||||
})
|
||||
);
|
||||
expect(relativeHistory.getCurrentLocation()).toEqual(
|
||||
expect.objectContaining({
|
||||
pathname: "/baz/quux/",
|
||||
search: "",
|
||||
hash: "#browns",
|
||||
state: undefined,
|
||||
action: "POP",
|
||||
})
|
||||
);
|
||||
});
|
||||
it("should accept a location object", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
relativeHistory[method].call(relativeHistory, {
|
||||
pathname: "/baz/quux/",
|
||||
hash: "#browns",
|
||||
state: "california",
|
||||
});
|
||||
expect(memoryHistory.getCurrentLocation()).toEqual(
|
||||
expect.objectContaining({
|
||||
pathname: "/my/gateway/baz/quux/",
|
||||
search: "",
|
||||
hash: "#browns",
|
||||
state: "california",
|
||||
action: "POP",
|
||||
})
|
||||
);
|
||||
expect(relativeHistory.getCurrentLocation()).toEqual(
|
||||
expect.objectContaining({
|
||||
pathname: "/baz/quux/",
|
||||
search: "",
|
||||
hash: "#browns",
|
||||
state: "california",
|
||||
action: "POP",
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
describe("push", () => {
|
||||
testTransitionFunction("push");
|
||||
});
|
||||
describe("replace", () => {
|
||||
testTransitionFunction("replace");
|
||||
});
|
||||
|
||||
describe("go, goForward, and goBack", () => {
|
||||
const createFivePageHistory = () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
relativeHistory.push("/1/");
|
||||
relativeHistory.push("/2/");
|
||||
relativeHistory.push("/3/");
|
||||
relativeHistory.push("/4/");
|
||||
relativeHistory.push("/5/");
|
||||
return {
|
||||
memoryHistory,
|
||||
relativeHistory,
|
||||
expectPageNumber: (n) =>
|
||||
expectPageNumber(relativeHistory, memoryHistory, n),
|
||||
};
|
||||
};
|
||||
function expectPageNumber(relativeHistory, memoryHistory, n) {
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
`/${n}/`
|
||||
);
|
||||
expect(memoryHistory.getCurrentLocation().pathname).toEqual(
|
||||
`/my/gateway/${n}/`
|
||||
);
|
||||
}
|
||||
|
||||
it("navigates back three, then forward two", () => {
|
||||
const {relativeHistory, expectPageNumber} = createFivePageHistory();
|
||||
expectPageNumber(5);
|
||||
relativeHistory.go(-3);
|
||||
expectPageNumber(2);
|
||||
relativeHistory.go(2);
|
||||
expectPageNumber(4);
|
||||
});
|
||||
|
||||
it("goes back", () => {
|
||||
const {relativeHistory, expectPageNumber} = createFivePageHistory();
|
||||
expectPageNumber(5);
|
||||
relativeHistory.goBack();
|
||||
expectPageNumber(4);
|
||||
relativeHistory.goBack();
|
||||
expectPageNumber(3);
|
||||
});
|
||||
|
||||
it("goes forward", () => {
|
||||
const {relativeHistory, expectPageNumber} = createFivePageHistory();
|
||||
expectPageNumber(5);
|
||||
relativeHistory.go(-2);
|
||||
relativeHistory.goBack();
|
||||
expectPageNumber(2);
|
||||
relativeHistory.goForward();
|
||||
expectPageNumber(3);
|
||||
relativeHistory.goForward();
|
||||
expectPageNumber(4);
|
||||
});
|
||||
|
||||
it("warns on overflow", () => {
|
||||
const {relativeHistory} = createFivePageHistory();
|
||||
relativeHistory.goBack();
|
||||
// Setup by configureEnzyme()
|
||||
const errorMock: JestMockFn<
|
||||
$ReadOnlyArray<void>,
|
||||
void
|
||||
> = (console.error: any);
|
||||
expect(errorMock).not.toHaveBeenCalled();
|
||||
relativeHistory.go(2);
|
||||
expect(errorMock).toHaveBeenCalledTimes(1);
|
||||
expect(errorMock.mock.calls[0][0]).toMatch(
|
||||
/Warning:.*there is not enough history/
|
||||
);
|
||||
// Reset console.error to a clean mock to satisfy afterEach check from
|
||||
// configureEnzyme()
|
||||
// $FlowExpectedError
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
it("warns on underflow", () => {
|
||||
const {relativeHistory} = createFivePageHistory();
|
||||
// Setup by configureEnzyme()
|
||||
const errorMock: JestMockFn<
|
||||
$ReadOnlyArray<void>,
|
||||
void
|
||||
> = (console.error: any);
|
||||
relativeHistory.go(-4);
|
||||
expect(errorMock).not.toHaveBeenCalled();
|
||||
relativeHistory.go(-2);
|
||||
expect(errorMock).toHaveBeenCalledTimes(1);
|
||||
expect(errorMock.mock.calls[0][0]).toMatch(
|
||||
/Warning:.*there is not enough history/
|
||||
);
|
||||
// Reset console.error to a clean mock to satisfy afterEach check from
|
||||
// configureEnzyme()
|
||||
// $FlowExpectedError
|
||||
console.error = jest.fn();
|
||||
});
|
||||
|
||||
it("accounts for interleaved changes in the delegate state", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
relativeHistory.push("/1/");
|
||||
memoryHistory.push("/my/gateway/2/");
|
||||
relativeHistory.push("/3/");
|
||||
memoryHistory.push("/my/gateway/4/");
|
||||
relativeHistory.push("/5/");
|
||||
|
||||
expectPageNumber(relativeHistory, memoryHistory, 5);
|
||||
relativeHistory.go(-3);
|
||||
expectPageNumber(relativeHistory, memoryHistory, 2);
|
||||
relativeHistory.go(2);
|
||||
expectPageNumber(relativeHistory, memoryHistory, 4);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createKey", () => {
|
||||
it("returns a string", () => {
|
||||
const {relativeHistory} = createStandardHistory();
|
||||
const key = relativeHistory.createKey(); // nondeterministic
|
||||
expect(key).toEqual(expect.stringContaining(""));
|
||||
});
|
||||
it("delegates", () => {
|
||||
const {memoryHistory, relativeHistory} = createStandardHistory();
|
||||
const secret = "ouagadougou";
|
||||
memoryHistory.createKey = jest
|
||||
.fn()
|
||||
.mockImplementationOnce(() => secret);
|
||||
expect(relativeHistory.createKey()).toEqual(secret);
|
||||
expect(memoryHistory.createKey).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createPath", () => {
|
||||
// We have no idea what this function is supposed to do. It
|
||||
// shouldn't be called. If it is, fail.
|
||||
it("throws unconditionally", () => {
|
||||
const {relativeHistory} = createStandardHistory();
|
||||
expect(() => relativeHistory.createPath("/wat/")).toThrow(
|
||||
"createPath is not part of the public API"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createHref", () => {
|
||||
it("should return DOM-space at root", () => {
|
||||
expect(
|
||||
createHistory(
|
||||
"/my/gateway/",
|
||||
"/my/gateway/"
|
||||
).relativeHistory.createHref("/favicon.png")
|
||||
).toEqual("favicon.png");
|
||||
});
|
||||
it("should return DOM-space at non-root", () => {
|
||||
expect(
|
||||
createStandardHistory().relativeHistory.createHref("/favicon.png")
|
||||
).toEqual("../../favicon.png");
|
||||
});
|
||||
it("should traverse up and back down the tree", () => {
|
||||
expect(
|
||||
createStandardHistory().relativeHistory.createHref(
|
||||
"/baz/quux/data.csv"
|
||||
)
|
||||
).toEqual("../../baz/quux/data.csv");
|
||||
});
|
||||
it("should resolve the root", () => {
|
||||
expect(
|
||||
createStandardHistory().relativeHistory.createHref("/")
|
||||
).toEqual("../../");
|
||||
});
|
||||
});
|
||||
|
||||
describe("createLocation", () => {
|
||||
it("should return React-space at root", () => {
|
||||
expect(
|
||||
createHistory(
|
||||
"/my/gateway/",
|
||||
"/my/gateway/"
|
||||
).relativeHistory.createLocation("/baz/quux/")
|
||||
).toEqual(expect.objectContaining({pathname: "/baz/quux/"}));
|
||||
});
|
||||
it("should return React-space at non-root", () => {
|
||||
expect(
|
||||
createStandardHistory().relativeHistory.createLocation("/baz/quux/")
|
||||
).toEqual(expect.objectContaining({pathname: "/baz/quux/"}));
|
||||
});
|
||||
it("should include the given action", () => {
|
||||
expect(
|
||||
createStandardHistory().relativeHistory.createLocation(
|
||||
"/baz/quux/",
|
||||
"REPLACE"
|
||||
)
|
||||
).toEqual(expect.objectContaining({action: "REPLACE"}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with another instance of itself as the delegate", () => {
|
||||
it("seems to work", () => {
|
||||
// Why? Because it's classy, mostly.
|
||||
const h0 = createMemoryHistory("/a1/a2/b1/b2/c/");
|
||||
const h1 = createRelativeHistory(h0, "/a1/a2/");
|
||||
const h2 = createRelativeHistory(h1, "/b1/b2/");
|
||||
expect(h2.getCurrentLocation().pathname).toEqual("/c/");
|
||||
h2.push("/c1/c2/");
|
||||
expect(h0.getCurrentLocation().pathname).toEqual("/a1/a2/b1/b2/c1/c2/");
|
||||
expect(h1.getCurrentLocation().pathname).toEqual("/b1/b2/c1/c2/");
|
||||
expect(h2.getCurrentLocation().pathname).toEqual("/c1/c2/");
|
||||
h2.goBack();
|
||||
expect(h0.getCurrentLocation().pathname).toEqual("/a1/a2/b1/b2/c/");
|
||||
expect(h1.getCurrentLocation().pathname).toEqual("/b1/b2/c/");
|
||||
expect(h2.getCurrentLocation().pathname).toEqual("/c/");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("in a React app", () => {
|
||||
class MainPage extends React.Component<{|
|
||||
+router: Router,
|
||||
+children: ReactNode,
|
||||
|}> {
|
||||
render() {
|
||||
const {router} = this.props;
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome</h1>
|
||||
<p>
|
||||
<i>currently viewing route:</i>{" "}
|
||||
<tt>{router.getCurrentLocation().pathname}</tt>
|
||||
</p>
|
||||
<img alt="logo" src={router.createHref("/logo.png")} />
|
||||
<nav>
|
||||
<Link to="/about/">About us</Link>
|
||||
</nav>
|
||||
<main>{this.props.children}</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
class AboutPage extends React.Component<{|+router: Router|}> {
|
||||
render() {
|
||||
return <p>content coming soon</p>;
|
||||
}
|
||||
}
|
||||
class App extends React.Component<{|+history: History|}> {
|
||||
render() {
|
||||
return (
|
||||
<Router history={this.props.history}>
|
||||
<Route path="/" component={MainPage}>
|
||||
<Route path="/about/" component={AboutPage} />
|
||||
</Route>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function test(basename) {
|
||||
it("should render to proper markup at index", () => {
|
||||
const {memoryHistory, relativeHistory} = createHistory(
|
||||
basename,
|
||||
normalize(basename + "/")
|
||||
);
|
||||
const e = render(<App history={relativeHistory} />);
|
||||
expect(e.find("tt").text()).toEqual("/");
|
||||
expect(e.find("img").attr("src")).toEqual("logo.png");
|
||||
expect(e.find("a").attr("href")).toEqual("about/");
|
||||
expect(e.find("main").children()).toHaveLength(0);
|
||||
expect(e.find("main").text()).toEqual("");
|
||||
expect(memoryHistory.getCurrentLocation().pathname).toEqual(
|
||||
normalize(basename + "/")
|
||||
);
|
||||
});
|
||||
|
||||
it("should render to proper markup at subroute", () => {
|
||||
const {memoryHistory, relativeHistory} = createHistory(
|
||||
basename,
|
||||
normalize(basename + "/about/")
|
||||
);
|
||||
const e = render(<App history={relativeHistory} />);
|
||||
expect(e.find("tt").text()).toEqual("/about/");
|
||||
expect(e.find("img").attr("src")).toEqual("../logo.png");
|
||||
expect(e.find("a").attr("href")).toEqual("../about/");
|
||||
expect(e.find("main").children()).toHaveLength(1);
|
||||
expect(e.find("main").text()).toEqual("content coming soon");
|
||||
expect(memoryHistory.getCurrentLocation().pathname).toEqual(
|
||||
normalize(basename + "/about/")
|
||||
);
|
||||
expect(e.html()).toEqual(
|
||||
render(
|
||||
<App history={createHistory("/", "/about/").relativeHistory} />
|
||||
).html()
|
||||
);
|
||||
});
|
||||
|
||||
function agreeWithServer(path) {
|
||||
const server = render(
|
||||
<App history={createHistory("/", path).relativeHistory} />
|
||||
);
|
||||
const client = render(
|
||||
<App
|
||||
history={
|
||||
createHistory(basename, normalize(basename + path))
|
||||
.relativeHistory
|
||||
}
|
||||
/>
|
||||
);
|
||||
expect(server.html()).toEqual(client.html());
|
||||
}
|
||||
it("should agree between client and server at index", () => {
|
||||
agreeWithServer("/");
|
||||
});
|
||||
it("should agree between client and server at subroute", () => {
|
||||
agreeWithServer("/about/");
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
it("should properly transition when clicking a link", () => {
|
||||
const {memoryHistory, relativeHistory} = createHistory(
|
||||
basename,
|
||||
normalize(basename + "/")
|
||||
);
|
||||
const e = mount(<App history={relativeHistory} />);
|
||||
expect(e.find("tt").text()).toEqual("/");
|
||||
expect(e.find("Link")).toHaveLength(1);
|
||||
click(e.find("a"));
|
||||
expect(relativeHistory.getCurrentLocation().pathname).toEqual(
|
||||
"/about/"
|
||||
);
|
||||
expect(memoryHistory.getCurrentLocation().pathname).toEqual(
|
||||
normalize(basename + "/about/")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
describe("when hosted at root", () => {
|
||||
test("/");
|
||||
});
|
||||
|
||||
describe("when hosted at a non-root gateway", () => {
|
||||
test("/some/arbitrary/gateway/");
|
||||
});
|
||||
});
|
||||
});
|
@ -1,31 +0,0 @@
|
||||
// @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;
|
||||
}
|
@ -1,111 +0,0 @@
|
||||
// @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("webutil/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…
x
Reference in New Issue
Block a user