From 1f20ab5adbd95e74532e50b7ae2df1733e509a87 Mon Sep 17 00:00:00 2001 From: Simon Andrews Date: Wed, 12 Dec 2018 21:48:09 +0000 Subject: [PATCH 1/4] build: reset prettier config to default --- .prettierrc | 1 + 1 file changed, 1 insertion(+) create mode 100644 .prettierrc diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1 @@ +{} From 243e2902dadd3a86433781cdcde1e30f9445cff9 Mon Sep 17 00:00:00 2001 From: Simon Andrews Date: Wed, 12 Dec 2018 21:56:07 +0000 Subject: [PATCH 2/4] refactor: extract rehydrateChildren to a separate file --- packages/react-from-markup/src/index.ts | 3 +- .../src/rehydrateChildren.ts | 52 +++++++++++++++++++ packages/react-from-markup/src/rehydrator.ts | 50 +----------------- 3 files changed, 55 insertions(+), 50 deletions(-) create mode 100644 packages/react-from-markup/src/rehydrateChildren.ts diff --git a/packages/react-from-markup/src/index.ts b/packages/react-from-markup/src/index.ts index 054d4b3..2888eb5 100644 --- a/packages/react-from-markup/src/index.ts +++ b/packages/react-from-markup/src/index.ts @@ -1 +1,2 @@ -export { default, rehydrateChildren } from "./rehydrator"; +export { default } from "./rehydrator"; +export { default as rehydrateChildren } from "./rehydrateChildren"; diff --git a/packages/react-from-markup/src/rehydrateChildren.ts b/packages/react-from-markup/src/rehydrateChildren.ts new file mode 100644 index 0000000..e4eea68 --- /dev/null +++ b/packages/react-from-markup/src/rehydrateChildren.ts @@ -0,0 +1,52 @@ +import domElementToReact from "dom-element-to-react"; + +import IOptions from "./IOptions"; +import IRehydrator from "./IRehydrator"; + +const rehydratableToReactElement = async ( + el: Element, + rehydrators: IRehydrator, + options: IOptions +): Promise> => { + const rehydratorName = el.getAttribute("data-rehydratable"); + + if (!rehydratorName) { + throw new Error("Rehydrator name is missing from element."); + } + + const rehydrator = rehydrators[rehydratorName]; + + if (!rehydrator) { + throw new Error(`No rehydrator found for type ${rehydratorName}`); + } + + return rehydrator( + el, + children => rehydrateChildren(children, rehydrators, options), + options.extra + ); +}; + +const createCustomHandler = ( + rehydrators: IRehydrator, + options: IOptions +) => async (node: Node) => { + // This function will run on _every_ node that domElementToReact encounters. + // Make sure to keep the conditional highly performant. + if ( + node.nodeType === Node.ELEMENT_NODE && + (node as Element).hasAttribute("data-rehydratable") + ) { + return rehydratableToReactElement(node as Element, rehydrators, options); + } + + return false; +}; + +const rehydrateChildren = ( + el: Node, + rehydrators: IRehydrator, + options: IOptions +) => domElementToReact(el, createCustomHandler(rehydrators, options)); + +export default rehydrateChildren; diff --git a/packages/react-from-markup/src/rehydrator.ts b/packages/react-from-markup/src/rehydrator.ts index ee23ebc..20f6efe 100644 --- a/packages/react-from-markup/src/rehydrator.ts +++ b/packages/react-from-markup/src/rehydrator.ts @@ -1,54 +1,8 @@ -import domElementToReact from "dom-element-to-react"; import * as ReactDOM from "react-dom"; import IOptions from "./IOptions"; import IRehydrator from "./IRehydrator"; - -const rehydratableToReactElement = async ( - el: Element, - rehydrators: IRehydrator, - options: IOptions -): Promise> => { - const rehydratorName = el.getAttribute("data-rehydratable"); - - if (!rehydratorName) { - throw new Error("Rehydrator name is missing from element."); - } - - const rehydrator = rehydrators[rehydratorName]; - - if (!rehydrator) { - throw new Error(`No rehydrator found for type ${rehydratorName}`); - } - - return rehydrator( - el, - children => rehydrateChildren(children, rehydrators, options), - options.extra - ); -}; - -const createCustomHandler = ( - rehydrators: IRehydrator, - options: IOptions -) => async (node: Node) => { - // This function will run on _every_ node that domElementToReact encounters. - // Make sure to keep the conditional highly performant. - if ( - node.nodeType === Node.ELEMENT_NODE && - (node as Element).hasAttribute("data-rehydratable") - ) { - return rehydratableToReactElement(node as Element, rehydrators, options); - } - - return false; -}; - -const rehydrateChildren = ( - el: Node, - rehydrators: IRehydrator, - options: IOptions -) => domElementToReact(el, createCustomHandler(rehydrators, options)); +import rehydrateChildren from "./rehydrateChildren"; const render = ({ rehydrated, @@ -111,5 +65,3 @@ export default async ( await Promise.all(renders.map(r => r().then(render))); }; - -export { IRehydrator, rehydratableToReactElement, rehydrateChildren }; From c85f29cd8c2708ec50d6e747aa03475af5a7f400 Mon Sep 17 00:00:00 2001 From: Simon Andrews Date: Wed, 12 Dec 2018 21:56:32 +0000 Subject: [PATCH 3/4] feat: export IOptions and IRehydrator types from package --- packages/react-from-markup/src/index.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/react-from-markup/src/index.ts b/packages/react-from-markup/src/index.ts index 2888eb5..c48f168 100644 --- a/packages/react-from-markup/src/index.ts +++ b/packages/react-from-markup/src/index.ts @@ -1,2 +1,5 @@ export { default } from "./rehydrator"; export { default as rehydrateChildren } from "./rehydrateChildren"; + +export { default as IOptions } from "./IOptions"; +export { default as IRehydrator } from "./IRehydrator"; From 17cbe9859eb8e2743b5c3098abc5fc08b2291d66 Mon Sep 17 00:00:00 2001 From: Simon Andrews Date: Wed, 12 Dec 2018 22:56:23 +0000 Subject: [PATCH 4/4] test: add tests for rehydrate --- .../__snapshots__/rehydrator.test.ts.snap | 8 + .../src/__tests__/rehydrator.test.ts | 182 ++++++++++++++++++ 2 files changed, 190 insertions(+) create mode 100644 packages/react-from-markup/src/__tests__/__snapshots__/rehydrator.test.ts.snap create mode 100644 packages/react-from-markup/src/__tests__/rehydrator.test.ts diff --git a/packages/react-from-markup/src/__tests__/__snapshots__/rehydrator.test.ts.snap b/packages/react-from-markup/src/__tests__/__snapshots__/rehydrator.test.ts.snap new file mode 100644 index 0000000..972e024 --- /dev/null +++ b/packages/react-from-markup/src/__tests__/__snapshots__/rehydrator.test.ts.snap @@ -0,0 +1,8 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`rehydrate should handle an exception in rehydrateChildren 1`] = ` +Array [ + "Rehydration failure", + "test rejection", +] +`; diff --git a/packages/react-from-markup/src/__tests__/rehydrator.test.ts b/packages/react-from-markup/src/__tests__/rehydrator.test.ts new file mode 100644 index 0000000..6691883 --- /dev/null +++ b/packages/react-from-markup/src/__tests__/rehydrator.test.ts @@ -0,0 +1,182 @@ +// We're testing some console functionality +/* tslint:disable no-console */ + +import rehydrate from "../rehydrator"; + +import * as MockReactDOM from "react-dom"; +import mockRehydrateChildren from "../rehydrateChildren"; + +jest.mock("react-dom"); +jest.mock("../rehydrateChildren"); + +const defaultRehydrators = {}; +const defaultOptions = { + extra: {} +}; + +describe("rehydrate", () => { + // tslint:disable-next-line no-console + const originalConsoleError = console.error; + + beforeEach(() => { + (mockRehydrateChildren as any).mockClear(); + (mockRehydrateChildren as any).mockImplementation(() => + Promise.resolve({}) + ); + + (MockReactDOM.render as any).mockClear(); + (MockReactDOM.unmountComponentAtNode as any).mockClear(); + + // tslint:disable-next-line no-console + console.error = jest.fn(); + }); + + afterEach(() => { + // tslint:disable-next-line no-console + console.error = originalConsoleError; + }); + + it("should find markup containers", async () => { + const el = document.createElement("div"); + + el.innerHTML = ` +
+
+

+ + + `; + + const containers = Array.from( + el.querySelectorAll("[data-react-from-markup-container]") + ); + + await rehydrate(el, defaultRehydrators, defaultOptions); + + expect(mockRehydrateChildren).toHaveBeenCalledTimes(2); + + for (const container of containers) { + expect(mockRehydrateChildren).toHaveBeenCalledWith( + container, + {}, + { + extra: {} + } + ); + } + }); + + it("should not rehydrate inside nested containers", async () => { + const el = document.createElement("div"); + + el.innerHTML = ` +
+
+
+ `; + + const containers = Array.from( + el.querySelectorAll( + "[data-react-from-markup-container] [data-react-from-markup-container]" + ) + ); + + await rehydrate(el, defaultRehydrators, defaultOptions); + + expect(mockRehydrateChildren).toHaveBeenCalledTimes(1); + + for (const container of containers) { + expect(mockRehydrateChildren).not.toHaveBeenCalledWith( + container, + {}, + { + extra: {} + } + ); + } + }); + + it("should handle an exception in rehydrateChildren", async () => { + (mockRehydrateChildren as any).mockImplementation(() => + Promise.reject("test rejection") + ); + + const el = document.createElement("div"); + + el.innerHTML = ` +
+ hello world +
+ `; + + await rehydrate(el, defaultRehydrators, defaultOptions); + + expect(console.error).toHaveBeenCalledTimes(1); + expect((console.error as any).mock.calls[0]).toMatchSnapshot(); + }); + + it("should resolve only when all containers have rehydrated", async () => { + const resolves: Array<() => void> = []; + + (mockRehydrateChildren as any).mockImplementation( + () => new Promise(resolve => resolves.push(resolve)) + ); + + const el = document.createElement("div"); + + el.innerHTML = ` +
+ hello world +
+
+ hello world 2 +
+ `; + + let resolved = false; + + const promise = rehydrate(el, defaultRehydrators, defaultOptions).then( + () => (resolved = true) + ); + + expect(resolved).toBe(false); + + for (const resolve of resolves) { + resolve(); + + if (resolves.indexOf(resolve) === resolves.length - 1) { + await promise; + expect(resolved).toBe(true); + } else { + expect(resolved).toBe(false); + } + } + }); + + it("should always attempt to unmount before rendering", async () => { + const el = document.createElement("div"); + + el.innerHTML = ` +
+ hello world +
+
+ hello world 2 +
+ `; + + const containers = Array.from( + el.querySelectorAll("[data-react-from-markup-container]") + ); + + await rehydrate(el, defaultRehydrators, defaultOptions); + + expect(MockReactDOM.unmountComponentAtNode).toHaveBeenCalledTimes(2); + + for (const container of containers) { + expect(MockReactDOM.unmountComponentAtNode).toHaveBeenCalledWith( + container + ); + } + }); +});