Commit 41e8a41c by Wee

chore(test): import unit tests from react-router-v4

parent f9d4549c
import warning from "warning";
import React from "react";
import PropTypes from "prop-types";
import { createMemoryHistory as createHistory } from "history";
import Router from "./Router";
/**
* The public API for a <Router> that stores location in memory.
*/
class MemoryRouter extends React.Component {
static propTypes = {
initialEntries: PropTypes.array,
initialIndex: PropTypes.number,
getUserConfirmation: PropTypes.func,
keyLength: PropTypes.number,
children: PropTypes.node
};
history = createHistory(this.props);
componentWillMount() {
warning(
!this.props.history,
"<MemoryRouter> ignores the history prop. To use a custom history, " +
"use `import { Router }` instead of `import { MemoryRouter as Router }`."
);
}
render() {
return <Router history={this.history} children={this.props.children} />;
}
}
export default MemoryRouter;
import React from "react";
import PropTypes from "prop-types";
import invariant from "invariant";
/**
* The public API for prompting the user before navigating away
* from a screen with a component.
*/
class Prompt extends React.Component {
static propTypes = {
when: PropTypes.bool,
message: PropTypes.oneOfType([PropTypes.func, PropTypes.string]).isRequired
};
static defaultProps = {
when: true
};
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
block: PropTypes.func.isRequired
}).isRequired
}).isRequired
};
enable(message) {
if (this.unblock) this.unblock();
this.unblock = this.context.router.history.block(message);
}
disable() {
if (this.unblock) {
this.unblock();
this.unblock = null;
}
}
componentWillMount() {
invariant(
this.context.router,
"You should not use <Prompt> outside a <Router>"
);
if (this.props.when) this.enable(this.props.message);
}
componentWillReceiveProps(nextProps) {
if (nextProps.when) {
if (!this.props.when || this.props.message !== nextProps.message)
this.enable(nextProps.message);
} else {
this.disable();
}
}
componentWillUnmount() {
this.disable();
}
render() {
return null;
}
}
export default Prompt;
import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
import { createLocation, locationsAreEqual } from "history";
import generatePath from "./generatePath";
/**
* The public API for updating the location programmatically
* with a component.
*/
class Redirect extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // private, from <Switch>
push: PropTypes.bool,
from: PropTypes.string,
to: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired
};
static defaultProps = {
push: false
};
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.shape({
push: PropTypes.func.isRequired,
replace: PropTypes.func.isRequired
}).isRequired,
staticContext: PropTypes.object
}).isRequired
};
isStatic() {
return this.context.router && this.context.router.staticContext;
}
componentWillMount() {
invariant(
this.context.router,
"You should not use <Redirect> outside a <Router>"
);
if (this.isStatic()) this.perform();
}
componentDidMount() {
if (!this.isStatic()) this.perform();
}
componentDidUpdate(prevProps) {
const prevTo = createLocation(prevProps.to);
const nextTo = createLocation(this.props.to);
if (locationsAreEqual(prevTo, nextTo)) {
warning(
false,
`You tried to redirect to the same route you're currently on: ` +
`"${nextTo.pathname}${nextTo.search}"`
);
return;
}
this.perform();
}
computeTo({ computedMatch, to }) {
if (computedMatch) {
if (typeof to === "string") {
return generatePath(to, computedMatch.params);
} else {
return {
...to,
pathname: generatePath(to.pathname, computedMatch.params)
};
}
}
return to;
}
perform() {
const { history } = this.context.router;
const { push } = this.props;
const to = this.computeTo(this.props);
if (push) {
history.push(to);
} else {
history.replace(to);
}
}
render() {
return null;
}
}
export default Redirect;
import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";
import matchPath from "./matchPath";
const isEmptyChildren = children => React.Children.count(children) === 0;
/**
* The public API for matching a single path and rendering.
*/
class Route extends React.Component {
static propTypes = {
computedMatch: PropTypes.object, // private, from <Switch>
path: PropTypes.string,
exact: PropTypes.bool,
strict: PropTypes.bool,
sensitive: PropTypes.bool,
component: PropTypes.func,
render: PropTypes.func,
children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
location: PropTypes.object
};
static contextTypes = {
router: PropTypes.shape({
history: PropTypes.object.isRequired,
route: PropTypes.object.isRequired,
staticContext: PropTypes.object
})
};
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
route: {
location: this.props.location || this.context.router.route.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props, this.context.router)
};
computeMatch(
{ computedMatch, location, path, strict, exact, sensitive },
router
) {
if (computedMatch) return computedMatch; // <Switch> already computed the match for us
invariant(
router,
"You should not use <Route> or withRouter() outside a <Router>"
);
const { route } = router;
const pathname = (location || route.location).pathname;
return matchPath(pathname, { path, strict, exact, sensitive }, route.match);
}
componentWillMount() {
warning(
!(this.props.component && this.props.render),
"You should not use <Route component> and <Route render> in the same route; <Route render> will be ignored"
);
warning(
!(
this.props.component &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use <Route component> and <Route children> in the same route; <Route children> will be ignored"
);
warning(
!(
this.props.render &&
this.props.children &&
!isEmptyChildren(this.props.children)
),
"You should not use <Route render> and <Route children> in the same route; <Route children> will be ignored"
);
}
componentWillReceiveProps(nextProps, nextContext) {
warning(
!(nextProps.location && !this.props.location),
'<Route> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
);
warning(
!(!nextProps.location && this.props.location),
'<Route> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
);
this.setState({
match: this.computeMatch(nextProps, nextContext.router)
});
}
render() {
const { match } = this.state;
const { children, component, render } = this.props;
const { history, route, staticContext } = this.context.router;
const location = this.props.location || route.location;
const props = { match, location, history, staticContext };
if (component) return match ? React.createElement(component, props) : null;
if (render) return match ? render(props) : null;
if (typeof children === "function") return children(props);
if (children && !isEmptyChildren(children))
return React.Children.only(children);
return null;
}
}
export default Route;
import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";
/**
* The public API for putting history on context.
*/
class Router extends React.Component {
static propTypes = {
history: PropTypes.object.isRequired,
children: PropTypes.node
};
static contextTypes = {
router: PropTypes.object
};
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
...this.context.router,
history: this.props.history,
route: {
location: this.props.history.location,
match: this.state.match
}
}
};
}
state = {
match: this.computeMatch(this.props.history.location.pathname)
};
computeMatch(pathname) {
return {
path: "/",
url: "/",
params: {},
isExact: pathname === "/"
};
}
componentWillMount() {
const { children, history } = this.props;
invariant(
children == null || React.Children.count(children) === 1,
"A <Router> may have only one child element"
);
// Do this here so we can setState when a <Redirect> changes the
// location in componentWillMount. This happens e.g. when doing
// server rendering using a <StaticRouter>.
this.unlisten = history.listen(() => {
this.setState({
match: this.computeMatch(history.location.pathname)
});
});
}
componentWillReceiveProps(nextProps) {
warning(
this.props.history === nextProps.history,
"You cannot change <Router history>"
);
}
componentWillUnmount() {
this.unlisten();
}
render() {
const { children } = this.props;
return children ? React.Children.only(children) : null;
}
}
export default Router;
import warning from "warning";
import invariant from "invariant";
import React from "react";
import PropTypes from "prop-types";
import { createLocation, createPath } from "history";
import Router from "./Router";
const addLeadingSlash = path => {
return path.charAt(0) === "/" ? path : "/" + path;
};
const addBasename = (basename, location) => {
if (!basename) return location;
return {
...location,
pathname: addLeadingSlash(basename) + location.pathname
};
};
const stripBasename = (basename, location) => {
if (!basename) return location;
const base = addLeadingSlash(basename);
if (location.pathname.indexOf(base) !== 0) return location;
return {
...location,
pathname: location.pathname.substr(base.length)
};
};
const createURL = location =>
typeof location === "string" ? location : createPath(location);
const staticHandler = methodName => () => {
invariant(false, "You cannot %s with <StaticRouter>", methodName);
};
const noop = () => {};
/**
* The public top-level API for a "static" <Router>, so-called because it
* can't actually change the current location. Instead, it just records
* location changes in a context object. Useful mainly in testing and
* server-rendering scenarios.
*/
class StaticRouter extends React.Component {
static propTypes = {
basename: PropTypes.string,
context: PropTypes.object.isRequired,
location: PropTypes.oneOfType([PropTypes.string, PropTypes.object])
};
static defaultProps = {
basename: "",
location: "/"
};
static childContextTypes = {
router: PropTypes.object.isRequired
};
getChildContext() {
return {
router: {
staticContext: this.props.context
}
};
}
createHref = path => addLeadingSlash(this.props.basename + createURL(path));
handlePush = location => {
const { basename, context } = this.props;
context.action = "PUSH";
context.location = addBasename(basename, createLocation(location));
context.url = createURL(context.location);
};
handleReplace = location => {
const { basename, context } = this.props;
context.action = "REPLACE";
context.location = addBasename(basename, createLocation(location));
context.url = createURL(context.location);
};
handleListen = () => noop;
handleBlock = () => noop;
componentWillMount() {
warning(
!this.props.history,
"<StaticRouter> ignores the history prop. To use a custom history, " +
"use `import { Router }` instead of `import { StaticRouter as Router }`."
);
}
render() {
const { basename, context, location, ...props } = this.props;
const history = {
createHref: this.createHref,
action: "POP",
location: stripBasename(basename, createLocation(location)),
push: this.handlePush,
replace: this.handleReplace,
go: staticHandler("go"),
goBack: staticHandler("goBack"),
goForward: staticHandler("goForward"),
listen: this.handleListen,
block: this.handleBlock
};
return <Router {...props} history={history} />;
}
}
export default StaticRouter;
import React from "react";
import PropTypes from "prop-types";
import warning from "warning";
import invariant from "invariant";
import matchPath from "./matchPath";
/**
* The public API for rendering the first <Route> that matches.
*/
class Switch extends React.Component {
static contextTypes = {
router: PropTypes.shape({
route: PropTypes.object.isRequired
}).isRequired
};
static propTypes = {
children: PropTypes.node,
location: PropTypes.object
};
componentWillMount() {
invariant(
this.context.router,
"You should not use <Switch> outside a <Router>"
);
}
componentWillReceiveProps(nextProps) {
warning(
!(nextProps.location && !this.props.location),
'<Switch> elements should not change from uncontrolled to controlled (or vice versa). You initially used no "location" prop and then provided one on a subsequent render.'
);
warning(
!(!nextProps.location && this.props.location),
'<Switch> elements should not change from controlled to uncontrolled (or vice versa). You provided a "location" prop initially but omitted it on a subsequent render.'
);
}
render() {
const { route } = this.context.router;
const { children } = this.props;
const location = this.props.location || route.location; // 可以手动指定 location
let match, child;
React.Children.forEach(children, element => {
if (match == null && React.isValidElement(element)) {
const {
path: pathProp, // path
exact, // 路径完全匹配
strict, // 是否结尾匹配带/的路径
sensitive, // 大小写敏感
from // path 的备胎
} = element.props;
const path = pathProp || from;
child = element;
match = matchPath(
location.pathname,
{ path, exact, strict, sensitive },
route.match
);
}
});
return match
? React.cloneElement(child, { location, computedMatch: match }) // 重新计算两个参数
: null;
}
}
export default Switch;
......@@ -2,7 +2,7 @@ import React from "react";
import ReactDOM from "react-dom";
import MemoryRouter from "../MemoryRouter";
import Redirect from "../Redirect";
import Route from "../Route";
import Route from "../../LiveRoute";
import Switch from "../Switch";
describe("A <Redirect>", () => {
......
......@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
import { createMemoryHistory } from "history";
import MemoryRouter from "../MemoryRouter";
import Router from "../Router";
import Route from "../Route";
import Route from "../../LiveRoute";
describe("A <Route>", () => {
it("renders at the root", () => {
......
......@@ -4,7 +4,7 @@ import ReactDOMServer from "react-dom/server";
import PropTypes from "prop-types";
import StaticRouter from "../StaticRouter";
import Redirect from "../Redirect";
import Route from "../Route";
import Route from "../../LiveRoute";
import Prompt from "../Prompt";
describe("A <StaticRouter>", () => {
......
......@@ -2,7 +2,7 @@ import React from "react";
import ReactDOM from "react-dom";
import MemoryRouter from "../MemoryRouter";
import Switch from "../Switch";
import Route from "../Route";
import Route from "../../LiveRoute";
import Redirect from "../Redirect";
describe("A <Switch>", () => {
......
......@@ -3,7 +3,7 @@ import ReactDOM from "react-dom";
import { createMemoryHistory as createHistory } from "history";
import Router from "../Router";
import Switch from "../Switch";
import Route from "../Route";
import Route from "../../LiveRoute";
describe("A <Switch>", () => {
it("does not remount a <Route>", () => {
......
import React from "react";
import ReactDOM from "react-dom";
import MemoryRouter from "../MemoryRouter";
import Route from "../Route";
import Route from "../../LiveRoute";
describe("Integration Tests", () => {
it("renders nested matches", () => {
......
......@@ -2,7 +2,7 @@ import React from "react";
import ReactDOM from "react-dom";
import MemoryRouter from "../MemoryRouter";
import StaticRouter from "../StaticRouter";
import Route from "../Route";
import Route from "../../LiveRoute";
import withRouter from "../withRouter";
describe("withRouter", () => {
......
import pathToRegexp from "path-to-regexp";
const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;
const compileGenerator = pattern => {
const cacheKey = pattern;
const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
if (cache[pattern]) return cache[pattern];
const compiledGenerator = pathToRegexp.compile(pattern);
if (cacheCount < cacheLimit) {
cache[pattern] = compiledGenerator;
cacheCount++;
}
return compiledGenerator;
};
/**
* Public API for generating a URL pathname from a pattern and parameters.
*/
const generatePath = (pattern = "/", params = {}) => {
if (pattern === "/") {
return pattern;
}
const generator = compileGenerator(pattern);
return generator(params);
};
export default generatePath;
export MemoryRouter from "./MemoryRouter";
export Prompt from "./Prompt";
export Redirect from "./Redirect";
export Route from "./Route";
export Router from "./Router";
export StaticRouter from "./StaticRouter";
export Switch from "./Switch";
export generatePath from "./generatePath";
export matchPath from "./matchPath";
export withRouter from "./withRouter";
import pathToRegexp from "path-to-regexp";
// ES6 的 import 会共享,所以可以理解为一个全局缓存
// 缓存的结构是 option 对象下的 pattern
const patternCache = {};
const cacheLimit = 10000;
let cacheCount = 0;
const compilePath = (pattern, options) => {
const cacheKey = `${options.end}${options.strict}${options.sensitive}`;
const cache = patternCache[cacheKey] || (patternCache[cacheKey] = {});
if (cache[pattern]) return cache[pattern];
const keys = [];
const re = pathToRegexp(pattern, keys, options);
const compiledPattern = { re, keys };
// 这是 path-to-regex 的返回值
// var keys = []
// var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
if (cacheCount < cacheLimit) {
cache[pattern] = compiledPattern;
cacheCount++;
}
return compiledPattern;
};
/**
* Public API for matching a URL pathname to a path pattern.
*/
const matchPath = (pathname, options = {}, parent) => {
if (typeof options === "string") options = { path: options };
const { path, exact = false, strict = false, sensitive = false } = options;
if (path == null) return parent;
// path-to-regex 的 end 相当于就是 router 的 exact,相当于是否使用全局匹配 /g
const { re, keys } = compilePath(path, { end: exact, strict, sensitive });
// 使用生成的正则表达式去匹配
const match = re.exec(pathname);
// 不匹配直接返回
if (!match) return null;
const [url, ...values] = match;
const isExact = pathname === url;
// 如果在要求 exact 匹配时为非 exact 的匹配,则直接返回
if (exact && !isExact) return null;
return {
path, // the path pattern used to match
url: path === "/" && url === "" ? "/" : url, // the matched portion of the URL
isExact, // whether or not we matched exactly
params: keys.reduce((memo, key, index) => {
memo[key.name] = values[index];
return memo;
}, {})
};
};
export default matchPath;
import React from "react";
import PropTypes from "prop-types";
import hoistStatics from "hoist-non-react-statics";
import Route from "./Route";
/**
* A public higher-order component to access the imperative API
*/
const withRouter = Component => {
const C = props => {
const { wrappedComponentRef, ...remainingProps } = props;
return (
<Route
children={routeComponentProps => (
<Component
{...remainingProps}
{...routeComponentProps}
ref={wrappedComponentRef}
/>
)}
/>
);
};
C.displayName = `withRouter(${Component.displayName || Component.name})`;
C.WrappedComponent = Component;
C.propTypes = {
wrappedComponentRef: PropTypes.func
};
return hoistStatics(C, Component);
};
export default withRouter;
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment