├── .all-contributorsrc ├── .babelrc ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── commitlint.yml │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── commitlint.config.js ├── google-optimize-test.png ├── karma.conf.js ├── package.json ├── src ├── Experiment.js ├── OptimizeContext.js ├── Variant.js ├── index.d.ts └── index.js ├── test ├── setup.js ├── specs │ ├── experiment.spec.js │ └── variant.spec.js ├── test.node.js └── tests.bundle.js ├── webpack.config.js └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "projectName": "react-optimize", 3 | "projectOwner": "hudovisk", 4 | "repoType": "github", 5 | "repoHost": "https://github.com", 6 | "files": [ 7 | "README.md" 8 | ], 9 | "imageSize": 100, 10 | "commit": true, 11 | "commitConvention": "angular", 12 | "contributors": [ 13 | { 14 | "login": "hudovisk", 15 | "name": "Hudo Assenco", 16 | "avatar_url": "https://avatars2.githubusercontent.com/u/5161722?v=4", 17 | "profile": "https://github.com/hudovisk", 18 | "contributions": [ 19 | "code", 20 | "doc" 21 | ] 22 | }, 23 | { 24 | "login": "dobesv", 25 | "name": "Dobes Vandermeer", 26 | "avatar_url": "https://avatars2.githubusercontent.com/u/327833?v=4", 27 | "profile": "http://dobesv.com", 28 | "contributions": [ 29 | "code", 30 | "doc" 31 | ] 32 | }, 33 | { 34 | "login": "tlaak", 35 | "name": "Timo Laak", 36 | "avatar_url": "https://avatars0.githubusercontent.com/u/1674055?v=4", 37 | "profile": "https://github.com/tlaak", 38 | "contributions": [ 39 | "review" 40 | ] 41 | }, 42 | { 43 | "login": "kelvinmaues", 44 | "name": "Kelvin Maues", 45 | "avatar_url": "https://avatars0.githubusercontent.com/u/11196828?v=4", 46 | "profile": "https://kelvinmaues.github.io/", 47 | "contributions": [ 48 | "code", 49 | "doc" 50 | ] 51 | } 52 | ], 53 | "contributorsPerLine": 7, 54 | "skipCi": true 55 | } 56 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react"], 3 | "plugins": ["@babel/plugin-proposal-class-properties"] 4 | } 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { "react": { "version": "detect" } }, 3 | "parser": "babel-eslint", 4 | "extends": ["plugin:react/recommended", "plugin:prettier/recommended"] 5 | } 6 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "08:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/commitlint.yml: -------------------------------------------------------------------------------- 1 | name: Commitlint 2 | on: [pull_request] 3 | 4 | jobs: 5 | lint: 6 | runs-on: ubuntu-latest 7 | env: 8 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 9 | steps: 10 | - uses: actions/checkout@v1 11 | - uses: wagoid/commitlint-github-action@v1 12 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | env: 9 | CI: true 10 | 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - name: Use Node.js 18.x 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 18.x 18 | 19 | - name: Get yarn cache 20 | id: yarn-cache 21 | run: echo "::set-output name=dir::$(yarn cache dir)" 22 | 23 | - uses: actions/cache@v1 24 | with: 25 | path: ${{ steps.yarn-cache.outputs.dir }} 26 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 27 | restore-keys: | 28 | ${{ runner.os }}-yarn- 29 | 30 | - name: yarn 31 | run: yarn 32 | 33 | - name: build 34 | run: yarn build 35 | env: 36 | NODE_OPTIONS: --openssl-legacy-provider 37 | 38 | - name: test 39 | run: yarn test 40 | env: 41 | NODE_OPTIONS: --openssl-legacy-provider 42 | 43 | - name: release 44 | if: github.ref == 'refs/heads/master' 45 | run: npx semantic-release 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib 2 | node_modules/ 3 | coverage/ 4 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Hudo Assenco 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-optimize 2 | 3 | 4 | [![All Contributors](https://img.shields.io/badge/all_contributors-4-orange.svg?style=flat-square)](#contributors-) 5 | 6 | 7 | [![Build Status](https://github.com/hudovisk/react-optimize/workflows/Build/badge.svg)](https://github.com/hudovisk/react-optimize/actions) [![Greenkeeper badge](https://badges.greenkeeper.io/hudovisk/react-optimize.svg)](https://greenkeeper.io/) 8 | 9 | Integration with Google Optimize. 10 | 11 | Docs: 12 | 13 | - Optimize Deploy with GTAG: https://support.google.com/optimize/answer/7513085 14 | - Optimize JS API: https://support.google.com/optimize/answer/9059383 15 | 16 | ## Installation 17 | 18 | ``` 19 | yarn add react-optimize 20 | ``` 21 | 22 | You first need to add the gtag snippet with the optimize container id in it. If you are using [create-react-app](https://github.com/facebook/create-react-app) 23 | you can add the following to `public/index.html` 24 | 25 | ```html 26 | 27 | 34 | ``` 35 | 36 | and define them in your `.env` 37 | 38 | ``` 39 | REACT_APP_GA_ID=UA-xyz 40 | REACT_APP_OPTIMIZE_ID=GTM-abc 41 | ``` 42 | 43 | ## How to use 44 | 45 | #### A/B Test 46 | If the experience is a **A/B testing** you can use the lib like the following: 47 | 48 | ```jsx 49 | import React from 'react'; 50 | import { Experiment, Variant } from "react-optimize"; 51 | 52 | class App extends React.Component { 53 | render() { 54 | return( 55 | 56 | 57 | Original 58 | 59 | 60 | Variant 1 61 | 62 | 63 | Variant 2 64 | 65 | 66 | ) 67 | } 68 | } 69 | ``` 70 | 71 | #### Multivariate Test 72 | If the experience is a **multivariate testing** to test variants with two or more different sections. You can use the lib like the following applying the props **asMtvExperiment (confirm that is multivariate)** and the **indexSectionPosition** on google optimize like the image below: 73 | 74 | ![google optimize multivariate test](./google-optimize-test.png) 75 | 76 | ```jsx 77 | import React from 'react'; 78 | import { Experiment, Variant } from "react-optimize"; 79 | 80 | class App extends React.Component { 81 | render() { 82 | return( 83 | <> 84 | 89 | 90 | Original 91 | 92 | 93 | Variant 1 94 | 95 | 96 | 97 | 102 | 103 | Original 104 | 105 | 106 | Variant 1 107 | 108 | 109 | Variant 2 110 | 111 | 112 | 113 | 118 | 119 | Original 120 | 121 | 122 | Variant 1 123 | 124 | 125 | 126 | ) 127 | } 128 | } 129 | ``` 130 | 131 | ## Contributors ✨ 132 | 133 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 |

Hudo Assenco

💻 📖

Dobes Vandermeer

💻 📖

Timo Laak

👀

Kelvin Maues

💻 📖
146 | 147 | 148 | 149 | 150 | 151 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! 152 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = {extends: ['@commitlint/config-conventional']} 2 | -------------------------------------------------------------------------------- /google-optimize-test.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hudovisk/react-optimize/ddaff6d92b6f2b85c7f92f82b48c9c070b9a1918/google-optimize-test.png -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Apr 16 2019 23:13:15 GMT-0300 (Brasilia Standard Time) 3 | var webpackConfig = require("./webpack.config"); 4 | 5 | module.exports = function(config) { 6 | config.set({ 7 | // base path that will be used to resolve all patterns (eg. files, exclude) 8 | basePath: "", 9 | 10 | // frameworks to use 11 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 12 | frameworks: ["mocha"], 13 | 14 | // list of files / patterns to load in the browser 15 | files: ["test/tests.bundle.js"], 16 | 17 | // list of files / patterns to exclude 18 | exclude: [], 19 | 20 | // preprocess matching files before serving them to the browser 21 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 22 | preprocessors: { 23 | "test/tests.bundle.js": ["webpack"] 24 | }, 25 | webpack: { 26 | entry: "./test/tests.bundle.js", 27 | devtool: "cheap-module-source-map", 28 | mode: "development", 29 | module: webpackConfig.module, 30 | plugins: webpackConfig.plugins, 31 | resolve: webpackConfig.resolve 32 | }, 33 | 34 | // test results reporter to use 35 | // possible values: 'dots', 'progress' 36 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 37 | reporters: ["mocha"], 38 | 39 | // web server port 40 | port: 9876, 41 | 42 | // enable / disable colors in the output (reporters and logs) 43 | colors: true, 44 | 45 | // level of logging 46 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 47 | logLevel: config.LOG_WARN, 48 | 49 | // enable / disable watching file and executing tests whenever any file changes 50 | autoWatch: true, 51 | 52 | // start these browsers 53 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 54 | browsers: ["ChromeHeadless"], 55 | 56 | // Continuous Integration mode 57 | // if true, Karma captures browsers, runs the tests and exits 58 | singleRun: process.env.CI === "true", 59 | 60 | // Concurrency level 61 | // how many browser should be started simultaneous 62 | concurrency: Infinity 63 | }); 64 | }; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-optimize", 3 | "version": "1.0.2", 4 | "main": "lib/react-optimize.js", 5 | "types": "lib/react-optimize.d.ts", 6 | "repository": "hudovisk/react-optimize", 7 | "license": "MIT", 8 | "files": [ 9 | "/lib" 10 | ], 11 | "scripts": { 12 | "lint:fix": "yarn lint --fix", 13 | "lint": "eslint src/ test/", 14 | "test:node": "mocha --require @babel/register test/test.node.js", 15 | "test:browser": "cross-env NODE_ENV=test karma start", 16 | "test": "yarn run test:node && yarn run test:browser && yarn run lint", 17 | "build": "rm -Rf lib && webpack", 18 | "watch": "webpack --watch", 19 | "prepublishOnly": "yarn run build" 20 | }, 21 | "husky": { 22 | "hooks": { 23 | "commit-msg": "commitlint -E HUSKY_GIT_PARAMS", 24 | "pre-commit": "yarn run lint" 25 | } 26 | }, 27 | "devDependencies": { 28 | "@babel/core": "^7.20.12", 29 | "@babel/plugin-proposal-class-properties": "^7.14.5", 30 | "@babel/polyfill": "^7.12.1", 31 | "@babel/preset-env": "^7.20.2", 32 | "@babel/preset-react": "^7.18.6", 33 | "@babel/register": "^7.14.5", 34 | "@commitlint/cli": "^12.1.4", 35 | "@commitlint/config-conventional": "^12.1.4", 36 | "all-contributors-cli": "^6.20.0", 37 | "babel-eslint": "^10.1.0", 38 | "babel-loader": "^8.3.0", 39 | "chai": "^4.3.4", 40 | "copy-webpack-plugin": "^6.4.1", 41 | "cross-env": "^7.0.3", 42 | "enzyme": "^3.11.0", 43 | "enzyme-adapter-react-16": "^1.15.6", 44 | "eslint": "^7.32.0", 45 | "eslint-config-prettier": "^8.3.0", 46 | "eslint-plugin-prettier": "^3.4.0", 47 | "eslint-plugin-react": "^7.32.2", 48 | "husky": "^5.2.0", 49 | "karma": "^6.3.16", 50 | "karma-chrome-launcher": "^3.1.1", 51 | "karma-coverage": "^2.2.0", 52 | "karma-mocha": "^2.0.1", 53 | "karma-mocha-reporter": "^2.2.5", 54 | "karma-sourcemap-loader": "^0.3.8", 55 | "karma-webpack": "^4.0.2", 56 | "mocha": "^10.2.0", 57 | "prettier": "^2.8.3", 58 | "react": "^16.14.0", 59 | "react-dom": "^16.14.0", 60 | "semantic-release": "^20.1.0", 61 | "sinon": "^10.0.1", 62 | "webpack": "^4.46.0", 63 | "webpack-cli": "^4.10.0" 64 | }, 65 | "peerDependencies": { 66 | "prop-types": "^15.0.0", 67 | "react": "^16.3.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Experiment.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import OptimizeContext from "./OptimizeContext"; 4 | 5 | class Experiment extends React.Component { 6 | isUnmounted = false; 7 | state = { 8 | variant: null, 9 | }; 10 | 11 | updateVariantTimeout = null; 12 | 13 | updateVariant = (value) => { 14 | clearTimeout(this.updateVariantTimeout); 15 | // if experiment not active, render original 16 | const newVariant = value === undefined || value === null ? "0" : value; 17 | if (newVariant !== this.state.variant) { 18 | this.setState({ 19 | variant: newVariant, 20 | }); 21 | } 22 | }; 23 | 24 | applyMtvExperiment = (value) => { 25 | const sections = value.split("-"); 26 | const variant = sections[this.props.indexSectionPosition]; 27 | this.updateVariant(variant); 28 | }; 29 | 30 | updateVariantFromGlobalState = () => { 31 | const googleOptimizeExperimentValue = 32 | typeof window !== "undefined" && window.google_optimize 33 | ? window.google_optimize.get(this.props.id) 34 | : null; 35 | const isAMtvExperiment = 36 | this.props.asMtvExperiment && googleOptimizeExperimentValue; 37 | 38 | if (isAMtvExperiment) { 39 | this.applyMtvExperiment(googleOptimizeExperimentValue); 40 | } else { 41 | this.updateVariant(googleOptimizeExperimentValue); 42 | } 43 | }; 44 | 45 | setupOptimizeCallback = () => { 46 | this.updateVariantTimeout = setTimeout( 47 | this.updateVariantFromGlobalState, 48 | this.props.timeout 49 | ); 50 | const oldHideEnd = window.dataLayer.hide.end; 51 | window.dataLayer.hide.end = () => { 52 | if (!this.isUnmounted) { 53 | this.updateVariantFromGlobalState(); 54 | } 55 | oldHideEnd && oldHideEnd(); 56 | }; 57 | 58 | window.gtag && 59 | window.gtag("event", "optimize.callback", { 60 | name: this.props.id, 61 | callback: this.updateVariant, 62 | }); 63 | }; 64 | 65 | componentDidMount() { 66 | if (!this.props.id) { 67 | throw new Error("Please specify the experiment id"); 68 | } 69 | 70 | // Delayed init 71 | if (typeof window !== "undefined" && !window.google_optimize) { 72 | if (!window.dataLayer) { 73 | window.dataLayer = []; 74 | } 75 | if (!window.dataLayer.hide) { 76 | window.dataLayer.hide = { start: Date.now() }; 77 | } 78 | 79 | this.setupOptimizeCallback(); 80 | } else { 81 | // Google Optimize already loaded, or we're doing server-side rendering 82 | this.updateVariantFromGlobalState(); 83 | } 84 | } 85 | 86 | componentWillUnmount() { 87 | clearTimeout(this.updateVariantTimeout); 88 | this.isUnmounted = true; 89 | typeof window !== "undefined" && 90 | window.gtag && 91 | window.gtag("event", "optimize.callback", { 92 | name: this.props.id, 93 | callback: this.updateVariant, 94 | remove: true, 95 | }); 96 | } 97 | 98 | render() { 99 | return ( 100 | 101 | {this.state.variant === null ? this.props.loader : this.props.children} 102 | 103 | ); 104 | } 105 | } 106 | 107 | Experiment.propTypes = { 108 | id: PropTypes.string.isRequired, 109 | loader: PropTypes.node, 110 | timeout: PropTypes.number, 111 | children: PropTypes.node, 112 | asMtvExperiment: PropTypes.bool, 113 | indexSectionPosition: PropTypes.string, 114 | }; 115 | 116 | Experiment.defaultProps = { 117 | loader: null, 118 | timeout: 3000, 119 | asMtvExperiment: false, 120 | }; 121 | 122 | export default Experiment; 123 | -------------------------------------------------------------------------------- /src/OptimizeContext.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const OptimizeContext = React.createContext(); 4 | 5 | export default OptimizeContext; 6 | -------------------------------------------------------------------------------- /src/Variant.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import PropTypes from "prop-types"; 3 | import OptimizeContext from "./OptimizeContext"; 4 | 5 | class Variant extends React.Component { 6 | render() { 7 | return ( 8 | 9 | {(value) => (value === this.props.id ? this.props.children : null)} 10 | 11 | ); 12 | } 13 | } 14 | 15 | Variant.propTypes = { 16 | id: PropTypes.string.isRequired, 17 | children: PropTypes.node, 18 | }; 19 | 20 | export default Variant; 21 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-optimize" { 2 | import { ComponentType, ReactNode } from "react"; 3 | interface ExperimentProps { 4 | children: ReactNode; 5 | id: string; 6 | loader?: ReactNode; 7 | timeout?: number; 8 | asMtvExperiment?: boolean; 9 | indexSectionPosition?: string | number; 10 | } 11 | 12 | const Experiment: ComponentType; 13 | 14 | interface VariantProps { 15 | children: ReactNode; 16 | id: string; 17 | } 18 | const Variant: ComponentType; 19 | } 20 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export { default as Experiment } from "./Experiment"; 2 | export { default as Variant } from "./Variant"; 3 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import Enzyme from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | import chai from "chai"; 4 | 5 | Enzyme.configure({ adapter: new Adapter() }); 6 | 7 | // ---------------------------------------- 8 | // Chai 9 | // ---------------------------------------- 10 | global.expect = chai.expect; 11 | -------------------------------------------------------------------------------- /test/specs/experiment.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { shallow } from "enzyme"; 3 | import sinon from "sinon"; 4 | import { Experiment } from "../../src"; 5 | 6 | describe("experiment", () => { 7 | afterEach(() => { 8 | delete window.google_optimize; 9 | delete window.dataLayer; 10 | }); 11 | 12 | it("should require experiment id", () => { 13 | expect(() => shallow()).to.throw(); 14 | }); 15 | 16 | describe("on optimize already loaded", () => { 17 | it("should render original variant on experiment not active", () => { 18 | window.google_optimize = { get: sinon.stub().returns(null) }; 19 | 20 | const wrapper = shallow(); 21 | 22 | expect(wrapper.state("variant")).to.be.equal("0"); 23 | }); 24 | 25 | it("should get variant", () => { 26 | window.google_optimize = { get: sinon.stub().returns("2") }; 27 | 28 | const wrapper = shallow(); 29 | 30 | expect(window.google_optimize.get.calledWith("abc")).to.be.true; 31 | expect(wrapper.state("variant")).to.be.equal("2"); 32 | 33 | delete window.google_optimize; 34 | }); 35 | 36 | it("should get multivariante test variants", () => { 37 | window.google_optimize = { get: sinon.stub().returns("1-2-1") }; 38 | 39 | const wrapper = shallow( 40 | 41 | ); 42 | 43 | expect(window.google_optimize.get.calledWith("abc")).to.be.true; 44 | expect(wrapper.state("variant")).to.be.equal("1"); 45 | 46 | delete window.google_optimize; 47 | }); 48 | }); 49 | 50 | describe("on optimize not loaded yet", () => { 51 | it("should render loader", () => { 52 | delete window.dataLayer; 53 | const Loader = () =>
loader
; 54 | const wrapper = shallow(} />); 55 | 56 | expect(wrapper.find(Loader)).to.have.lengthOf(1); 57 | }); 58 | 59 | it("should update variant after optimize is loaded", () => { 60 | delete window.dataLayer; 61 | const wrapper = shallow(); 62 | 63 | expect(wrapper.state("variant")).to.be.equal(null); 64 | 65 | // Load google optimize 66 | window.google_optimize = { get: sinon.stub().returns("2") }; 67 | window.dataLayer.hide.end(); 68 | 69 | expect(wrapper.state("variant")).to.be.equal("2"); 70 | }); 71 | 72 | it("should not update variant after optimize is loaded if component was unmounted", () => { 73 | delete window.dataLayer; 74 | const wrapper = shallow(); 75 | 76 | expect(wrapper.state("variant")).to.be.equal(null); 77 | 78 | wrapper.unmount(); 79 | // Load google optimize 80 | window.google_optimize = { get: sinon.stub().returns("2") }; 81 | 82 | // If component state is updated while component is unmounted, this call will crash 83 | window.dataLayer.hide.end(); 84 | }); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /test/specs/variant.spec.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { mount } from "enzyme"; 3 | import { Variant } from "../../src"; 4 | import OptimizeContext from "../../src/OptimizeContext"; 5 | 6 | describe("variant", () => { 7 | it("should render on correct id", () => { 8 | const Variant1 = () =>
variant1
; 9 | const Variant2 = () =>
variant2
; 10 | 11 | const wrapper = mount( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | 19 | , 20 | { 21 | wrappingComponent: OptimizeContext.Provider, 22 | wrappingComponentProps: { value: "1" }, 23 | } 24 | ); 25 | 26 | expect(wrapper.find(Variant1)).to.have.lengthOf(1); 27 | expect(wrapper.find(Variant2)).to.have.lengthOf(0); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/test.node.js: -------------------------------------------------------------------------------- 1 | import "./setup"; 2 | 3 | import React from "react"; 4 | import ReactDOM from "react-dom/server"; 5 | import { Variant, Experiment } from "../lib/react-optimize"; 6 | 7 | describe("ssr", () => { 8 | it("should render without errors", () => { 9 | const Loader = () =>
loader
; 10 | const string = ReactDOM.renderToString( 11 | }> 12 | Original 13 | 14 | ); 15 | 16 | expect(string).to.not.be.empty; 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/tests.bundle.js: -------------------------------------------------------------------------------- 1 | import "./setup"; 2 | 3 | // require all modules ending in ".spec.js" from the 4 | // js directory and all subdirectories 5 | const testsContext = require.context("./specs/", true, /\.spec\.js$/); 6 | 7 | // only re-run changed tests, or all if none changed 8 | // https://www.npmjs.com/package/karma-webpack-with-fast-source-maps 9 | const __karmaWebpackManifest__ = []; 10 | let runnable = testsContext 11 | .keys() 12 | .filter((path) => __karmaWebpackManifest__.indexOf(path) >= 0); 13 | 14 | if (!runnable.length) runnable = testsContext.keys(); 15 | 16 | runnable.forEach(testsContext); 17 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | const CopyPlugin = require("copy-webpack-plugin"); 3 | 4 | module.exports = { 5 | mode: process.env.NODE_ENV === "production" ? "production" : "development", 6 | entry: path.resolve(__dirname, "src/index.js"), 7 | output: { 8 | path: path.resolve(__dirname, 'lib'), 9 | filename: "react-optimize.js", 10 | library: "react-optimize", 11 | libraryTarget: 'umd', 12 | umdNamedDefine: true, 13 | globalObject: 'this' 14 | }, 15 | externals: { 16 | react: 'react', 17 | "prop-types": "prop-types" 18 | }, 19 | module: { 20 | rules: [ 21 | { 22 | test: /\.m?js$/, 23 | exclude: /(node_modules|bower_components)/, 24 | use: { 25 | loader: "babel-loader" 26 | } 27 | } 28 | ] 29 | }, 30 | plugins: [ 31 | new CopyPlugin({ 32 | patterns: [ 33 | { 34 | from: path.join(__dirname, "src/index.d.ts"), 35 | to: path.join(__dirname, 'lib', 'react-optimize.d.ts'), 36 | } 37 | ] 38 | }) 39 | ] 40 | }; 41 | --------------------------------------------------------------------------------