From 15fd71a32fbb5a9ad79f3b84f2d25bf034166f09 Mon Sep 17 00:00:00 2001 From: Paul Sherman Date: Wed, 8 Aug 2018 11:58:18 -0500 Subject: [PATCH] Support suspended route navigation --- packages/react-universal/.size-snapshot.json | 28 +- .../react-universal/src/FinishNavigation.tsx | 41 +++ packages/react-universal/src/curiProvider.tsx | 25 +- packages/react-universal/src/index.ts | 4 +- .../tests/FinishNavigation.spec.tsx | 89 +++++++ .../react-universal/tests/Navigating.spec.tsx | 6 +- .../tests/curiProvider.spec.tsx | 3 - .../types/FinishNavigation.d.ts | 10 + .../react-universal/types/curiProvider.d.ts | 1 + packages/react-universal/types/index.d.ts | 4 +- packages/router/.size-snapshot.json | 24 +- packages/router/src/curi.ts | 68 ++++- packages/router/src/types/curi.ts | 3 + packages/router/tests/curi.spec.ts | 251 ++++++++++++++++-- packages/router/types/types/curi.d.ts | 3 + 15 files changed, 494 insertions(+), 66 deletions(-) create mode 100644 packages/react-universal/src/FinishNavigation.tsx create mode 100644 packages/react-universal/tests/FinishNavigation.spec.tsx create mode 100644 packages/react-universal/types/FinishNavigation.d.ts diff --git a/packages/react-universal/.size-snapshot.json b/packages/react-universal/.size-snapshot.json index 5f361dec5..db8ff3767 100644 --- a/packages/react-universal/.size-snapshot.json +++ b/packages/react-universal/.size-snapshot.json @@ -1,31 +1,31 @@ { "dist/curi-react-universal.es.js": { - "bundled": 6794, - "minified": 3460, - "gzipped": 1205, + "bundled": 8447, + "minified": 4296, + "gzipped": 1349, "treeshaked": { "rollup": { - "code": 441, + "code": 453, "import_statements": 21 }, "webpack": { - "code": 1435 + "code": 1449 } } }, "dist/curi-react-universal.js": { - "bundled": 7093, - "minified": 3704, - "gzipped": 1292 + "bundled": 8753, + "minified": 4545, + "gzipped": 1435 }, "dist/curi-react-universal.umd.js": { - "bundled": 7632, - "minified": 3460, - "gzipped": 1250 + "bundled": 9374, + "minified": 4194, + "gzipped": 1375 }, "dist/curi-react-universal.min.js": { - "bundled": 7271, - "minified": 3172, - "gzipped": 1102 + "bundled": 9013, + "minified": 3906, + "gzipped": 1226 } } diff --git a/packages/react-universal/src/FinishNavigation.tsx b/packages/react-universal/src/FinishNavigation.tsx new file mode 100644 index 000000000..0ead8de7d --- /dev/null +++ b/packages/react-universal/src/FinishNavigation.tsx @@ -0,0 +1,41 @@ +import React from "react"; + +import { Curious } from "./Context"; + +import { Navigation } from "@curi/router"; + +export interface FinishNavigationProps { + children: any; +} + +export interface BaseFinishNavigationProps extends FinishNavigationProps { + navigation: Navigation; +} + +class FinishNavigation extends React.Component { + componentDidMount() { + this.finish(); + } + + componentDidUpdate() { + this.finish(); + } + + finish() { + if (this.props.navigation.finish) { + this.props.navigation.finish(); + } + } + + render() { + return this.props.children; + } +} + +export default (props: FinishNavigationProps) => ( + + {({ navigation }) => ( + + )} + +); diff --git a/packages/react-universal/src/curiProvider.tsx b/packages/react-universal/src/curiProvider.tsx index 168880bc7..bd59b0b45 100644 --- a/packages/react-universal/src/curiProvider.tsx +++ b/packages/react-universal/src/curiProvider.tsx @@ -1,12 +1,17 @@ import React from "react"; import { Provider } from "./Context"; -import { CuriRouter, Emitted } from "@curi/router"; +import { + CuriRouter, + Response, + Emitted, +} from "@curi/router"; export type CuriRenderFn = (props: Emitted) => React.ReactNode; export interface RouterProps { children: CuriRenderFn; + suspend?: boolean; } interface RouterState { @@ -19,6 +24,7 @@ export default function curiProvider( return class Router extends React.Component { stopResponding: () => void; removed: boolean; + current: Response; constructor(props: RouterProps) { super(props); @@ -28,6 +34,14 @@ export default function curiProvider( router } }; + this.current = this.state.emitted.response; + } + + shouldComponentUpdate(nextProps: RouterProps, nextState: RouterState) { + if (nextProps.suspend) { + return nextState.emitted.response === this.current; + } + return true; } componentDidMount() { @@ -37,8 +51,15 @@ export default function curiProvider( setupRespond(router: CuriRouter) { this.stopResponding = router.observe( (emitted: Emitted) => { + this.current = emitted.response; if (!this.removed) { - this.setState({ emitted }); + if (this.props.suspend) { + setTimeout(() => { + this.setState({ emitted }); + }); + } else { + this.setState({ emitted }); + } } }, { initial: false } diff --git a/packages/react-universal/src/index.ts b/packages/react-universal/src/index.ts index 9a0b4bcdd..f3eca217e 100644 --- a/packages/react-universal/src/index.ts +++ b/packages/react-universal/src/index.ts @@ -2,11 +2,13 @@ export { ActiveProps } from "./Active"; export { BlockProps } from "./Block"; export { RouterProps, CuriRenderFn } from "./curiProvider"; export { NavigatingProps } from "./Navigating"; +export { FinishNavigationProps } from "./FinishNavigation"; import Active from "./Active"; import Block from "./Block"; import curiProvider from "./curiProvider"; import { Curious } from "./Context"; import Navigating from "./Navigating"; +import FinishNavigation from "./FinishNavigation"; -export { Active, Block, curiProvider, Curious, Navigating }; +export { Active, Block, curiProvider, Curious, Navigating, FinishNavigation }; diff --git a/packages/react-universal/tests/FinishNavigation.spec.tsx b/packages/react-universal/tests/FinishNavigation.spec.tsx new file mode 100644 index 000000000..651ad7b1b --- /dev/null +++ b/packages/react-universal/tests/FinishNavigation.spec.tsx @@ -0,0 +1,89 @@ +import "jest"; +import React from "react"; +import ReactDOM from "react-dom"; +import { curi, prepareRoutes } from "@curi/router"; +import InMemory from "@hickory/in-memory"; + +// @ts-ignore (resolved by jest) +import { curiProvider, FinishNavigation } from "@curi/react-universal"; + +describe("", () => { + let node; + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "About", path: "about" } + ]); + + beforeEach(() => { + node = document.createElement("div"); + }); + + afterEach(() => { + ReactDOM.unmountComponentAtNode(node); + }); + + it('"finishes" navigation after mounting', () => { + const history = InMemory(); + const router = curi(history, routes, { suspend: true }); + const Router = curiProvider(router); + + let navigations = []; + // intercept and mock navigation.finish() + router.observe( + ({ navigation }) => { + navigation.finish = jest.fn(); + navigations.push(navigation); + } + ); + + ReactDOM.render( + + {({ navigation }) => { + expect(navigation.finish.mock.calls.length).toBe(0); + return ( + +
Yo
+
+ ); + }} +
, + node, + () => { + expect(navigations[0].finish.mock.calls.length).toBe(1); + } + ); + }); + + it('"finishes" navigation after updating', () => { + const history = InMemory(); + const router = curi(history, routes, { suspend: true }); + const Router = curiProvider(router); + + let navigations = []; + // intercept and mock navigation.finish() + router.observe( + ({ navigation }) => { + navigation.finish = jest.fn(); + navigations.push(navigation); + } + ); + + ReactDOM.render( + + {({ navigation }) => { + expect(navigation.finish.mock.calls.length).toBe(0); + return ( + +
Yo
+
+ ); + }} +
, + node + ); + expect(navigations[0].finish.mock.calls.length).toBe(1); + + router.navigate({ name: "About" }); + expect(navigations[1].finish.mock.calls.length).toBe(1); + }); +}); diff --git a/packages/react-universal/tests/Navigating.spec.tsx b/packages/react-universal/tests/Navigating.spec.tsx index 9601e3b34..9e53ad8ab 100644 --- a/packages/react-universal/tests/Navigating.spec.tsx +++ b/packages/react-universal/tests/Navigating.spec.tsx @@ -2,14 +2,14 @@ import "jest"; import React from "react"; import ReactDOM from "react-dom"; import InMemory from "@hickory/in-memory"; -import { curi } from "@curi/router"; +import { curi, prepareRoutes } from "@curi/router"; // resolved by jest import { curiProvider, Navigating } from "@curi/react-universal"; describe("", () => { let node; - const routes = [ + const routes = prepareRoutes([ { name: "Home", path: "" }, { name: "Sync", path: "sync" }, { @@ -35,7 +35,7 @@ describe("", () => { } }, { name: "Catch All", path: "(.*)" } - ]; + ]); beforeEach(() => { node = document.createElement("div"); diff --git a/packages/react-universal/tests/curiProvider.spec.tsx b/packages/react-universal/tests/curiProvider.spec.tsx index 96810a108..3f963bfe5 100644 --- a/packages/react-universal/tests/curiProvider.spec.tsx +++ b/packages/react-universal/tests/curiProvider.spec.tsx @@ -23,8 +23,6 @@ describe("curiProvider()", () => { ReactDOM.unmountComponentAtNode(node); }); - describe("router argument", () => {}); - describe("children prop", () => { it("calls children() function when it renders", () => { const history = InMemory(); @@ -42,7 +40,6 @@ describe("curiProvider()", () => { it("re-renders when the location changes", done => { const history = InMemory(); const router = curi(history, routes); - let pushedHistory = false; let firstCall = true; const fn = jest.fn(({ response }) => { if (firstCall) { diff --git a/packages/react-universal/types/FinishNavigation.d.ts b/packages/react-universal/types/FinishNavigation.d.ts new file mode 100644 index 000000000..06e41db05 --- /dev/null +++ b/packages/react-universal/types/FinishNavigation.d.ts @@ -0,0 +1,10 @@ +/// +import { Navigation } from "@curi/router"; +export interface FinishNavigationProps { + children: any; +} +export interface BaseFinishNavigationProps extends FinishNavigationProps { + navigation: Navigation; +} +declare const _default: (props: FinishNavigationProps) => JSX.Element; +export default _default; diff --git a/packages/react-universal/types/curiProvider.d.ts b/packages/react-universal/types/curiProvider.d.ts index 6f5fb3d5f..3b03931ea 100644 --- a/packages/react-universal/types/curiProvider.d.ts +++ b/packages/react-universal/types/curiProvider.d.ts @@ -3,5 +3,6 @@ import { CuriRouter, Emitted } from "@curi/router"; export declare type CuriRenderFn = (props: Emitted) => React.ReactNode; export interface RouterProps { children: CuriRenderFn; + suspend?: boolean; } export default function curiProvider(router: CuriRouter): React.ComponentType; diff --git a/packages/react-universal/types/index.d.ts b/packages/react-universal/types/index.d.ts index 1d804dad7..914a66556 100644 --- a/packages/react-universal/types/index.d.ts +++ b/packages/react-universal/types/index.d.ts @@ -2,9 +2,11 @@ export { ActiveProps } from "./Active"; export { BlockProps } from "./Block"; export { RouterProps, CuriRenderFn } from "./curiProvider"; export { NavigatingProps } from "./Navigating"; +export { FinishNavigationProps } from "./FinishNavigation"; import Active from "./Active"; import Block from "./Block"; import curiProvider from "./curiProvider"; import { Curious } from "./Context"; import Navigating from "./Navigating"; -export { Active, Block, curiProvider, Curious, Navigating }; +import FinishNavigation from "./FinishNavigation"; +export { Active, Block, curiProvider, Curious, Navigating, FinishNavigation }; diff --git a/packages/router/.size-snapshot.json b/packages/router/.size-snapshot.json index b79ee9dd8..01f694fb3 100644 --- a/packages/router/.size-snapshot.json +++ b/packages/router/.size-snapshot.json @@ -1,8 +1,8 @@ { "dist/curi-router.es.js": { - "bundled": 21411, - "minified": 7965, - "gzipped": 3142, + "bundled": 22773, + "minified": 8338, + "gzipped": 3222, "treeshaked": { "rollup": { "code": 45, @@ -14,18 +14,18 @@ } }, "dist/curi-router.js": { - "bundled": 21670, - "minified": 8170, - "gzipped": 3218 + "bundled": 23032, + "minified": 8543, + "gzipped": 3298 }, "dist/curi-router.umd.js": { - "bundled": 35046, - "minified": 10391, - "gzipped": 4295 + "bundled": 36548, + "minified": 10764, + "gzipped": 4380 }, "dist/curi-router.min.js": { - "bundled": 33252, - "minified": 9237, - "gzipped": 3807 + "bundled": 34754, + "minified": 9610, + "gzipped": 3892 } } diff --git a/packages/router/src/curi.ts b/packages/router/src/curi.ts index fdc190dd4..dbd08094c 100644 --- a/packages/router/src/curi.ts +++ b/packages/router/src/curi.ts @@ -40,7 +40,8 @@ export default function createRouter( sideEffects = [], emitRedirects = true, automaticRedirects = true, - external + external, + suspend = false } = options; let routes: CompiledRouteArray; @@ -157,9 +158,42 @@ export default function createRouter( history, external ); - pending.finish(); - emitImmediate(response, navigation); - activeNavigation = undefined; + + if (suspend) { + navigation.finish = createFinisher(pending, response, navigation); + emitSuspended(response, navigation); + } else { + pending.finish(); + emitImmediate(response, navigation); + activeNavigation = undefined; + } + } + + function createFinisher( + pending: PendingNavigation, + response: Response, + navigation: Navigation + ) { + let called = false; + return function finisher() { + if (called || pending.cancelled) { + return; + } + called = true; + + if (finishCallback) { + finishCallback(); + } + resetCallbacks(); + + if (pending.finish) { + pending.finish(); + } + + activeNavigation = undefined; + + callSideEffects({ response, navigation, router }); + }; } function resetCallbacks() { @@ -167,14 +201,14 @@ export default function createRouter( finishCallback = undefined; } - function callObservers(emitted: Emitted) { - observers.forEach(fn => { + function callObserversAndOneTimers(emitted: Emitted) { + [...observers, ...oneTimers.splice(0)].forEach(fn => { fn(emitted); }); } - function callOneTimersAndSideEffects(emitted: Emitted) { - [...oneTimers.splice(0), ...sideEffects].forEach(fn => { + function callSideEffects(emitted: Emitted) { + sideEffects.forEach(fn => { fn(emitted); }); } @@ -190,8 +224,8 @@ export default function createRouter( mostRecent.response = response; mostRecent.navigation = navigation; - callObservers({ response, navigation, router }); - callOneTimersAndSideEffects({ response, navigation, router }); + callObserversAndOneTimers({ response, navigation, router }); + callSideEffects({ response, navigation, router }); } if (response.redirectTo !== undefined && automaticRedirects) { @@ -233,6 +267,20 @@ export default function createRouter( } } + function emitSuspended(response: Response, navigation: Navigation) { + if (!response.redirectTo || emitRedirects) { + // store for current() and respond() + mostRecent.response = response; + mostRecent.navigation = navigation; + + callObserversAndOneTimers({ response, navigation, router }); + } + + if (response.redirectTo !== undefined) { + history.navigate(response.redirectTo, "REPLACE"); + } + } + const router: CuriRouter = { route: routeInteractions, history, diff --git a/packages/router/src/types/curi.ts b/packages/router/src/types/curi.ts index 6e1e7ae67..b8204e13d 100644 --- a/packages/router/src/types/curi.ts +++ b/packages/router/src/types/curi.ts @@ -8,6 +8,7 @@ import { Response, Params } from "./response"; export interface Navigation { action: Action; previous: Response | null; + finish?: () => void; } export interface Emitted { @@ -34,6 +35,7 @@ export interface RouterOptions { emitRedirects?: boolean; automaticRedirects?: boolean; external?: any; + suspend?: boolean; } export interface CurrentResponse { @@ -61,4 +63,5 @@ export interface CuriRouter { history: History; current(): CurrentResponse; navigate(options: NavigationDetails): void; + finish?: () => void; } diff --git a/packages/router/tests/curi.spec.ts b/packages/router/tests/curi.spec.ts index c67f429cc..3a709a4f1 100644 --- a/packages/router/tests/curi.spec.ts +++ b/packages/router/tests/curi.spec.ts @@ -8,12 +8,6 @@ import { NavType } from "@hickory/root"; import { curi, prepareRoutes } from "@curi/router"; describe("curi", () => { - let history; - - beforeEach(() => { - history = InMemory(); - }); - describe("constructor", () => { // these tests rely on the fact that the pathname generator // is a default interaction @@ -23,6 +17,7 @@ describe("curi", () => { { name: "About", path: "about" }, { name: "Contact", path: "contact" } ]); + const history = InMemory(); const router = curi(history, routes); const names = ["Home", "About", "Contact"]; @@ -44,6 +39,7 @@ describe("curi", () => { ] } ]); + const history = InMemory(); const router = curi(history, routes); const names = ["Email", "Phone"]; names.forEach(n => { @@ -55,6 +51,7 @@ describe("curi", () => { const realWarn = console.warn; const fakeWarn = (console.warn = jest.fn()); + // don't wrap in prepareRoutes()! const routes = [ { name: "Home", path: "" }, { name: "About", path: "about" }, @@ -67,19 +64,24 @@ describe("curi", () => { ] } ]; + const history = InMemory(); const router = curi(history, routes); expect(fakeWarn.mock.calls.length).toBe(1); console.warn = realWarn; }); it("makes interactions available through router.route", () => { - const routes = prepareRoutes([{ name: "Home", path: "" }]); + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "Catch All", path: "(.*)" } + ]); const createfakeInteraction = () => ({ name: "fake", register: () => {}, reset: () => {}, get: () => {} }); + const history = InMemory(); const router = curi(history, routes, { route: [createfakeInteraction()] }); @@ -89,7 +91,11 @@ describe("curi", () => { describe("options", () => { describe("interactions", () => { it("includes pathname interaction by default", () => { - const routes = prepareRoutes([{ name: "Home", path: "" }]); + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "Catch All", path: "(.*)" } + ]); + const history = InMemory(); const router = curi(history, routes); expect(router.route.pathname).toBeDefined(); }); @@ -107,7 +113,11 @@ describe("curi", () => { }; }; - const routes = prepareRoutes([{ name: "Home", path: "" }]); + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "Catch All", path: "(.*)" } + ]); + const history = InMemory(); const router = curi(history, routes, { route: [createfirstInteraction()] }); @@ -195,7 +205,7 @@ describe("curi", () => { it("calls side effect methods AFTER a response is generated, passing response, navigation, and router", done => { const routes = prepareRoutes([{ name: "All", path: "(.*)" }]); const sideEffect = jest.fn(); - + const history = InMemory(); const router = curi(history, routes, { sideEffects: [sideEffect] }); @@ -221,7 +231,7 @@ describe("curi", () => { expect(router).toBe(router); done(); }; - + const history = InMemory(); const router = curi(history, routes, { sideEffects: [sideEffect] }); @@ -254,6 +264,7 @@ describe("curi", () => { firstCall = false; } }; + const history = InMemory(); const router = curi(history, routes, { sideEffects: [logger] }); @@ -275,9 +286,11 @@ describe("curi", () => { { name: "Other", path: "other" - } + }, + { name: "Catch All", path: "(.*)" } ]); + const history = InMemory(); const router = curi(history, routes, { emitRedirects: false }); @@ -306,8 +319,10 @@ describe("curi", () => { { name: "Other", path: "other" - } + }, + { name: "Catch All", path: "(.*)" } ]); + const history = InMemory(); const router = curi(history, routes); // because the routes are not asynchronous, an automatic redirect // will be triggered before once is called, so its response @@ -341,8 +356,10 @@ describe("curi", () => { { name: "Other", path: "other" - } + }, + { name: "Catch All", path: "(.*)" } ]); + const history = InMemory(); const router = curi(history, routes, { automaticRedirects: false }); @@ -407,10 +424,103 @@ describe("curi", () => { }); }); }); + describe("suspend", () => { + it("when false (default), emits navigation objects without a finish() fn", () => { + const routes = prepareRoutes([ + { + name: "Home", + path: "" + }, + { + name: "Not Found", + path: "(.*)" + } + ]); + const history = InMemory(); + const router = curi(history, routes, { + suspend: false + }); + + const { navigation } = router.current(); + expect(navigation.finish).toBeUndefined(); + }); + + it("when true, emits navigation objects with a finish() fn", () => { + const routes = prepareRoutes([ + { + name: "Home", + path: "" + }, + { + name: "Not Found", + path: "(.*)" + } + ]); + const history = InMemory(); + const router = curi(history, routes, { + suspend: true + }); + + const { navigation } = router.current(); + expect(typeof navigation.finish).toBe("function"); + }); + + it("calls correct navigation's finish() fn", () => { + const routes = prepareRoutes([ + { + name: "Home", + path: "" + }, + { + name: "One", + path: "one" + }, + { + name: "Two", + path: "two" + }, + { + name: "Not Found", + path: "(.*)" + } + ]); + const history = InMemory(); + const router = curi(history, routes, { + suspend: true + }); + // start a navigation, but don't finish it + let oneNavigation; + router.navigate({ name: "One" }); + router.once(({ navigation }) => { + oneNavigation = navigation; + }); + + // start a second navigation, but don't finish it + // at this point, the first navigation has been cancelled + let twoNavigation; + router.navigate({ name: "Two" }); + router.once(({ navigation }) => { + twoNavigation = navigation; + }); + + // try to finish the first navigation + oneNavigation.finish(); + // the navigation is cancelled, so still at Home + expect(history.location.pathname).toBe(router.route.pathname("Home")); + + twoNavigation.finish(); + expect(history.location.pathname).toBe(router.route.pathname("Two")); + }); + }); + }); describe("sync/async matching", () => { it("does synchronous matching by default", () => { - const routes = prepareRoutes([{ name: "Home", path: "" }]); + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "Catch All", path: "(.*)" } + ]); + const history = InMemory(); const router = curi(history, routes); const after = jest.fn(); router.once(r => { @@ -427,8 +537,10 @@ describe("curi", () => { resolve: { test: () => Promise.resolve() } - } + }, + { name: "Catch All", path: "(.*)" } ]); + const history = InMemory(); const router = curi(history, routes); const after = jest.fn(); router.once(r => { @@ -474,7 +586,10 @@ describe("curi", () => { describe("current", () => { describe("sync", () => { it("initial value is an object with resolved response and navigation properties", () => { - const routes = prepareRoutes([{ name: "Catch All", path: "(.*)" }]); + const routes = prepareRoutes([ + { name: "Catch All", path: "(.*)" } + ]); + const history = InMemory(); const router = curi(history, routes); expect(router.current()).toMatchObject({ response: { name: "Catch All" }, @@ -485,6 +600,7 @@ describe("curi", () => { describe("async", () => { it("initial value is an object with null response and navigation properties", () => { + const history = InMemory(); const routes = prepareRoutes([ { name: "Catch All", @@ -503,7 +619,11 @@ describe("curi", () => { }); it("response and navigation are the last resolved response and navigation", () => { - const routes = prepareRoutes([{ name: "Home", path: "" }]); + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "Catch All", path: "(.*)" } + ]); + const history = InMemory(); const router = curi(history, routes); router.once(({ response, navigation }) => { expect(router.current()).toMatchObject({ @@ -514,9 +634,11 @@ describe("curi", () => { }); it("updates properties when a new response is resolved", done => { + const history = InMemory(); const routes = prepareRoutes([ { name: "Home", path: "" }, - { name: "About", path: "about" } + { name: "About", path: "about" }, + { name: "Catch All", path: "(.*)" } ]); const router = curi(history, routes); let calls = 0; @@ -558,6 +680,7 @@ describe("curi", () => { { name: "Contacto", path: "contacto" } ]); + const history = InMemory(); const router = curi(history, englishRoutes); router.refresh(spanishRoutes); @@ -658,6 +781,7 @@ describe("curi", () => { }); it("returns a function to unsubscribe when called", () => { + const history = InMemory(); const routes = prepareRoutes([ { name: "Home", path: "" }, { name: "Not Found", path: "(.*)" } @@ -809,6 +933,44 @@ describe("curi", () => { router.observe(check); }); + describe("suspend", () => { + it("calls function before navigation is finished", done => { + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "About", path: "about" }, + { + name: "Contact", + path: "contact", + children: [ + { + name: "How", + path: ":method", + resolve: { + test: () => Promise.resolve() + } + } + ] + } + ]); + const history = InMemory({ locations: ["/"] }); + const router = curi(history, routes, { + suspend: true + }); + // Register an observer function, but don't call it immediately + // so that we can compare its received location to the history's + // current location. + router.observe( + ({ response }) => { + expect(response.location.pathname).toBe("/contact/phone"); + expect(history.location.pathname).toBe("/"); + done(); + }, + { initial: false } + ); + history.navigate("/contact/phone"); + }); + }); + it("does not emit responses for cancelled navigation", done => { const routes = prepareRoutes([ { name: "Home", path: "" }, @@ -845,6 +1007,7 @@ describe("curi", () => { it("immediately called with most recent response/navigation", () => { const routes = prepareRoutes([{ name: "Home", path: "" }]); const sub = jest.fn(); + const history = InMemory(); const router = curi(history, routes); const { response, navigation } = router.current(); router.observe(sub, { initial: true }); @@ -868,6 +1031,7 @@ describe("curi", () => { } ]); const sub = jest.fn(); + const history = InMemory(); const router = curi(history, routes); router.once(() => { router.observe(sub, { initial: true }); @@ -887,6 +1051,7 @@ describe("curi", () => { } ]); const sub = jest.fn(); + const history = InMemory(); const router = curi(history, routes); router.observe(sub, { initial: true }); expect(sub.mock.calls.length).toBe(0); @@ -914,6 +1079,7 @@ describe("curi", () => { expect(response.name).toBe("Catch All"); done(); }); + const history = InMemory(); const router = curi(history, routes); router.once(() => { router.observe(everyTime, { initial: false }); @@ -1047,6 +1213,48 @@ describe("curi", () => { router.once(check); }); + describe("suspend", () => { + it("calls function before navigation is finished", done => { + const routes = prepareRoutes([ + { name: "Home", path: "" }, + { name: "About", path: "about" }, + { + name: "Contact", + path: "contact", + children: [ + { + name: "How", + path: ":method", + resolve: { + test: () => Promise.resolve() + } + } + ] + } + ]); + const history = InMemory({ locations: ["/"] }); + const router = curi(history, routes, { + suspend: true + }); + // Register a one timer function, but don't call it immediately + // so that we can compare its received location to the history's + // current location. + // While router.once() is typically used with the initial location, + // the easiest way to verify that a one time function is called before + // finishing navigation is to verify that its location is the new one, + // while the history's is the old one. + router.once( + ({ response }) => { + expect(response.location.pathname).toBe("/contact/phone"); + expect(history.location.pathname).toBe("/"); + done(); + }, + { initial: false } + ); + history.navigate("/contact/phone"); + }); + }); + it("does not emit responses for cancelled navigation", done => { const routes = prepareRoutes([ { name: "Home", path: "" }, @@ -1083,6 +1291,7 @@ describe("curi", () => { it("immediately called with most recent response/navigation", () => { const routes = prepareRoutes([{ name: "Home", path: "" }]); const sub = jest.fn(); + const history = InMemory(); const router = curi(history, routes); const { response, navigation } = router.current(); router.once(sub, { initial: true }); @@ -1135,6 +1344,7 @@ describe("curi", () => { it("has response, is not immediately called", done => { const routes = prepareRoutes([{ name: "Home", path: "" }]); const oneTime = jest.fn(); + const history = InMemory(); const router = curi(history, routes); router.once(() => { router.once(oneTime, { initial: false }); @@ -1152,6 +1362,7 @@ describe("curi", () => { expect(response.name).toBe("Catch All"); done(); }); + const history = InMemory(); const router = curi(history, routes); router.once(() => { router.once(oneTime, { initial: false }); @@ -1597,7 +1808,7 @@ describe("curi", () => { } ]); let hasEmitted = false; - + const history = InMemory(); history.navigate = jest.fn((loc, navType) => { expect(navType).toBe("REPLACE"); expect(hasEmitted).toBe(true); diff --git a/packages/router/types/types/curi.d.ts b/packages/router/types/types/curi.d.ts index a16e38e24..479532279 100644 --- a/packages/router/types/types/curi.d.ts +++ b/packages/router/types/types/curi.d.ts @@ -6,6 +6,7 @@ import { Response, Params } from "./response"; export interface Navigation { action: Action; previous: Response | null; + finish?: () => void; } export interface Emitted { response: Response; @@ -27,6 +28,7 @@ export interface RouterOptions { emitRedirects?: boolean; automaticRedirects?: boolean; external?: any; + suspend?: boolean; } export interface CurrentResponse { response: Response | null; @@ -51,4 +53,5 @@ export interface CuriRouter { history: History; current(): CurrentResponse; navigate(options: NavigationDetails): void; + finish?: () => void; }