From adf2d57214c052c025e5d220dff07079628deaed Mon Sep 17 00:00:00 2001 From: Sean Matheson Date: Mon, 11 Apr 2016 14:24:00 +0100 Subject: [PATCH] perf(api): Massive simplification to API, along with guards and 100% test cov. The API has been refocused to allow for the injection of Components of any type (i.e. class/stateless/createClass). The Provider has been renamed to make it more explicit, and the Injector API has been greatly simplified. A lot more invariant based checks have been added to the project and we have achieved full test coverage. closes #2 closes #3 BREAKING CHANGE: Provider renamed to InjectableProvider. BREAKING CHANGE: The Injector helper has been greatly simplified. Instead of wrapping a component that will host the injection, it rather produces an injection component which you can render into any component that you would like to initiate an injection. This produces a much cleaner API and allows you to easily pass down specific props to the component that will be injected. Please look at the updated readme for example usage. --- README.md | 123 +++++---- examples/router/package.json | 11 +- examples/router/src/InjectableHeader.js | 8 +- examples/router/src/PageOne.js | 34 ++- examples/router/src/PageTwo.js | 30 ++- examples/router/src/index.js | 2 +- package.json | 27 +- src/Injectable.js | 21 +- src/{Provider.js => InjectablesProvider.js} | 123 +++++---- src/Injector.js | 58 ++-- src/index.js | 2 +- test/Injectable.test.js | 139 ++++++++++ test/InjectablesProvider.test.js | 279 ++++++++++++++++++++ test/Injector.test.js | 268 +++++++++++++++++++ test/Provider.test.js | 178 ------------- test/integration.test.js | 83 +++--- 16 files changed, 964 insertions(+), 422 deletions(-) rename src/{Provider.js => InjectablesProvider.js} (50%) create mode 100644 test/Injectable.test.js create mode 100644 test/InjectablesProvider.test.js create mode 100644 test/Injector.test.js delete mode 100644 test/Provider.test.js diff --git a/README.md b/README.md index ea95764..b6c3b22 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

-

Explicitly inject Elements to any part of your React render tree

+

Explicitly inject Components to any part of your React render tree

[![Travis](https://img.shields.io/travis/ctrlplusb/react-injectables.svg?style=flat-square)](https://travis-ci.org/ctrlplusb/react-injectables) @@ -9,15 +9,11 @@ [![Codecov](https://img.shields.io/codecov/c/github/ctrlplusb/react-injectables.svg?style=flat-square)](https://codecov.io/github/ctrlplusb/react-injectables) [![Maintenance](https://img.shields.io/maintenance/yes/2016.svg?style=flat-square)]() -Tiny, and the only dependency is a peer dependency of React. - -### Warning - -I am actively developing this project whilst applying it to a production use-case. This is exposing the limitations of the API pretty fast requiring that I do quick updates. Expect the versions to bump excessively over the next couple of weeks, but I will stabilise it as quickly as I can. I'm not a fan of 0.x.x beta releases and don't mind big numbers. I like that each version I release clearly indicates breaking changes. This message will disappear as soon as I believe the API will remain stable for an extended period of time. +This is a teeny tiny React library - it's almost unnoticable when gzipped. ## What is this for? -Placeholders, Modals, etc etc. +Placeholders, Modals, etc. ## Overview @@ -44,26 +40,34 @@ One option would be to use react-routers native capability to pass down multiple Enter Injectables. -Injectables aims to provide you with a mechanism to explicity define an Injectable target and Injector source component(s). +Injectables aims to provide you with a mechanism to explicity define `Injectable` and `Injector` Components. An `Injector` produces a Component that gets injected into an `Injectable`. -With Injectables you can easily inject into `Sidebar` from your `ProductPage` doing something similar to the following: +With Injectables you can easily inject a Component into the `Sidebar` when your `ProductPage` Component gets mounted. Here is a partial example of this: ```javascript -import { Injector } from 'react-injectables'; +const MyBasketSidebarInjection = Injector({ + into: InjectableSidebar +})(MyBasket) class ProductsPage extends Component { ... + + render() { + return ( +
+ + +

Product Page

+ ... +
+ ); + } } - -export default Injector({ - to: Sidebar, - inject: function () { return (); } -})(ProductsPage); ``` -Now every time `ProductPage` is rendered onto the page the `MyBasketSummary` will be injected into `Sidebar`. Ok, there is a tiny bit of additional setup. But it's quick sticks to get going. +Now every time the `ProductPage` Component is mounted the `MyBasket` Component will be injected into `Sidebar` Component. Ok, there is a bit of additional setup required, but the above is a basic overview of how easy it is to define your injections after the initial configuration. -Fairy dust is involved, but we attempt to keep things as un-magical as possible, keeping things explict, whilst also easy to use. +Yep, some fairy dust is involved, but we attempt to keep things as un-magical as possible, pushing for explictness whilst keeping it easy to use. ## Usage @@ -76,9 +80,9 @@ First install the library. To get going there are only 3 steps: - 1. Wrap your application with our `Provider`. - 2. Wrap a component you would like to _receive_ content with our `Injectable`. e.g. `Injectable(Sidebar)` - 4. Wrap a component you would like to _produce_ content with our `Injector`. e.g.: `Injector({ to: InjectableSidebar, inject: () => )(ProductPage)` + 1. Wrap your application with our `InjectablesProvider`. + 2. Wrap a Component you would like to _receive_ injected content with our `Injectable` helper. e.g. `Injectable(MainScreen)` + 4. Wrap a Component you would like to _inject_ with our `Injector` helper. e.g.: `Injector({ into: MainScreen })(MyModal)` ### Full Tutorial @@ -86,38 +90,35 @@ Ok, here's a more detailed run through with example code. __Step 1__ -Somewhere very low down in your application wrap your component tree with our `Provider`. This is the engine that will do all the heavy lifting for you. For example: +Somewhere very low down in your application wrap your component tree with our `InjectablesProvider`. This is the engine that will do all the heavy lifting for you. For example: ```javascript import React from 'react'; import ReactDOM from 'react-dom'; -import { Provider } from 'react-injectables'; +import { InjectablesProvider } from 'react-injectables'; ReactDOM.render(( - + ... - + ), document.getElementById('app')); ``` -_Note:_ If you already have another component with the name of `Provider` in your app (think redux), then you can rename the import, like so: -`import { Provider as InjectablesProvider } from 'react-injectables';` - __Step 2__ -Now you need to create an Injectable Component. Let's say that you would like to create a Sidebar component that you could inject in to. You would do the following: +Now you need to create an `Injectable` Component. Let's say that you would like to create a `Sidebar` component that you could inject in to. You would do the following: ```javascript import React, { PropTypes } from 'react'; import { Injectable } from 'react-injectables'; -// Note the 'injected' prop. This will contain any injected elements. -function Sidebar({ injected }) { +// Note the 'injections' prop. This will contain any injected elements. +function Sidebar({ injections }) { return (
- {injected} + {injections}
); } @@ -132,45 +133,43 @@ __Step 3__ Ok, so you have an `InjectableSidebar` now, but you need to declare the components that will inject content in to it. -You need to make use of our `Injector`, wrapping your component, whilst also providing the target injectable component and the elements that you wish to inject. +You need to make use of our `Injector`, wrapping the Component you would like to inject, whilst also providing the target `Injectable` Component you created. ```javascript import React from 'react'; import { Injector } from 'react-injectables'; import InjectableSidebar from './InjectableSidebar'; -import BasketViewPage from './BasketViewPage'; +import MyBasketView from './MyBasketView'; + +// This sets up our injection. +const MyBasketViewSidebarInjection = Injector({ + into: InjectableSidebar // The target Injectable Component. The actual Component - explicit. :) +})(MyBasketView); // The Component you would like to be injected. class ProductsPage extends Component { .... + + render() { + return ( +
+ {/* The injection! Nothing actually gets rendered here, it gets sent to + our target Injectable. In this case it means that MyBasketView will + be injected into the Sidebar. + Notice how you can also pass down props into your injected component too. */} + + +

Products Page

+
+ ); + } } -// We are using this stateless component as our inject resolver. -// It will receive any props that will be passed into ProductsPage, including updated ones. -function Inject(props) { - return (); -} - -export default Injector({ - to: InjectableSidebar, // You have to specify the actual Injectable Component. Explicit. - inject: Inject // The inject function or a React Element. -})(ProductsPage); +export default ProductsPage; ``` -The `inject` property argument for the `Injector` accepts either of two things: - - * A function with a `props` argument that when executed will return a React Element. i.e. a stateless functional component. It will recieve the same props that will be passed into the component being wrapped by the `Injector`. If you wrap your component with multiple higher order functions/components then please make sure that `Injector` is the first of the functions used to wrap your component. For e.g. - ```javascript - export default compose( // executes last to first. - Connect(stateM, propsM), - Injector(args) - )(ProductsPage); - ``` - - * A React element. This works, however the element is created up front before it is even needed. Also you lose the capability to interogate any props that may have been passed into your component. - --- -And that's it. Any time the `ProductsPage` is rendered it will inject it's content into the `Sidebar`. When it unmounts, it's injected elements will be removed from the `Sidebar`. +And that's it. Any time the `ProductsPage` is mounted it will inject the `MyBasketView` Component into the `Sidebar`. When it unmounts, the respective Component will be removed from the `Sidebar`. As you can see it's all explicit, so you can follow the import references to find out any relationships. @@ -180,17 +179,17 @@ You will need to repeat steps 2 and 3 for any set of Injectable targets and Inje Here are a few basic properties you should be aware of: - * All injection happens within the initial render cycle by react-dom. Injection does not cause a double render to occur. This is a result of us trying to intentionally keep injection as "uni-directional" / "input -> output" as possible. + * All injection happens within the initial render cycle by react-dom. Injection does not cause a double render to occur on your `Injectable`s. This is a result of us trying to intentionally keep injection as "input to output" as possible. - * You can have multiple instances of an Injectable rendered in your app. They will all recieve the same injected content from their respective Injectors. + * You can have multiple instances of an `Injectable` rendered in your app. They will all recieve the same injected content from their respective `Injector`s. - * You can create multiple Injectors components targetting the same Injectable component. The rendered Injectors shall have their injected elements collected and passed through to the target Injectable. For example, you may want to pass in action buttons from different components into an InjectableActions component. + * You can create multiple `Injector`s Components targetting the same `Injectable` component. For example, you may want to pass in action buttons from different components into an InjectableActions component. - * If an Injector is removed from the tree then it's injected elements will automatically be removed from the Injectable target. + * If an Component that is hosting `Injector` is unmounted then the injected Components will automatically be removed from the `Injectable` target. - * Any new Injectors that are rendered into the tree will automatically have their injected content rendered within any rendered Injectable target. + * Any new `Injector`s that are rendered into the tree will automatically have their injected Components passed into any existing `Injectable` targets. i.e. a props update. - * Injectors are allowed to be rendered before any Injectables. Once their target Injectable Component is rendered then any content from existing the Injectors will automatically be rendered within the newly rendered Injectable. + * `Injector`s are allowed to be mounted before any `Injectable`s. Once their target `Injectable` Component is mounted then any Components from existing `Injector`s will automatically be passed into the newly mounted `Injectable`. ## Examples diff --git a/examples/router/package.json b/examples/router/package.json index 6f90afa..7957d28 100644 --- a/examples/router/package.json +++ b/examples/router/package.json @@ -9,19 +9,18 @@ "author": "", "license": "MIT", "dependencies": { - "babel-cli": "6.6.5", - "babel-core": "6.7.4", + "babel-cli": "6.7.5", + "babel-core": "6.7.6", "babel-loader": "6.2.4", "babel-preset-es2015": "6.6.0", "babel-preset-react": "6.5.0", "babel-preset-stage-1": "6.5.0", "babel-register": "6.7.2", "express": "4.13.4", - "react": "0.14.8", - "react-dom": "0.14.8", - "react-injectables": "1.0.0", + "react": "15.0.1", + "react-dom": "15.0.1", "react-router": "2.0.1", - "webpack": "1.12.14", + "webpack": "1.12.15", "webpack-dev-middleware": "1.6.1", "webpack-hot-middleware": "2.10.0" } diff --git a/examples/router/src/InjectableHeader.js b/examples/router/src/InjectableHeader.js index 0a07d15..0cd4f29 100644 --- a/examples/router/src/InjectableHeader.js +++ b/examples/router/src/InjectableHeader.js @@ -2,17 +2,17 @@ import React, { PropTypes } from 'react'; import { Injectable } from '../../../src/index.js'; // Prep a header component which we intend to make injectable. -// Note the prop named 'injected'. This will contain any injected elements. -const Header = ({ injected }) => ( +// Note the prop named 'injections'. This will contain any injected elements. +const Header = ({ injections }) => (

INJECTABLE HEADER

- {injected.length > 0 ? injected :
Nothing has been injected
} + {injections.length > 0 ? injections :
Nothing has been injected
}
); Header.propTypes = { - injected: PropTypes.arrayOf(PropTypes.element) + injections: PropTypes.arrayOf(PropTypes.element) }; // Convert our header into an injectable! diff --git a/examples/router/src/PageOne.js b/examples/router/src/PageOne.js index 215c230..13b78d0 100644 --- a/examples/router/src/PageOne.js +++ b/examples/router/src/PageOne.js @@ -2,34 +2,24 @@ import React, { Component, PropTypes } from 'react'; import { Injector } from '../../../src/index.js'; import InjectableHeader from './InjectableHeader'; -const Content = ({ active }) => ( -
-

I am page one.

-

My State is {active ? `active` : `inactive`}

-
-); -Content.propTypes = { - active: PropTypes.bool.isRequired -}; - -const Inject = ({ active }) => ( +// Our component that we will inject. +const Injection = ({ active }) => (

Injection from Page One

The active prop value is: {active ? `active` : `inactive`}

); -Inject.propTypes = { +Injection.propTypes = { active: PropTypes.bool.isRequired }; -const InjectingContent = Injector({ - to: InjectableHeader, - inject: Inject -})(Content); +// Our Injector instance configured to inject into the InjectableHeader. +const HeaderInjection = Injector({ + into: InjectableHeader +})(Injection); /** - * We wrap our injecting content with a class based component so we can track - * state and pass down props, thereby adding behaviour. + * This is the component that when rendered will cause the injection to occur. */ class PageOne extends Component { state = { @@ -45,7 +35,13 @@ class PageOne extends Component { return (
- + {/* The injection! Nothing gets rendered here. */} + + +
+

I am page one.

+

My State is {active ? `active` : `inactive`}

+
diff --git a/examples/router/src/PageTwo.js b/examples/router/src/PageTwo.js index 206b1c7..d3ae335 100644 --- a/examples/router/src/PageTwo.js +++ b/examples/router/src/PageTwo.js @@ -2,12 +2,7 @@ import React from 'react'; import { Injector } from '../../../src/index.js'; import InjectableHeader from './InjectableHeader'; -const PageTwo = () => ( -
- I am page two. -
-); - +// Our component that we will inject. const Inject = (props) => (
Injection from Page Two.
@@ -16,7 +11,22 @@ const Inject = (props) => (
); -export default Injector({ - to: InjectableHeader, - inject: Inject -})(PageTwo); +// Our Injector instance configured to inject into the InjectableHeader. +const HeaderInjection = Injector({ + into: InjectableHeader +})(Inject); + +/** + * This is the component that when rendered will cause the injection to occur. + */ +const PageTwo = () => ( +
+ {/* The injection! Nothing actually gets rendered here, it gets sent to + our target Injectable. */} + + + I am page two. +
+); + +export default PageTwo; diff --git a/examples/router/src/index.js b/examples/router/src/index.js index efd7375..a7258b0 100644 --- a/examples/router/src/index.js +++ b/examples/router/src/index.js @@ -1,6 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom'; -import { Provider as InjectablesProvider } from '../../../src/index.js'; +import { InjectablesProvider } from '../../../src/index.js'; import { browserHistory, Router, Route, IndexRoute } from 'react-router'; import App from './App'; diff --git a/package.json b/package.json index 3c58208..a82e32c 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,12 @@ "url": "https://github.com/ctrlplusb/react-injectables/issues" }, "homepage": "https://github.com/ctrlplusb/react-injectables#readme", - "dependencies": {}, + "dependencies": { + "invariant": "2.2.1" + }, "devDependencies": { - "babel-cli": "6.6.5", - "babel-core": "6.7.4", + "babel-cli": "6.7.5", + "babel-core": "6.7.6", "babel-eslint": "6.0.2", "babel-loader": "6.2.4", "babel-plugin-typecheck": "3.8.0", @@ -45,27 +47,28 @@ "codecov.io": "0.1.6", "commitizen": "2.7.6", "cz-conventional-changelog": "1.1.5", + "enzyme": "2.2.0", "eslint": "2.7.0", "eslint-config-airbnb": "6.2.0", "eslint-loader": "1.3.0", - "eslint-plugin-mocha": "2.0.0", - "eslint-plugin-react": "4.2.3", - "ghooks": "1.1.1", + "eslint-plugin-mocha": "2.1.0", + "eslint-plugin-react": "4.3.0", + "ghooks": "1.2.0", "isparta": "4.0.0", - "jsdom": "8.3.0", + "jsdom": "8.3.1", "mocha": "2.4.5", "path": "0.12.7", - "react": "0.14.8", - "react-dom": "0.14.8", + "react": "15.0.1", + "react-addons-test-utils": "15.0.1", + "react-dom": "15.0.1", "semantic-release": "^6.2.1", "sinon": "1.17.3", "sinon-chai": "2.8.0", "stats-webpack-plugin": "0.3.1", - "teaspoon": "6.2.0", - "webpack": "1.12.14" + "webpack": "1.12.15" }, "peerDependencies": { - "react": "^0.14.0 || ^15.0.0-rc.1" + "react": "^0.14.0 || ^15.0.0" }, "czConfig": { "path": "node_modules/cz-conventional-changelog" diff --git a/src/Injectable.js b/src/Injectable.js index 9b0ded7..889edf5 100644 --- a/src/Injectable.js +++ b/src/Injectable.js @@ -3,7 +3,7 @@ import { containsUniq, keyedElements } from './utils'; let injectionIdIndex = 0; -const Injectable = (WrappedComponent) => { +const Injectable = (VeinComponent) => { injectionIdIndex++; const injectionId = `injectionId_${injectionIdIndex}`; @@ -16,36 +16,37 @@ const Injectable = (WrappedComponent) => { }; state = { - injected: [] + injections: [] } componentWillMount() { this.context.registerInjectable({ injectionId, - injectable: this + injectableInstance: this, + receive: (elements) => this.consume(elements) }); } componentWillUnmount() { this.context.removeInjectable({ injectionId, - injectable: this + injectableInstance: this }); } consume = (elements) => { - if (elements.length !== this.state.injected.length || - containsUniq(this.state.injected, elements)) { - this.setState({ injected: elements }); + if (elements.length !== this.state.injections.length || + containsUniq(this.state.injections, elements)) { + this.setState({ injections: elements }); } } render() { - const keyed = keyedElements(`injected`, this.state.injected); + const keyed = keyedElements(`injections`, this.state.injections); return ( - ); diff --git a/src/Provider.js b/src/InjectablesProvider.js similarity index 50% rename from src/Provider.js rename to src/InjectablesProvider.js index c071d59..0f7b342 100644 --- a/src/Provider.js +++ b/src/InjectablesProvider.js @@ -1,5 +1,6 @@ import { Children, Component, PropTypes } from 'react'; import { compose, find, filter, map, uniqBy, without, withoutAll } from './utils'; +import invariant from 'invariant'; class InjectablesProvider extends Component { static childContextTypes = { @@ -33,7 +34,7 @@ class InjectablesProvider extends Component { }; } - getRegistration(args: { injectionId: string }) { + getRegistration(args) { const { injectionId } = args; let registration = find( @@ -53,7 +54,7 @@ class InjectablesProvider extends Component { return registration; } - runInjections(args: { registration: Object }) { + runInjections(args) { const { registration } = args; const { injectables, injections } = registration; @@ -64,53 +65,88 @@ class InjectablesProvider extends Component { )(injections); injectables.forEach(injectable => { - injectable.consume(elements); + injectable.receive(elements); }); } - removeRegistration(args: { registration: Object }) { + removeRegistration(args) { const { registration } = args; this.registrations = without(registration)(this.registrations); } - registerInjectable(args: { injectionId: string, injectable: Object}) { - const { injectionId, injectable } = args; + findInjectable({ registration, injectableInstance }) { + const isInjectableInstance = x => Object.is(x.instance, injectableInstance); + return find(isInjectableInstance)(registration.injectables); + } + + clearRegistrationIfEmpty({ registration }) { + if (registration.injectables.length === 0 && registration.injections.length === 0) { + this.removeRegistration({ registration }); + } + } + + registerInjectable(args) { + const { injectionId, injectableInstance, receive } = args; const registration = this.getRegistration({ injectionId }); + const injectable = this.findInjectable( + { registration, injectableInstance }); if (withoutAll(registration.injectables)([injectable]).length > 0) { - registration.injectables = [...registration.injectables, injectable]; + const newInjectable = { + instance: injectableInstance, + receive + }; + registration.injectables = [...registration.injectables, newInjectable]; this.runInjections({ registration }); // First time consumption. } } - removeInjectable(args: { injectionId: string, injectable: Object }) { - const { injectionId, injectable } = args; + removeInjectable(args) { + const { injectionId, injectableInstance } = args; const registration = this.getRegistration({ injectionId }); + const injectable = this.findInjectable( + { registration, injectableInstance }); - const injectables = without(injectable)(registration.injectables); + registration.injectables = without(injectable)(registration.injectables); - if (injectables.length === 0 && registration.injections.length === 0) { - this.removeRegistration({ registration }); - } else { - registration.injectables = injectables; - } + this.clearRegistrationIfEmpty({ registration }); } - findInjection({ registration, injector }) { - return find(x => Object.is(x.injector, injector))(registration.injections); + findInjection({ registration, injectorInstance }) { + const isInjectorInstance = x => Object.is(x.instance, injectorInstance); + return find(isInjectorInstance)(registration.injections); } - registerInjector(args - : { injectionId: string, injectorId: string, injector: Object, inject: Function }) { - const { injectionId, injectorId, injector, inject } = args; + findInjector({ registration, injectorId }) { + const isInjectorId = x => x.injectorId === injectorId; + return find(isInjectorId)(registration.injections); + } + + registerInjector(args) { + const { injectionId, injectorId, injectorInstance, inject } = args; const registration = this.getRegistration({ injectionId }); - const existingInjection = this.findInjection({ registration, injector }); + const existingInjection = this.findInjection({ registration, injectorInstance }); + + invariant( + !existingInjection, + `An Injector instance is being registered multiple times.`); - if (existingInjection) { - return; + if (process.env.NODE_ENV !== `production`) { + const existingInjector = this.findInjector({ registration, injectorId }); + + if (existingInjector && console && console.warn) { // eslint-disable-line no-console + console.warn( // eslint-disable-line no-console + `Multiple instances of an Injector has been found. This may not be ` + + `your intended behaviour`); + } } - const newInjection = { injectorId, injector, inject }; + const newInjection = { + injectorId, + instance: injectorInstance, + inject + }; + registration.injections = [ ...registration.injections, newInjection @@ -119,36 +155,33 @@ class InjectablesProvider extends Component { this.runInjections({ registration }); } - updateInjector(args - : { injectionId: string, injector: Object, inject: Function }) { - const { injectionId, injector, inject } = args; + updateInjector(args) { + const { injectionId, injectorInstance, inject } = args; const registration = this.getRegistration({ injectionId }); - const existingInjection = this.findInjection({ registration, injector }); + const existingInjection = this.findInjection({ registration, injectorInstance }); - if (!existingInjection) { - throw new Error(`Trying to update an Injector that is not registered`); - } + invariant( + existingInjection, + `Trying to update an Injector that is not registered`); existingInjection.inject = inject; this.runInjections({ registration }); } - removeInjector(args: { injectionId: string, injector: Object }) { - const { injectionId, injector } = args; + removeInjector(args) { + const { injectionId, injectorInstance } = args; const registration = this.getRegistration({ injectionId }); - const injection = this.findInjection({ registration, injector }); - - if (injection) { - registration.injections = without(injection)(registration.injections); - this.runInjections({ registration }); - } else { - /* istanbul ignore next */ - if (process.env.NODE_ENV === `development`) { - throw new Error( - `Trying to remove an injector which has not been registered`); - } - } + const injection = this.findInjection({ registration, injectorInstance }); + + invariant( + !!injection, + `Trying to remove an injector which has not been registered`); + + registration.injections = without(injection)(registration.injections); + this.runInjections({ registration }); + + this.clearRegistrationIfEmpty({ registration }); } render() { diff --git a/src/Injector.js b/src/Injector.js index 5f16921..95880bd 100644 --- a/src/Injector.js +++ b/src/Injector.js @@ -1,16 +1,36 @@ import React, { PropTypes, Component } from 'react'; +import invariant from 'invariant'; -type Inject = Object | (props: Object) => Object; +const invalidTargetMsg = + `Invalid Injectable target. Please provide a Component that has been wrapped ` + + `Injectable wrapped Component.`; +const invalidInjectMsg = + `Invalid injection value provided into Injector. You must supply a Component ` + + `or stateless Component.`; let injectorIndex = 0; -const Injector = (args: { to: Object, inject: Inject }) => { - const { to, inject } = args; +const Injector = ({ into } = {}) => { + invariant( + into && + typeof into === `function` && + into.injectionId && + into.contextTypes && + into.contextTypes.registerInjectable && + into.contextTypes.removeInjectable, + // Error message + invalidTargetMsg); injectorIndex++; const injectorId = `injector_${injectorIndex}`; - return function WrapComponent(WrappedComponent) { + return function WrapComponent(InjectionComponent) { + invariant( + InjectionComponent && + typeof InjectionComponent === `function`, + invalidInjectMsg + ); + class InjectorComponent extends Component { static contextTypes = { registerInjector: PropTypes.func.isRequired, @@ -20,40 +40,32 @@ const Injector = (args: { to: Object, inject: Inject }) => { componentWillMount() { this.context.registerInjector({ - injectionId: to.injectionId, + injectionId: into.injectionId, injectorId, - injector: this, - inject: () => { - if (typeof inject === `function`) { - return inject(this.props); - } - return inject; - } + injectorInstance: this, + inject: () => }); } componentWillUpdate(nextProps) { this.context.updateInjector({ - injectionId: to.injectionId, - injector: this, - inject: () => { - if (typeof inject === `function`) { - return inject(nextProps); - } - return inject; - } + injectionId: into.injectionId, + injectorId, + injectorInstance: this, + inject: () => }); } componentWillUnmount() { this.context.removeInjector({ - injectionId: to.injectionId, - injector: this + injectionId: into.injectionId, + injectorId, + injectorInstance: this }); } render() { - return (); + return null; } } diff --git a/src/index.js b/src/index.js index 6d7842a..a08d3f1 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,3 @@ -export Provider from './Provider'; +export InjectablesProvider from './InjectablesProvider'; export Injectable from './Injectable'; export Injector from './Injector'; diff --git a/test/Injectable.test.js b/test/Injectable.test.js new file mode 100644 index 0000000..67e2aef --- /dev/null +++ b/test/Injectable.test.js @@ -0,0 +1,139 @@ +/* eslint-disable react/no-multi-comp */ + +import React, { Component } from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; + +describe(`Given the Injector interface`, () => { + const Injectable = require(`../src/Injectable`).default; + + describe(`When creating an Injectable`, () => { + function assertIsValidInjectable(injectableInstance) { + const actual = typeof injectableInstance === `function` && + !!injectableInstance.contextTypes && + !!injectableInstance.contextTypes.registerInjectable && + !!injectableInstance.contextTypes.removeInjectable; + + const expected = true; + + expect(actual).to.equal(expected, `Invalid Injectable created.`); + } + + it(`It should allow use of a stateless component`, () => { + const StatelessComponent = () =>
bar
; + const InjectableBob = Injectable(StatelessComponent); + assertIsValidInjectable(InjectableBob); + }); + + it(`It should allow use of an ES6 class based component`, () => { + class ClassComponent extends Component { + state = { bob: `baz` } + render() { + return
foo
; + } + } + const InjectableBob = Injectable(ClassComponent); + assertIsValidInjectable(InjectableBob); + }); + + it(`It should allow use of a React.createClass based component`, () => { + const CreateClassComponent = + React.createClass({ // eslint-disable-line react/prefer-es6-class + state: { foo: `bar` }, + render() { + return
foo
; + } + }); + const InjectableBob = Injectable(CreateClassComponent); + assertIsValidInjectable(InjectableBob); + }); + }); + + describe(`When using an Injectable Component`, () => { + let InjectableComponentBob; + let context; + + beforeEach(() => { + InjectableComponentBob = Injectable(({ injections }) =>
{injections}
); + + context = { + registerInjectable: sinon.spy(), + removeInjectable: sinon.spy() + }; + }); + + it(`It should have an "injectionId" static set`, () => { + expect(InjectableComponentBob.injectionId) + .to.match(/^injectionId_[\d]+$/); + }); + + it(`It should not render anything when initially mounted`, () => { + const mounted = mount(, { context }); + + expect(mounted.html()).to.equal(`
`); + }); + + it(`It should call the correct context items on mount`, () => { + const mounted = mount(, { context }); + + expect(context.registerInjectable.callCount).to.equal(1); + expect(context.removeInjectable.callCount).to.equal(0); + + const { + injectionId: actualInjectionId, + injectableInstance: actualInjectableInstance, + receive: actualReceive + } = context.registerInjectable.args[0][0]; + + expect(actualInjectionId).to.equal(InjectableComponentBob.injectionId); + expect(actualInjectableInstance).to.equal(mounted.instance()); + expect(typeof actualReceive).to.equal(`function`); + }); + + it(`It should call the correct context items on unmount`, () => { + const mounted = mount(, { context }); + const actualInstance = mounted.instance(); + mounted.unmount(); + + expect(context.registerInjectable.callCount).to.equal(1); + expect(context.removeInjectable.callCount).to.equal(1); + + const { + injectionId: actualInjectionId, + injectableInstance: actualInjectableInstance + } = context.registerInjectable.args[0][0]; + + expect(actualInjectionId).to.equal(InjectableComponentBob.injectionId); + expect(actualInjectableInstance).to.equal(actualInstance); + }); + + it(`It should render consumed injections`, () => { + const mounted = mount(, { context }); + + const injectionOne =
injection 1
; + const injectionTwo =
injection 2
; + + mounted.instance().consume([ + injectionOne, + injectionTwo + ]); + + expect(mounted.state(`injections`)).to.eql([injectionOne, injectionTwo]); + expect(mounted.html()) + .to.equal(`
injection 1
injection 2
`); + }); + + it(`It should not render duplicate elements`, () => { + const mounted = mount(, { context }); + + const injectionOne =
injection 1
; + + mounted.instance().consume([injectionOne]); + mounted.instance().consume([injectionOne]); + + expect(mounted.html()) + .to.equal(`
injection 1
`); + }); + }); +}); diff --git a/test/InjectablesProvider.test.js b/test/InjectablesProvider.test.js new file mode 100644 index 0000000..f21ec67 --- /dev/null +++ b/test/InjectablesProvider.test.js @@ -0,0 +1,279 @@ +import React from 'react'; +import { expect } from 'chai'; + +describe(`Given the Injectables Provider`, () => { + let instance; + + beforeEach(() => { + const Provider = require(`../src/InjectablesProvider`).default; + instance = new Provider(); + }); + + it(`Then a newly added injectable should not receive any elements`, () => { + let receivedInjections = []; + + instance.registerInjectable({ + injectionId: `foo`, + injectableInstance: () =>
foo
, + receive: (elements) => { receivedInjections = elements; } + }); + + expect(receivedInjections).to.eql([]); + }); + + it(`Then injector registration, update, and removal should affect injectables`, () => { + // Register injectable + let receivedInjections = []; + instance.registerInjectable({ + injectionId: `foo`, + injectableInstance: () =>
foo
, + receive: (elements) => { receivedInjections = elements; } + }); + + // Register injector. + const injection =
injection
; + const injectorInstance = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () => injection + }); + expect(receivedInjections).to.eql([injection]); + + // Update injector + const newInjection =
new injection
; + instance.updateInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () => newInjection + }); + expect(receivedInjections).to.eql([newInjection]); + + // Remove injector + instance.removeInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance + }); + expect(receivedInjections).to.eql([]); + }); + + it(`Then a removed injectable should not receive any injections`, () => { + // Register injectable + const injectableInstance = () => null; + let receivedInjections = []; + instance.registerInjectable({ + injectionId: `foo`, + injectableInstance, + receive: (elements) => { receivedInjections = elements; } + }); + expect(receivedInjections).to.eql([]); + + // Remove injectable + instance.removeInjectable({ + injectionId: `foo`, + injectableInstance + }); + + // Register injector. + const injection =
injection
; + const injectorInstance = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () => injection + }); + + expect(receivedInjections).to.eql([]); + }); + + it(`Then multiple injectables should recieve existing injections`, () => { + // Register injector. + const injection =
injection
; + const injectorInstance = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () => injection + }); + + // Register injectable + const injectableInstance = () => null; + let receivedInjections = []; + instance.registerInjectable({ + injectionId: `foo`, + injectableInstance, + receive: (elements) => { receivedInjections = elements; } + }); + expect(receivedInjections).to.eql([injection]); + + // Register injectable + const injectableInstanceTwo = () => null; + let receivedInjectionsTwo = []; + instance.registerInjectable({ + injectionId: `foo`, + injectableInstance: injectableInstanceTwo, + receive: (elements) => { receivedInjectionsTwo = elements; } + }); + expect(receivedInjectionsTwo).to.eql([injection]); + }); + + it(`Then a duplicate injector instance registration should result in an error`, () => { + // Register injector. + const injection =
injection
; + const injectorInstance = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () => injection + }); + + // Re-Register injector. + const duplicateRegister = () => + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () => injection + }); + + expect(duplicateRegister).to.throw(/An Injector instance is being registered multiple times/); + }); + + it(`Then a duplicate injector id registration should result in an warning`, () => { + const prevWarn = console.warn; // eslint-disable-line no-console + const warnings = []; + console.warn = (msg) => { warnings.push(msg); }; // eslint-disable-line no-console + + // Register injector. + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance: () => null, + inject: () =>
injection
+ }); + + // Register injector. + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance: () => null, + inject: () =>
injection
+ }); + + expect(warnings.length).to.eql(1); + expect(warnings[0]).to.match(/Multiple instances of an Injector has been found/); + + console.warn = prevWarn; // eslint-disable-line no-console + }); + + it(`Then multiple unique injectors should result in multiple injection changes`, () => { + // Register injectable + const injectableInstance = () => null; + let receivedInjections = []; + instance.registerInjectable({ + injectionId: `foo`, + injectableInstance, + receive: (elements) => { receivedInjections = elements; } + }); + + // Register injector one. + const injection =
injection
; + const injectorInstance = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () => injection + }); + expect(receivedInjections).to.eql([injection]); + + // Register injector two. + const injectionTwo =
injection 2
; + const injectorInstanceTwo = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector2`, + injectorInstanceTwo, + inject: () => injectionTwo + }); + expect(receivedInjections).to.eql([injection, injectionTwo]); + + // Remove injector one. + instance.removeInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance + }); + expect(receivedInjections).to.eql([injectionTwo]); + + // Remove injector two. + instance.removeInjector({ + injectionId: `foo`, + injectorId: `injector2`, + injectorInstanceTwo + }); + expect(receivedInjections).to.eql([]); + }); + + it(`Then removing all Injectors and Injectables should clear registrations`, () => { + // Register two injectables. + const injectableInstance = () => null; + instance.registerInjectable({ + injectionId: `foo`, + injectableInstance, + receive: () => undefined + }); + const injectableInstanceTwo = () => null; + instance.registerInjectable({ + injectionId: `foo`, + injectableInstanceTwo, + receive: () => undefined + }); + // Register two injector. + const injectorInstance = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector1`, + injectorInstance, + inject: () =>
injection
+ }); + const injectorInstanceTwo = () => null; + instance.registerInjector({ + injectionId: `foo`, + injectorId: `injector2`, + injectorInstanceTwo, + inject: () =>
injection
+ }); + + expect(instance.registrations.length).to.equal(1); + + // Remove injectables. + instance.removeInjectable({ + injectionId: `foo`, + injectableInstance + }); + instance.removeInjectable({ + injectionId: `foo`, + injectableInstanceTwo + }); + // Remove injectors. + instance.removeInjector({ + injectionId: `foo`, + injectorId: `injector2`, + injectorInstance + }); + instance.removeInjector({ + injectionId: `foo`, + injectorId: `injector2`, + injectorInstanceTwo + }); + + expect(instance.registrations.length).to.equal(0); + }); +}); diff --git a/test/Injector.test.js b/test/Injector.test.js new file mode 100644 index 0000000..7eecebd --- /dev/null +++ b/test/Injector.test.js @@ -0,0 +1,268 @@ +/* eslint-disable react/no-multi-comp */ + +import React, { Component } from 'react'; +import { expect } from 'chai'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; + +describe(`Given the Injector interface`, () => { + const Injector = require(`../src/Injector`).default; + const Injectable = require(`../src/Injectable`).default; + + describe(`When creating an Injector`, () => { + let ValidInjectable; + + function assertIsValidInjector(injectorInstance) { + const actual = typeof injectorInstance === `function` && + !!injectorInstance.contextTypes && + !!injectorInstance.contextTypes.registerInjector && + !!injectorInstance.contextTypes.updateInjector && + !!injectorInstance.contextTypes.removeInjector; + + const expected = true; + + expect(actual).to.equal(expected, `Invalid Injector created.`); + } + + beforeEach(() => { + ValidInjectable = Injectable(() =>
foo
); + }); + + it(`It should allow a stateless component for the injection`, () => { + const StatelessComponentInjection = () =>
bar
; + const InjectorBob = Injector({ + into: ValidInjectable + })(StatelessComponentInjection); + assertIsValidInjector(InjectorBob); + }); + + it(`It should allow an ES6 class based component for the injection`, () => { + class ClassComponentInjection extends Component { + state = { bob: `baz` } + render() { + return
foo
; + } + } + const InjectorBob = Injector({ + into: ValidInjectable + })(ClassComponentInjection); + assertIsValidInjector(InjectorBob); + }); + + it(`It should allow an React.createClass based component for the injection`, () => { + const CreateClassComponentInjection = + React.createClass({ // eslint-disable-line react/prefer-es6-class + state: { foo: `bar` }, + render() { + return
foo
; + } + }); + const InjectorBob = Injector({ + into: ValidInjectable + })(CreateClassComponentInjection); + assertIsValidInjector(InjectorBob); + }); + }); + + describe(`When using an Injector Component`, () => { + let InjectorComponentBob; + let InjectionComponent; + let context; + let injectionId; + + beforeEach(() => { + const InjectableComponent = Injectable(() =>
foo
); + injectionId = InjectableComponent.injectionId; + + InjectionComponent = () => +
injection
; + + InjectorComponentBob = Injector({ + into: InjectableComponent + })(InjectionComponent); + + context = { + registerInjector: sinon.spy(), + removeInjector: sinon.spy(), + updateInjector: sinon.spy() + }; + }); + + it(`It should not render anything when mounted`, () => { + const mounted = mount(, { context }); + + expect(mounted.html()).to.equal(null); + }); + + it(`It should call the correct context items on mount`, () => { + const mounted = mount(, { context }); + + expect(context.registerInjector.callCount).to.equal(1); + expect(context.updateInjector.callCount).to.equal(0); + expect(context.removeInjector.callCount).to.equal(0); + + const { + injectionId: actualInjectionId, + injectorId: actualInjectorId, + injectorInstance: actualInjectorInstance, + inject: actualInject + } = context.registerInjector.args[0][0]; + + expect(actualInjectionId).to.equal(injectionId); + expect(actualInjectorId).to.match(/^injector_[\d]+$/); + expect(actualInjectorInstance).to.equal(mounted.instance()); + expect(typeof actualInject).to.equal(`function`); + expect( + mount(
{actualInject()}
) + .find(InjectionComponent) + .length + ).to.equal(1); + }); + + it(`It should call the correct context items on updates`, () => { + const mounted = mount(, { context }); + mounted.update(); + + expect(context.registerInjector.callCount).to.equal(1); + expect(context.updateInjector.callCount).to.equal(1); + expect(context.removeInjector.callCount).to.equal(0); + + const { + injectionId: actualInjectionId, + injectorId: actualInjectorId, + injectorInstance: actualInjectorInstance, + inject: actualInject + } = context.updateInjector.args[0][0]; + + expect(actualInjectionId).to.equal(injectionId); + expect(actualInjectorId).to.match(/^injector_[\d]+$/); + expect(actualInjectorInstance).to.equal(mounted.instance()); + expect(typeof actualInject).to.equal(`function`); + expect( + mount(
{actualInject()}
) + .find(InjectionComponent) + .length + ).to.equal(1); + }); + + it(`It should call the correct context items on unmount`, () => { + const mounted = mount(, { context }); + const actualInstance = mounted.instance(); + mounted.unmount(); + + expect(context.registerInjector.callCount).to.equal(1); + expect(context.updateInjector.callCount).to.equal(0); + expect(context.removeInjector.callCount).to.equal(1); + + const { + injectionId: actualInjectionId, + injectorId: actualInjectorId, + injectorInstance: actualInjectorInstance + } = context.removeInjector.args[0][0]; + + expect(actualInjectionId).to.equal(injectionId); + expect(actualInjectorId).to.match(/^injector_[\d]+$/); + expect(actualInjectorInstance).to.equal(actualInstance); + }); + }); + + describe(`When trying to create an Injector with an invalid "injection"`, () => { + it(`Then an error should be thrown`, () => { + const invalidInjections = [ + 1, + `2`, + true, +
foo
, + new Date(), + {}, + [] + ]; + + const validToArg = Injectable(() =>
foo
); + + invalidInjections.forEach(invalidInjection => { + const actual = () => Injector({ + into: validToArg + })(invalidInjection); + + const expected = /Invalid injection value/; + + expect(actual).to.throw(expected); + }); + }); + }); + + describe(`When trying to create an Injector with an invalid "to" argument`, () => { + it(`Then an error should be thrown`, () => { + // Invalid type + const InvalidInjectable1 = {}; + + // Invalid interface + const InvalidInjectable2 = () => undefined; + + // Invalid interface + class InvalidInjectable3 extends Component { + static injectionId = `foo`; + + render() { + return
Foo
; + } + } + + // Invalid interface + class InvalidInjectable4 extends Component { + static injectionId = `foo`; + + static contextTypes = { } + + render() { + return
Foo
; + } + } + + // Invalid interface + class InvalidInjectable5 extends Component { + static injectionId = `foo`; + + static contextTypes = { + registerInjectable: () => undefined + } + + render() { + return
Foo
; + } + } + + // Invalid interface + class InvalidInjectable6 extends Component { + static injectionId = `foo`; + + static contextTypes = { + removeInjectable: () => undefined + } + + render() { + return
Foo
; + } + } + + const invalidCases = []; + invalidCases.push(InvalidInjectable1); + invalidCases.push(InvalidInjectable2); + invalidCases.push(InvalidInjectable3); + invalidCases.push(InvalidInjectable4); + invalidCases.push(InvalidInjectable5); + invalidCases.push(InvalidInjectable6); + + invalidCases.forEach(invalidInjectable => { + const actual = () => Injector({ + into: invalidInjectable + })(() =>
bar
); + + const expected = /Invalid Injectable target/; + + expect(actual).to.throw(expected); + }); + }); + }); +}); diff --git a/test/Provider.test.js b/test/Provider.test.js deleted file mode 100644 index ee7e530..0000000 --- a/test/Provider.test.js +++ /dev/null @@ -1,178 +0,0 @@ -import React from 'react'; -import { expect } from 'chai'; - -describe(`Given the Injectables Provider`, () => { - let instance; - let element1; - let element2; - let consumer1; - let consumer2; - let consumed1; - let consumed2; - let producer1; - let producer2; - let producer3; - let inject1; - let inject2; - - before(() => { - const Provider = require(`../src/Provider`).default; - instance = new Provider(); - element1 = (
foo
); - element2 = (
foo
); - consumed1 = []; - consumed2 = []; - consumer1 = { consume: (elements) => { consumed1 = elements; } }; - consumer2 = { consume: (elements) => { consumed2 = elements; } }; - producer1 = {}; - producer2 = {}; - inject1 = () => element1; - inject2 = () => element2; - }); - - it(`Then a newly added consumer should initially receive now elements`, () => { - instance.registerInjectable({ - injectionId: `foo`, - injectable: consumer1 - }); - - const expected = []; - const actual = consumed1; - expect(actual).to.eql(expected); - }); - - it(`Then a produced element should be consumed`, () => { - instance.registerInjector({ - injectionId: `foo`, - injectorId: `injector1`, - injector: producer1, - inject: inject1 - }); - - const expected = [element1]; - const actual = consumed1; - expect(actual).to.eql(expected); - }); - - it(`Then a removed producer should update the consumer`, () => { - instance.removeInjector({ - injectionId: `foo`, - injector: producer1 - }); - - const expected = []; - const actual = consumed1; - expect(actual).to.eql(expected); - }); - - it(`Then a removed consumer should not recieve any more produced elements`, () => { - instance.removeInjectable({ - injectionId: `foo`, - injectable: consumer1 - }); - instance.registerInjector({ - injectionId: `foo`, - injectorId: `injector1`, - injector: producer1, - inject: inject1 - }); - - const expected = []; - const actual = consumed1; - expect(actual).to.eql(expected); - }); - - it(`Then new consumers should both recieve elements that were already produced`, () => { - instance.registerInjectable({ - injectionId: `foo`, - injectable: consumer1 - }); - instance.registerInjectable({ - injectionId: `foo`, - injectable: consumer2 - }); - - const expected = [element1]; - const actual = consumed1; - expect(actual).to.eql(expected); - - const actual2 = consumed2; - expect(actual2).to.eql(expected); - }); - - it(`Then a duplicate producer should result in no change to element consumption`, () => { - instance.registerInjector({ - injectionId: `foo`, - injectorId: `injector1`, - injector: producer1, - inject: inject1 - }); - - const expected = [element1]; - const actual = consumed1; - expect(actual).to.eql(expected); - - const actual2 = consumed2; - expect(actual2).to.eql(expected); - }); - - it(`Then a duplicate producer registration should result in no consumption change`, () => { - instance.registerInjector({ - injectionId: `foo`, - injectorId: `injector1`, - injector: producer1, - inject: inject1 - }); - - const expected = [element1]; - const actual = consumed1; - expect(actual).to.eql(expected); - - const actual2 = consumed2; - expect(actual2).to.eql(expected); - }); - - it(`Then a new producer with an new element should result in 2 element consumption`, () => { - instance.registerInjector({ - injectionId: `foo`, - injectorId: `injector2`, - injector: producer2, - inject: inject2 - }); - - const expected = [element1, element2]; - const actual = consumed1; - expect(actual).to.eql(expected); - - const actual2 = consumed2; - expect(actual2).to.eql(expected); - }); - - it(`Then removing producer 2 should result in a consumption change`, () => { - instance.removeInjector({ - injectionId: `foo`, - injector: producer2 - }); - - const expected = [element1]; - const actual = consumed1; - expect(actual).to.eql(expected); - - const actual2 = consumed2; - expect(actual2).to.eql(expected); - }); - - it(`Then removing the all producers should result in all consumptions being zeroed`, () => { - instance.removeInjector({ - injectionId: `foo`, - injector: producer1 - }); - - const expected = []; - const actual = consumed1; - expect(actual).to.eql(expected); - - const actual2 = consumed2; - expect(actual2).to.eql(expected); - }); -}); diff --git a/test/integration.test.js b/test/integration.test.js index 2cf4872..fe9083d 100644 --- a/test/integration.test.js +++ b/test/integration.test.js @@ -1,36 +1,37 @@ import React, { PropTypes } from 'react'; import { describeWithDOM } from './jsdom'; import { expect } from 'chai'; -import $ from 'teaspoon'; +import { mount } from 'enzyme'; describeWithDOM(`Given an Injectables configuration`, () => { - const { Provider, Injectable, Injector } = require(`../src/index.js`); + const { InjectablesProvider, Injectable, Injector } = require(`../src/index.js`); let InjectableHeader; let Layout; let HeaderInjectingSectionOne; let HeaderInjectingSectionTwo; + let HeaderInjection; let render; before(() => { - render = elements => $( - + render = elements => mount( + {elements} - - ).render(); + + ); - const Header = ({ injected }) => ( + const Header = ({ injections }) => ( ); Header.propTypes = { - injected: PropTypes.arrayOf(PropTypes.element).isRequired + injections: PropTypes.arrayOf(PropTypes.element).isRequired }; InjectableHeader = Injectable(Header); - Layout = ({ children }) => ( + Layout = ({ children }) => ( // eslint-disable-line react/prop-types
@@ -42,36 +43,17 @@ describeWithDOM(`Given an Injectables configuration`, () => { children: PropTypes.any }; - const sectionOnePropTypes = { - message: PropTypes.string - }; - - const SectionOne = () => ( -
-

Section One

-
+ HeaderInjection = props => ( +
{props.message || `injection`}.
// eslint-disable-line ); - SectionOne.propTypes = sectionOnePropTypes; - - const SectionOneHeaderInjection = props => -
Section One, {props.message}.
; - SectionOneHeaderInjection.propTypes = sectionOnePropTypes; HeaderInjectingSectionOne = Injector({ - to: InjectableHeader, - inject: SectionOneHeaderInjection - })(SectionOne); - - const SectionTwo = () => ( -
-

Section Two

-
- ); + into: InjectableHeader + })(HeaderInjection); HeaderInjectingSectionTwo = Injector({ - to: InjectableHeader, - inject:
Section Two Header Injection.
- })(SectionTwo); + into: InjectableHeader + })(HeaderInjection); }); describe(`When the injector is rendered as a child of the injectable`, () => { @@ -87,9 +69,10 @@ describeWithDOM(`Given an Injectables configuration`, () => { it(`Then the injected content should have been rendered in the header`, () => { const expected = 1; + const actual = rendered - .find($.s`${InjectableHeader}`) - .find(`div[id=Injection]`) + .find(InjectableHeader) + .find(HeaderInjection) .length; expect(actual).to.equal(expected, `The injected content was not found.`); @@ -111,8 +94,8 @@ describeWithDOM(`Given an Injectables configuration`, () => { it(`Then the injected content should have been rendered in the header`, () => { const expected = 1; const actual = rendered - .find($.s`${InjectableHeader}`) - .find(`div[id=Injection]`) + .find(InjectableHeader) + .find(HeaderInjection) .length; expect(actual).to.equal(expected, `The injected content was not found.`); @@ -136,8 +119,8 @@ describeWithDOM(`Given an Injectables configuration`, () => { it(`Then the injected content should have been rendered in the header`, () => { const expected = 1; const actual = rendered - .find($.s`${InjectableHeader}`) - .find(`div[id=Injection]`) + .find(InjectableHeader) + .find(HeaderInjection) .length; expect(actual).to.equal(expected, `The injected content was not found.`); @@ -163,8 +146,8 @@ describeWithDOM(`Given an Injectables configuration`, () => { it(`Then the injected content should have been rendered in the header`, () => { const expected = 2; const actual = rendered - .find($.s`${InjectableHeader}`) - .find(`div[id=Injection]`) + .find(InjectableHeader) + .find(HeaderInjection) .length; expect(actual).to.equal(expected, `The injected content was not found.`); @@ -174,9 +157,8 @@ describeWithDOM(`Given an Injectables configuration`, () => { describe(`When rendering a null/undefined from an Injector`, () => { it(`Then nothing should be rendered`, () => { const NullInjector = Injector({ - to: InjectableHeader, - inject: () => null - })(() =>