├── .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 | [](#contributors-)
5 |
6 |
7 | [](https://github.com/hudovisk/react-optimize/actions) [](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 | 
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 |
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 |
--------------------------------------------------------------------------------