diff --git a/packages/subapp-react/lib/framework-lib.js b/packages/subapp-react/lib/framework-lib.js index 52c26ed04..5c34df629 100644 --- a/packages/subapp-react/lib/framework-lib.js +++ b/packages/subapp-react/lib/framework-lib.js @@ -141,7 +141,8 @@ class FrameworkLib { assert(Provider, "subapp-web: react-redux Provider not available"); // finally render the element with Redux Provider and the store created return await this.renderTo( - React.createElement(Provider, { store: this.store }, this.createTopComponent()) + React.createElement(Provider, { store: this.store }, this.createTopComponent()), + options ); } return ""; diff --git a/packages/subapp-react/package.json b/packages/subapp-react/package.json index 81f26da61..9f7ff3aec 100644 --- a/packages/subapp-react/package.json +++ b/packages/subapp-react/package.json @@ -39,13 +39,15 @@ "@babel/register": "^7.7.7", "babel-preset-minify": "^0.5.1", "electrode-archetype-njs-module-dev": "^3.0.0", + "jsdom": "^15.2.1", "react": "^16.8.3", "react-async-ssr": "^0.6.0", "react-dom": "^16.8.3", "react-redux": "^6.0.1", "react-router": "^5.1.2", "react-router-dom": "^5.1.2", - "redux": "^4.0.1" + "redux": "^4.0.1", + "run-verify": "^1.2.2" }, "peerDependencies": { "react": "*", @@ -78,10 +80,10 @@ "**/.babelrc.js" ], "check-coverage": true, - "statements": 0, - "branches": 0, - "functions": 0, - "lines": 0, + "statements": 100, + "branches": 100, + "functions": 100, + "lines": 100, "cache": true } } diff --git a/packages/subapp-react/src/fe-framework-lib.js b/packages/subapp-react/src/fe-framework-lib.js index deacf817a..7cb9095c7 100644 --- a/packages/subapp-react/src/fe-framework-lib.js +++ b/packages/subapp-react/src/fe-framework-lib.js @@ -11,7 +11,6 @@ class FrameworkLib { const props = { ...options._prepared, ...options.props }; const Component = subApp.info.StartComponent || subApp.info.Component; - if (element) { if (options.serverSideRendering) { hydrate(, element); diff --git a/packages/subapp-react/src/index.js b/packages/subapp-react/src/index.js index 91d0e89aa..117a801d6 100644 --- a/packages/subapp-react/src/index.js +++ b/packages/subapp-react/src/index.js @@ -9,3 +9,5 @@ export * from "subapp-web"; export { default as React } from "react"; export { default as AppContext } from "./app-context"; + +export { FrameworkLib }; diff --git a/packages/subapp-react/test/spec/fe-framework.spec.js b/packages/subapp-react/test/spec/fe-framework.spec.js new file mode 100644 index 000000000..884cd2461 --- /dev/null +++ b/packages/subapp-react/test/spec/fe-framework.spec.js @@ -0,0 +1,62 @@ +"use strict"; + +const React = require("react"); // eslint-disable-line +const feLib = require("../../src"); +const { JSDOM } = require("jsdom"); + +describe("FE React framework", function() { + // + it("should setup FrameworkLib", () => { + expect(feLib.React).to.be.ok; + expect(feLib.AppContext).to.be.ok; + expect(feLib.loadSubApp).to.be.a("function"); + expect(feLib.FrameworkLib).to.be.ok; + }); + + it("should render component into DOM element", () => { + const dom = new JSDOM(`
`); + global.window = dom.window; + const element = dom.window.document.getElementById("test"); + const framework = new feLib.FrameworkLib({ + subApp: { + info: { + Component: props =>

hello {props.foo}

+ } + }, + element, + options: { props: { foo: "bar" } } + }); + framework.renderStart(); + expect(element.innerHTML).equals(`

hello bar

`); + }); + + it("should hydrate render component into DOM element", () => { + const dom = new JSDOM(`

hello bar

`); + global.window = dom.window; + const element = dom.window.document.getElementById("test"); + const framework = new feLib.FrameworkLib({ + subApp: { + info: { + Component: props =>

hello {props.foo}

+ } + }, + element, + options: { props: { foo: "bar" }, serverSideRendering: true } + }); + framework.renderStart(); + expect(element.innerHTML).equals(`

hello bar

`); + }); + + it("should just return the component without DOM element", () => { + const Component = props =>

hello {props.foo}

; + + const framework = new feLib.FrameworkLib({ + subApp: { + info: { Component } + }, + options: { props: { foo: "bar" }, serverSideRendering: true } + }); + const c = framework.renderStart(); + expect(c.type).equals(Component); + }); +}); diff --git a/packages/subapp-react/test/spec/ssr-framework.spec.js b/packages/subapp-react/test/spec/ssr-framework.spec.js index 919df0b9d..fa4a37176 100644 --- a/packages/subapp-react/test/spec/ssr-framework.spec.js +++ b/packages/subapp-react/test/spec/ssr-framework.spec.js @@ -5,6 +5,9 @@ const React = require("react"); // eslint-disable-line const lib = require("../../lib"); const { withRouter } = require("react-router"); const { Route, Switch } = require("react-router-dom"); // eslint-disable-line +const { asyncVerify } = require("run-verify"); +const Redux = require("redux"); +const { connect } = require("react-redux"); describe("SSR React framework", function() { it("should setup React framework", () => { @@ -24,6 +27,16 @@ describe("SSR React framework", function() { expect(res).contains("has no StartComponent"); }); + it("should not do SSR if serverSideRendering is not true", async () => { + const framework = new lib.FrameworkLib({ + subApp: { Component: () => {} }, + subAppServer: {}, + options: { serverSideRendering: false } + }); + const res = await framework.handleSSR(); + expect(res).equals(""); + }); + it("should render subapp with w/o initial props if no prepare provided", async () => { const framework = new lib.FrameworkLib({ subApp: { @@ -59,6 +72,84 @@ describe("SSR React framework", function() { expect(res).contains("Hello foo bar"); }); + it("should render Component with streaming if enabled", () => { + const framework = new lib.FrameworkLib({ + subApp: { + prepare: () => ({ test: "foo bar" }), + Component: props => { + return
Hello {props.test}
; + } + }, + subAppServer: {}, + options: { serverSideRendering: true, streaming: true }, + context: { + user: {} + } + }); + return asyncVerify( + () => framework.handleSSR(), + (stream, next) => { + let res = ""; + stream.on("data", data => (res += data.toString())); + stream.on("end", () => next(null, res)); + stream.on("error", next); + }, + res => expect(res).contains("Hello foo bar") + ); + }); + + it("should hydrate render Component with streaming if enabled", () => { + const framework = new lib.FrameworkLib({ + subApp: { + prepare: () => ({ test: "foo bar" }), + Component: props => { + return
Hello {props.test}
; + } + }, + subAppServer: {}, + options: { serverSideRendering: true, streaming: true, hydrateServerData: true }, + context: { + user: {} + } + }); + return asyncVerify( + () => framework.handleSSR(), + (stream, next) => { + let res = ""; + stream.on("data", data => (res += data.toString())); + stream.on("end", () => next(null, res)); + stream.on("error", next); + }, + res => expect(res).contains(`
Hello foo bar
`) + ); + }); + + it("should render Component from subapp with hydration info", async () => { + const framework = new lib.FrameworkLib({ + subApp: { + prepare: () => ({ + test: "foo bar" + }), + Component: props => { + return
Hello {props.test}
; + } + }, + subAppServer: {}, + options: { + serverSideRendering: true, + hydrateServerData: true + }, + context: { + user: {} + } + }); + // data-reactroot isn't getting created due to Context.Provider + // see https://github.com/facebook/react/issues/15012 + const res = await framework.handleSSR(); + // but the non-static renderToString adds a for some reason + expect(res).contains("Hello foo bar"); + }); + it("should render Component from subapp with initial props from server's prepare", async () => { const framework = new lib.FrameworkLib({ subApp: { @@ -78,6 +169,113 @@ describe("SSR React framework", function() { expect(res).contains("Hello foo bar"); }); + it("should init redux store and render Component", async () => { + const Component = connect(x => x)(props =>
Hello {props.test}
); + + const framework = new lib.FrameworkLib({ + subApp: { + __redux: true, + Component, + reduxCreateStore: initState => Redux.createStore(x => x, initState), + prepare: () => ({ test: "foo bar" }) + }, + subAppServer: {}, + options: { serverSideRendering: true }, + context: { + user: {} + } + }); + const res = await framework.handleSSR(); + expect(res).contains("Hello foo bar"); + expect(framework.initialStateStr).equals(`{"test":"foo bar"}`); + }); + + it("should init redux store and render Component but doesn't attach initial state", async () => { + const Component = connect(x => x)(props =>
Hello {props.test}
); + + const framework = new lib.FrameworkLib({ + subApp: { + __redux: true, + Component, + reduxCreateStore: initState => Redux.createStore(x => x, initState), + prepare: () => ({ test: "foo bar" }) + }, + subAppServer: { attachInitialState: false }, + options: { serverSideRendering: true }, + context: { + user: {} + } + }); + const res = await framework.handleSSR(); + expect(res).contains("Hello foo bar"); + expect(framework.initialStateStr).equals(undefined); + }); + + it("should init redux store but doesn't render Component if serverSideRendering is not true", async () => { + const Component = connect(x => x)(props =>
Hello {props.test}
); + + const framework = new lib.FrameworkLib({ + subApp: { + __redux: true, + Component, + reduxCreateStore: initState => Redux.createStore(x => x, initState), + prepare: () => ({ test: "foo bar" }) + }, + subAppServer: { attachInitialState: false }, + options: { serverSideRendering: false }, + context: { + user: {} + } + }); + const res = await framework.handleSSR(); + expect(res).equals(""); + expect(framework.initialStateStr).equals(undefined); + }); + + it("should init redux store with empty state without prepare and render Component", async () => { + const Component = connect(x => x)(props =>
Hello {props.test}
); + + const framework = new lib.FrameworkLib({ + subApp: { + __redux: true, + Component, + reduxCreateStore: initState => Redux.createStore(x => x, initState) + }, + subAppServer: {}, + options: { serverSideRendering: true }, + context: { + user: {} + } + }); + const res = await framework.handleSSR(); + expect(res).contains("Hello <"); + expect(framework.initialStateStr).equals(`{}`); + }); + + it("should hydrate render Component with suspense using react-async-ssr", async () => { + const framework = new lib.FrameworkLib({ + subApp: { + Component: props => { + return ( + Loading...}> +
Hello {props.test}
+
+ ); + } + }, + subAppServer: { + prepare: () => ({ test: "foo bar" }) + }, + options: { serverSideRendering: true, suspenseSsr: true, hydrateServerData: true }, + context: { + user: {} + } + }); + const res = await framework.handleSSR(); + // react-async-ssr includes data-reactroot + expect(res).contains(`
Hello foo bar
`); + }); + it("should render Component with suspense using react-async-ssr", async () => { const framework = new lib.FrameworkLib({ subApp: {