diff --git a/.eslintrc.js b/.eslintrc.js index 9eba935..bbebf16 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,6 +18,7 @@ module.exports = { 'plugins': [ 'react', '@typescript-eslint', + 'eslint-plugin-tsdoc', 'import' ], 'rules': { @@ -63,5 +64,11 @@ module.exports = { 'import/no-self-import': 'error', 'import/no-default-export': 'error', } + }, { + // Enable TSDoc rules for TypeScript files, allowing the use of JSDoc in JS files. + 'files': ['**/*.ts', '**/*.tsx'], + 'rules': { + 'tsdoc/syntax': 'warn' + } }] } diff --git a/.gitignore b/.gitignore index 204ddd1..767609d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ .DS_Store /.vscode /node_modules -/lib -/es +/cjs +/esm /umd /types /coverage diff --git a/CHANGES.txt b/CHANGES.txt index a386c49..484690e 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,19 @@ +2.0.0 (November 1, 2024) + - Added support for targeting rules based on large segments. + - Added support for passing factory instances to the `factory` prop of the `SplitFactoryProvider` component from other SDK packages that extends the `SplitIO.IBrowserSDK` interface, such as `@splitsoftware/splitio-react-native`, `@splitsoftware/splitio-browserjs` and `@splitsoftware/browser-suite` packages. + - Updated @splitsoftware/splitio package to version 11.0.0 that includes major updates, and updated some transitive dependencies for vulnerability fixes. + - Renamed distribution folders from `/lib` to `/cjs` for CommonJS build, and `/es` to `/esm` for ECMAScript Modules build. + - Bugfixing - When the `config` prop is provided, the `SplitFactoryProvider` now makes the SDK factory and client instances available in the context immediately during the initial render, instead of waiting for the first SDK event (Related to https://github.com/splitio/react-client/issues/198). This change fixes a bug in the `useTrack` hook, which was not retrieving the client's `track` method during the initial render. + - BREAKING CHANGES: + - Updated the default value of the `updateOnSdkUpdate` and `updateOnSdkTimedout` parameters of the `useSplitClient` and `useSplitTreatments` hooks options object to `true`, to re-render on all SDK events by default. The same applies for the equivalent props in the `[with]SplitClient` and `[with]SplitTreatments` components. + - Updated error handling: using the library modules without wrapping them in a `SplitFactoryProvider` component will now throw an error instead of logging it, as the modules requires the `SplitContext` to work properly. + - Updated the `SplitFactoryProvider` component to not accept a child as a function (render prop), to avoid unnecessary re-renders when using the library hooks. Refer to ./MIGRATION-GUIDE.md for instructions on how to migrate the child as a function to a regular component. + - Removed the `core.trafficType` option from the SDK configuration object, and the `trafficType` parameter from the SDK `client()` method, `useSplitClient`, `useTrack`, `withSplitClient` and `SplitClient` component. This is because traffic types can no longer be bound to SDK clients in JavaScript SDK v11.0.0, and so the traffic type must be provided as first argument in the `track` method calls. + - Removed deprecated modules: `SplitFactory` component, `useClient`, `useTreatments` and `useManager` hooks. Refer to ./MIGRATION-GUIDE.md for instructions on how to migrate to the new alternatives. + - Renamed `SplitSdk` to `SplitFactory` function, which is the underlying Split SDK factory, i.e., `import { SplitFactory } from '@splitsoftware/splitio'`. + - Renamed TypeScript interface: `ISplitFactoryProps` to `ISplitFactoryProviderProps`. + - Dropped support for React below 16.8.0, as the library components where rewritten using the React Hooks API available in React v16.8.0 and above. This refactor simplifies code maintenance and reduces bundle size. + 1.13.0 (September 6, 2024) - Updated @splitsoftware/splitio package to version 10.28.0 that includes minor updates: - Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks. diff --git a/MIGRATION-GUIDE.md b/MIGRATION-GUIDE.md index 7cb805f..36932cd 100644 --- a/MIGRATION-GUIDE.md +++ b/MIGRATION-GUIDE.md @@ -14,7 +14,7 @@ Notable changes to consider when migrating: - When using the `config` prop with `SplitFactoryProvider`, the `factory` and `client` properties in `SplitContext` and the `manager` property in `useSplitManager` results are `null` in the first render, until the context is updated when some event is emitted on the SDK main client (ready, ready from cache, timeout, or update, depending on the configuration of the `updateOn` props of the component). This differs from the previous behavior where `factory`, `client`, and `manager` were immediately available. Nonetheless, it is not recommended to use the `client` and `factory` properties directly as better alternatives are available. For example, use the `useTrack` and `useSplitTreatments` hooks rather than the client's `track` and `getTreatments` methods. - - Updating the `config` prop in `SplitFactoryProvider` reinitializes the SDK with the new configuration, while `SplitFactory` does not reinitialize the SDK. You should pass a reference to the configuration object (e.g., via a global variable, `useState`, or `useMemo`) rather than a new instance on each render, to avoid unnecessary reinitializations. + - Updating the `config` prop in `SplitFactoryProvider` re-initializes the SDK with the new configuration, while `SplitFactory` does not reinitialize the SDK. You should pass a reference to the configuration object (e.g., via a global variable, `useState`, or `useMemo`) rather than a new instance on each render, to avoid unnecessary re-initializations. - Updating the `factory` prop in `SplitFactoryProvider` replaces the current SDK instance, unlike `SplitFactory` where it is ignored. diff --git a/package-lock.json b/package-lock.json index 3197930..957d359 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.13.0", + "version": "2.0.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio-react", - "version": "1.13.0", + "version": "2.0.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio": "10.28.0", + "@splitsoftware/splitio": "11.0.0", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" @@ -30,6 +30,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-tsdoc": "^0.3.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", @@ -45,7 +46,7 @@ "webpack-merge": "^5.8.0" }, "peerDependencies": { - "react": ">=16.3.0" + "react": ">=16.8.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -1494,6 +1495,46 @@ "integrity": "sha512-h/luqw9oAmMF1C/GuUY/PAgZlF4wx71q2bdH+ct8vmjcvseCY32au8XmYy7xZ8l5VJiY/3ltFpr5YiO55v0mzg==", "dev": true }, + "node_modules/@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "dev": true + }, + "node_modules/@microsoft/tsdoc-config": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", + "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.15.0", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@microsoft/tsdoc-config/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1548,13 +1589,11 @@ } }, "node_modules/@splitsoftware/splitio": { - "version": "10.28.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0.tgz", - "integrity": "sha512-hzBnBZHmUTXvyMBbVTDUYtspLHjyjb/YqKtetNh7pAvkmj37vOXyXfF50Of5jr3Qmvdo0YFbKvMIOEXlXSGWaQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.0.0.tgz", + "integrity": "sha512-9ZEhbpsgjvg41FQaahaG+JouzTQ91mX3xgzSClPMPL55kryjXAnQk9jawapnlkI1cKqE4VDvrznkmfr2/4qHRA==", "dependencies": { - "@splitsoftware/splitio-commons": "1.17.0", - "@types/google.analytics": "0.0.40", - "@types/ioredis": "^4.28.0", + "@splitsoftware/splitio-commons": "2.0.0", "bloom-filters": "^3.0.0", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -1563,15 +1602,15 @@ "unfetch": "^4.2.0" }, "engines": { - "node": ">=6", - "npm": ">=3" + "node": ">=14.0.0" } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0.tgz", - "integrity": "sha512-rvP+0LGUN92bcTytiqyVxq9UzBG5kTkIYjU7b7AU2awBUYgM0bqT3xhQ9/MJ/2fsBbqC6QIsxoKDOz9pMgbAQw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.0.0.tgz", + "integrity": "sha512-Sz4+vFacl29xw3451z9IUgB4zBFKUWZdCnmOB0DDXA803YKPqjXphdAwN6nV+1vsX9pXV/OS6UaNC4oUICa6PA==", "dependencies": { + "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" }, "peerDependencies": { @@ -1847,11 +1886,6 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, - "node_modules/@types/google.analytics": { - "version": "0.0.40", - "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.40.tgz", - "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==" - }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -4201,6 +4235,16 @@ "semver": "bin/semver.js" } }, + "node_modules/eslint-plugin-tsdoc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.3.0.tgz", + "integrity": "sha512-0MuFdBrrJVBjT/gyhkP2BqpD0np1NxNLfQ38xXDlSs/KVVpKI2A6vN7jx2Rve/CyUsvOsMGwp9KKrinv7q9g3A==", + "dev": true, + "dependencies": { + "@microsoft/tsdoc": "0.15.0", + "@microsoft/tsdoc-config": "0.17.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -4876,10 +4920,13 @@ } }, "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, "node_modules/function.prototype.name": { "version": "1.1.5", @@ -5173,6 +5220,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -5513,12 +5572,15 @@ "dev": true }, "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "dependencies": { - "has": "^1.0.3" + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7848,6 +7910,12 @@ "node": ">=8" } }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9192,6 +9260,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -9205,12 +9282,12 @@ "dev": true }, "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "dependencies": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -11990,6 +12067,44 @@ "integrity": "sha512-h/luqw9oAmMF1C/GuUY/PAgZlF4wx71q2bdH+ct8vmjcvseCY32au8XmYy7xZ8l5VJiY/3ltFpr5YiO55v0mzg==", "dev": true }, + "@microsoft/tsdoc": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc/-/tsdoc-0.15.0.tgz", + "integrity": "sha512-HZpPoABogPvjeJOdzCOSJsXeL/SMCBgBZMVC3X3d7YYp2gf31MfxhUoYUNwf1ERPJOnQc0wkFn9trqI6ZEdZuA==", + "dev": true + }, + "@microsoft/tsdoc-config": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@microsoft/tsdoc-config/-/tsdoc-config-0.17.0.tgz", + "integrity": "sha512-v/EYRXnCAIHxOHW+Plb6OWuUoMotxTN0GLatnpOb1xq0KuTNw/WI3pamJx/UbsoJP5k9MCw1QxvvhPcF9pH3Zg==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.15.0", + "ajv": "~8.12.0", + "jju": "~1.4.0", + "resolve": "~1.22.2" + }, + "dependencies": { + "ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + } + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -12035,13 +12150,11 @@ } }, "@splitsoftware/splitio": { - "version": "10.28.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-10.28.0.tgz", - "integrity": "sha512-hzBnBZHmUTXvyMBbVTDUYtspLHjyjb/YqKtetNh7pAvkmj37vOXyXfF50Of5jr3Qmvdo0YFbKvMIOEXlXSGWaQ==", + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio/-/splitio-11.0.0.tgz", + "integrity": "sha512-9ZEhbpsgjvg41FQaahaG+JouzTQ91mX3xgzSClPMPL55kryjXAnQk9jawapnlkI1cKqE4VDvrznkmfr2/4qHRA==", "requires": { - "@splitsoftware/splitio-commons": "1.17.0", - "@types/google.analytics": "0.0.40", - "@types/ioredis": "^4.28.0", + "@splitsoftware/splitio-commons": "2.0.0", "bloom-filters": "^3.0.0", "ioredis": "^4.28.0", "js-yaml": "^3.13.1", @@ -12051,10 +12164,11 @@ } }, "@splitsoftware/splitio-commons": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.17.0.tgz", - "integrity": "sha512-rvP+0LGUN92bcTytiqyVxq9UzBG5kTkIYjU7b7AU2awBUYgM0bqT3xhQ9/MJ/2fsBbqC6QIsxoKDOz9pMgbAQw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.0.0.tgz", + "integrity": "sha512-Sz4+vFacl29xw3451z9IUgB4zBFKUWZdCnmOB0DDXA803YKPqjXphdAwN6nV+1vsX9pXV/OS6UaNC4oUICa6PA==", "requires": { + "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" } }, @@ -12269,11 +12383,6 @@ "integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==", "dev": true }, - "@types/google.analytics": { - "version": "0.0.40", - "resolved": "https://registry.npmjs.org/@types/google.analytics/-/google.analytics-0.0.40.tgz", - "integrity": "sha512-R3HpnLkqmKxhUAf8kIVvDVGJqPtaaZlW4yowNwjOZUTmYUQEgHh8Nh5wkSXKMroNAuQM8gbXJHmNbbgA8tdb7Q==" - }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -14345,6 +14454,16 @@ "dev": true, "requires": {} }, + "eslint-plugin-tsdoc": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-tsdoc/-/eslint-plugin-tsdoc-0.3.0.tgz", + "integrity": "sha512-0MuFdBrrJVBjT/gyhkP2BqpD0np1NxNLfQ38xXDlSs/KVVpKI2A6vN7jx2Rve/CyUsvOsMGwp9KKrinv7q9g3A==", + "dev": true, + "requires": { + "@microsoft/tsdoc": "0.15.0", + "@microsoft/tsdoc-config": "0.17.0" + } + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -14610,9 +14729,9 @@ "optional": true }, "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", "dev": true }, "function.prototype.name": { @@ -14817,6 +14936,15 @@ "has-symbols": "^1.0.2" } }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, "hosted-git-info": { "version": "2.8.9", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", @@ -15057,12 +15185,12 @@ } }, "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", "dev": true, "requires": { - "has": "^1.0.3" + "hasown": "^2.0.2" } }, "is-date-object": { @@ -16775,6 +16903,12 @@ } } }, + "jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17822,6 +17956,12 @@ "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=", "dev": true }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, "require-main-filename": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", @@ -17835,12 +17975,12 @@ "dev": true }, "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", "dev": true, "requires": { - "is-core-module": "^2.9.0", + "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" } diff --git a/package.json b/package.json index caddcf5..9e8b856 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "@splitsoftware/splitio-react", - "version": "1.13.0", + "version": "2.0.0", "description": "A React library to easily integrate and use Split JS SDK", - "main": "lib/index.js", - "module": "es/index.js", + "main": "cjs/index.js", + "module": "esm/index.js", "types": "types/index.d.ts", "files": [ "README.md", @@ -12,21 +12,21 @@ "LICENSE", "CHANGES.txt", "src", - "lib", - "es", + "cjs", + "esm", "types" ], "sideEffects": false, "scripts": { - "build:cjs": "rimraf lib/* types/* && tsc -m commonjs --outDir lib -d true --declarationDir types", - "build:esm": "rimraf es/* && tsc", + "build:cjs": "rimraf cjs/* types/* && tsc -m commonjs --outDir cjs -d true --declarationDir types", + "build:esm": "rimraf esm/* && tsc", "build:umd": "rimraf umd/* && webpack --config webpack.dev.js --env branch=$BUILD_BRANCH && webpack --config webpack.prod.js --env branch=$BUILD_BRANCH", "build": "npm run build:cjs && npm run build:esm && npm run build:umd", - "postbuild": "replace 'REACT_SDK_VERSION_NUMBER' $npm_package_version ./lib/constants.js ./es/constants.js ./umd -r", + "postbuild": "replace 'REACT_SDK_VERSION_NUMBER' $npm_package_version ./cjs/constants.js ./esm/constants.js ./umd -r", "check": "npm run check:lint && npm run check:types", "check:lint": "eslint 'src/**/*.ts*'", "check:types": "tsc --noEmit", - "test": "jest src", + "test": "jest src --silent", "test:watch": "npm test -- --watch", "test:coverage": "jest src --coverage", "test:debug": "node --inspect node_modules/.bin/jest --runInBand", @@ -63,7 +63,7 @@ }, "homepage": "https://github.com/splitio/react-client#readme", "dependencies": { - "@splitsoftware/splitio": "10.28.0", + "@splitsoftware/splitio": "11.0.0", "memoize-one": "^5.1.1", "shallowequal": "^1.1.0", "tslib": "^2.3.1" @@ -84,6 +84,7 @@ "eslint-plugin-import": "^2.27.5", "eslint-plugin-react": "^7.32.2", "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-tsdoc": "^0.3.0", "husky": "^3.1.0", "jest": "^27.2.3", "react": "^18.0.0", @@ -99,7 +100,7 @@ "webpack-merge": "^5.8.0" }, "peerDependencies": { - "react": ">=16.3.0" + "react": ">=16.8.0" }, "husky": { "hooks": { diff --git a/src/SplitClient.tsx b/src/SplitClient.tsx index 22e325c..7640ee6 100644 --- a/src/SplitClient.tsx +++ b/src/SplitClient.tsx @@ -1,144 +1,28 @@ import React from 'react'; import { SplitContext } from './SplitContext'; -import { ISplitClientProps, ISplitContextValues, IUpdateProps } from './types'; -import { getStatus, getSplitClient, initAttributes, IClientWithContext } from './utils'; -import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; - -/** - * Common component used to handle the status and events of a Split client passed as prop. - * Reused by both SplitFactoryProvider (main client) and SplitClient (any client) components. - */ -export class SplitComponent extends React.Component { - - static defaultProps = { - children: null, - factory: null, - client: null, - ...DEFAULT_UPDATE_OPTIONS, - } - - // Using `getDerivedStateFromProps` since the state depends on the status of the client in props, which might change over time. - // It could be avoided by removing the client and its status from the component state. - // But it implies to have another instance property to use instead of the state, because we need a unique reference value for SplitContext.Provider - static getDerivedStateFromProps(props: ISplitClientProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }, state: ISplitContextValues) { - const { client, factory, attributes } = props; - // initAttributes can be called in the `render` method too, but it is better here for separation of concerns - initAttributes(client, attributes); - const status = getStatus(client); - // no need to compare status.isTimedout, since it derives from isReady and hasTimedout - if (client !== state.client || - status.isReady !== state.isReady || - status.isReadyFromCache !== state.isReadyFromCache || - status.hasTimedout !== state.hasTimedout || - status.isDestroyed !== state.isDestroyed) { - return { - client, - factory, - ...status, - }; - } - return null; - } - - readonly state: Readonly; - - constructor(props: ISplitClientProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }) { - super(props); - const { factory, client } = props; - - this.state = { - factory, - client, - ...getStatus(client), - }; - } - - // Attach listeners for SDK events, to update state if client status change. - // The listeners take into account the value of `updateOnSdk***` props. - subscribeToEvents(client: SplitIO.IBrowserClient | null) { - if (client) { - const statusOnEffect = getStatus(client); - const status = this.state; - - if (this.props.updateOnSdkReady) { - if (!statusOnEffect.isReady) client.once(client.Event.SDK_READY, this.update); - else if (!status.isReady) this.update(); - } - if (this.props.updateOnSdkReadyFromCache) { - if (!statusOnEffect.isReadyFromCache) client.once(client.Event.SDK_READY_FROM_CACHE, this.update); - else if (!status.isReadyFromCache) this.update(); - } - if (this.props.updateOnSdkTimedout) { - if (!statusOnEffect.hasTimedout) client.once(client.Event.SDK_READY_TIMED_OUT, this.update); - else if (!status.hasTimedout) this.update(); - } - if (this.props.updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, this.update); - } - } - - unsubscribeFromEvents(client: SplitIO.IBrowserClient | null) { - if (client) { - client.off(client.Event.SDK_READY, this.update); - client.off(client.Event.SDK_READY_FROM_CACHE, this.update); - client.off(client.Event.SDK_READY_TIMED_OUT, this.update); - client.off(client.Event.SDK_UPDATE, this.update); - } - } - - update = () => { - this.setState({ lastUpdate: (this.state.client as IClientWithContext).__getStatus().lastUpdate }); - } - - componentDidMount() { - this.subscribeToEvents(this.props.client); - } - - componentDidUpdate(prevProps: ISplitClientProps & { factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }) { - if (this.props.client !== prevProps.client) { - this.unsubscribeFromEvents(prevProps.client); - this.subscribeToEvents(this.props.client); - } - } - - componentWillUnmount() { - // unsubscribe from events, to remove references to SplitClient instance methods - this.unsubscribeFromEvents(this.props.client); - } - - render() { - const { children } = this.props; - return ( - - { - typeof children === 'function' ? - children({ ...this.state }) : - children - } - - ); - } -} +import { ISplitClientProps } from './types'; +import { useSplitClient } from './useSplitClient'; /** * SplitClient will initialize a new SDK client and listen for its events in order to update the Split Context. * Children components will have access to the new client when accessing Split Context. * * The underlying SDK client can be changed during the component lifecycle - * if the component is updated with a different splitKey or trafficType prop. + * if the component is updated with a different splitKey prop. * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} + * @deprecated `SplitClient` will be removed in a future major release. We recommend replacing it with the `useSplitClient` hook. */ export function SplitClient(props: ISplitClientProps) { + const { children } = props; + const context = useSplitClient(props); + return ( - - {(splitContext: ISplitContextValues) => { - const { factory } = splitContext; - // getSplitClient is idempotent like factory.client: it returns the same client given the same factory, Split Key and TT - const client = factory ? getSplitClient(factory, props.splitKey, props.trafficType) : null; - return ( - - ); - }} - - ); + + { + typeof children === 'function' ? + children(context) : + children + } + + ) } diff --git a/src/SplitContext.ts b/src/SplitContext.ts index 812a0b4..5e07aff 100644 --- a/src/SplitContext.ts +++ b/src/SplitContext.ts @@ -1,23 +1,23 @@ import React from 'react'; import { ISplitContextValues } from './types'; -import { EXCEPTION_NO_REACT_OR_CREATECONTEXT } from './constants'; +import { EXCEPTION_NO_SFP } from './constants'; -if (!React || !React.createContext) throw new Error(EXCEPTION_NO_REACT_OR_CREATECONTEXT); - -export const INITIAL_CONTEXT: ISplitContextValues = { - client: null, - factory: null, - isReady: false, - isReadyFromCache: false, - isTimedout: false, - hasTimedout: false, - lastUpdate: 0, - isDestroyed: false, -} +/** + * Split Context is the React Context instance provided by the SplitFactoryProvider and consumed by the Split Hooks. + * It is used to share the SDK factory instance and other values across the application. + */ +export const SplitContext = React.createContext(undefined); /** - * Split Context is the React Context instance that represents our SplitIO global state. - * It contains Split SDK objects, such as a factory instance, a client and its status (isReady, isTimedout, lastUpdate) - * The context is created with default empty values, that SplitFactoryProvider and SplitClient access and update. + * Hook to access the value of `SplitContext`. + * + * @returns The Split Context object value + * @throws Throws an error if the Split Context is not set (i.e. the component is not wrapped in a SplitFactoryProvider) */ -export const SplitContext = React.createContext(INITIAL_CONTEXT); +export function useSplitContext() { + const context = React.useContext(SplitContext); + + if (!context) throw new Error(EXCEPTION_NO_SFP) + + return context; +} diff --git a/src/SplitFactory.tsx b/src/SplitFactory.tsx deleted file mode 100644 index 1636100..0000000 --- a/src/SplitFactory.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import React from 'react'; - -import { SplitComponent } from './SplitClient'; -import { ISplitFactoryProps } from './types'; -import { WARN_SF_CONFIG_AND_FACTORY, ERROR_SF_NO_CONFIG_AND_FACTORY } from './constants'; -import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient } from './utils'; -import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; - -/** - * SplitFactory will initialize the Split SDK and its main client, listen for its events in order to update the Split Context, - * and automatically shutdown and release resources when it is unmounted. SplitFactory must wrap other components and functions - * from this library, since they access the Split Context and its properties (factory, client, isReady, etc). - * - * The underlying SDK factory and client is set on the constructor, and cannot be changed during the component lifecycle, - * even if the component is updated with a different config or factory prop. - * - * @deprecated `SplitFactory` will be removed in a future major release. We recommend replacing it with the new `SplitFactoryProvider` component. - * - * `SplitFactoryProvider` is a revised version of `SplitFactory` that properly handles SDK side effects (i.e., factory creation and destruction) within the React component lifecycle, - * resolving memory leak issues in React development mode, strict mode and server-side rendering, and also ensuring that the SDK is updated if `config` or `factory` props change. - * - * Notable changes to consider when migrating: - * - `SplitFactoryProvider` utilizes the React Hooks API, requiring React 16.8.0 or later, while `SplitFactory` is compatible with React 16.3.0 or later. - * - When using the `config` prop with `SplitFactoryProvider`, the `factory` and `client` properties in `SplitContext` and the `manager` property in `useSplitManager` results - * are `null` in the first render, until the context is updated when some event is emitted on the SDK main client (ready, ready from cache, timeout, or update, depending on - * the configuration of the `updateOn` props of the component). This differs from the previous behavior where `factory`, `client`, and `manager` were immediately available. - * - Updating the `config` prop in `SplitFactoryProvider` reinitializes the SDK with the new configuration, while `SplitFactory` does not reinitialize the SDK. You should pass a - * reference to the configuration object (e.g., via a global variable, `useState`, or `useMemo`) rather than a new instance on each render, to avoid unnecessary reinitializations. - * - Updating the `factory` prop in `SplitFactoryProvider` replaces the current SDK instance, unlike `SplitFactory` where it is ignored. - * - * @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client} - */ -export class SplitFactory extends React.Component { - - static defaultProps: ISplitFactoryProps = { - children: null, - ...DEFAULT_UPDATE_OPTIONS, - }; - - readonly state: Readonly<{ factory: SplitIO.IBrowserSDK | null, client: SplitIO.IBrowserClient | null }>; - readonly isFactoryExternal: boolean; - - constructor(props: ISplitFactoryProps) { - super(props); - - // Log warning and error - const { factory: propFactory, config } = props; - if (!config && !propFactory) { - console.error(ERROR_SF_NO_CONFIG_AND_FACTORY); - } - if (config && propFactory) { - console.log(WARN_SF_CONFIG_AND_FACTORY); - } - - // Instantiate factory - let factory = null; - if (propFactory) { - factory = propFactory; - } else { - if (config) { - // We use an idempotent variant of the Split factory builder (i.e., given the same config, it returns the same already - // created instance), since React component constructors is part of render-phase and can be invoked multiple times. - factory = getSplitFactory(config); - } - } - this.isFactoryExternal = propFactory ? true : false; - - // Instantiate main client. Attributes are set on `SplitComponent.getDerivedStateFromProps` - const client = factory ? getSplitClient(factory) : null; - - this.state = { - client, - factory, - }; - } - - componentWillUnmount() { - // only destroy the client if the factory was created internally. Otherwise, the shutdown must be handled by the user - if (!this.isFactoryExternal && this.state.factory) { - destroySplitFactory(this.state.factory as IFactoryWithClients); - } - } - - render() { - const { factory, client } = this.state; - - return ( - - ); - } -} diff --git a/src/SplitFactoryProvider.tsx b/src/SplitFactoryProvider.tsx index 5b55b8b..02e36b0 100644 --- a/src/SplitFactoryProvider.tsx +++ b/src/SplitFactoryProvider.tsx @@ -1,86 +1,49 @@ import React from 'react'; -import { SplitComponent } from './SplitClient'; -import { ISplitFactoryProps } from './types'; +import { ISplitFactoryProviderProps } from './types'; import { WARN_SF_CONFIG_AND_FACTORY } from './constants'; -import { getSplitFactory, destroySplitFactory, IFactoryWithClients, getSplitClient, getStatus } from './utils'; -import { DEFAULT_UPDATE_OPTIONS } from './useSplitClient'; +import { getSplitFactory, destroySplitFactory, getSplitClient, getStatus, initAttributes } from './utils'; +import { SplitContext } from './SplitContext'; /** - * SplitFactoryProvider will initialize the Split SDK and its main client when `config` prop is provided or updated, listen for its events in order to update the Split Context, - * and automatically destroy the SDK (shutdown and release resources) when it is unmounted or `config` prop updated. SplitFactoryProvider must wrap other library components and - * functions since they access the Split Context and its properties (factory, client, isReady, etc). + * The SplitFactoryProvider is the top level component that provides the Split SDK factory to all child components via the Split Context. + * It accepts either an SDK `factory` instance or a `config` object as props to initialize a new SDK factory. * * NOTE: Either pass a `factory` instance or a `config` object as props. If both props are passed, the `config` prop will be ignored. - * Pass the same reference to the `config` or `factory` object rather than a new instance on each render, to avoid unnecessary props changes and SDK reinitializations. + * Pass the same reference to the `config` or `factory` object rather than a new instance on each render, to avoid unnecessary props changes and SDK re-initializations. * * @see {@link https://help.split.io/hc/en-us/articles/360038825091-React-SDK#2-instantiate-the-sdk-and-create-a-new-split-client} */ -export function SplitFactoryProvider(props: ISplitFactoryProps) { - let { - config, factory: propFactory, - updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate - } = { ...DEFAULT_UPDATE_OPTIONS, ...props }; +export function SplitFactoryProvider(props: ISplitFactoryProviderProps) { + const { config, factory: propFactory, attributes } = props; - if (config && propFactory) { - console.log(WARN_SF_CONFIG_AND_FACTORY); - config = undefined; - } + const factory = React.useMemo(() => { + const factory = propFactory || (config ? getSplitFactory(config) : undefined); + initAttributes(factory && factory.client(), attributes); + return factory; + }, [config, propFactory, attributes]); + const client = factory ? getSplitClient(factory) : undefined; - const [configFactory, setConfigFactory] = React.useState(null); - const factory = propFactory || (configFactory && config === configFactory.config ? configFactory : null); - const client = factory ? getSplitClient(factory) : null; - - // Effect to initialize and destroy the factory + // Effect to initialize and destroy the factory when config is provided React.useEffect(() => { + if (propFactory) { + if (config) console.log(WARN_SF_CONFIG_AND_FACTORY); + return; + } + if (config) { const factory = getSplitFactory(config); + factory.init && factory.init(); return () => { destroySplitFactory(factory); } } - }, [config]); - - // Effect to subscribe/unsubscribe to events - React.useEffect(() => { - const factory = config && getSplitFactory(config); - if (factory) { - const client = getSplitClient(factory); - const status = getStatus(client); - - // Unsubscribe from events and update state when first event is emitted - const update = () => { // eslint-disable-next-line no-use-before-define - unsubscribe(); - setConfigFactory(factory); - } - - const unsubscribe = () => { - client.off(client.Event.SDK_READY, update); - client.off(client.Event.SDK_READY_FROM_CACHE, update); - client.off(client.Event.SDK_READY_TIMED_OUT, update); - client.off(client.Event.SDK_UPDATE, update); - } - - if (updateOnSdkReady) { - if (status.isReady) update(); - else client.once(client.Event.SDK_READY, update); - } - if (updateOnSdkReadyFromCache) { - if (status.isReadyFromCache) update(); - else client.once(client.Event.SDK_READY_FROM_CACHE, update); - } - if (updateOnSdkTimedout) { - if (status.hasTimedout) update(); - else client.once(client.Event.SDK_READY_TIMED_OUT, update); - } - if (updateOnSdkUpdate) client.on(client.Event.SDK_UPDATE, update); - - return unsubscribe; - } - }, [config, updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate]); + }, [config, propFactory]); return ( - + + {props.children} + ); } diff --git a/src/SplitTreatments.tsx b/src/SplitTreatments.tsx index da803c3..1a9e2a8 100644 --- a/src/SplitTreatments.tsx +++ b/src/SplitTreatments.tsx @@ -1,37 +1,25 @@ import React from 'react'; + import { SplitContext } from './SplitContext'; -import { ISplitTreatmentsProps, ISplitContextValues } from './types'; -import { memoizeGetTreatmentsWithConfig } from './utils'; +import { ISplitTreatmentsProps } from './types'; +import { useSplitTreatments } from './useSplitTreatments'; /** * SplitTreatments accepts a list of feature flag names and optional attributes. It accesses the client at SplitContext to * call the 'client.getTreatmentsWithConfig()' method if the `names` prop is provided, or the 'client.getTreatmentsWithConfigByFlagSets()' method * if the `flagSets` prop is provided. It then passes the resulting treatments to a child component as a function. * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} + * @deprecated `SplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook. */ -export class SplitTreatments extends React.Component { - - private logWarning?: boolean; - - // Using a memoized `client.getTreatmentsWithConfig` function to avoid duplicated impressions - private evaluateFeatureFlags = memoizeGetTreatmentsWithConfig(); - - render() { - const { names, flagSets, children, attributes } = this.props; - - return ( - - {(splitContext: ISplitContextValues) => { - const { client, lastUpdate } = splitContext; - const treatments = this.evaluateFeatureFlags(client, lastUpdate, names, attributes, client ? { ...client.getAttributes() } : {}, flagSets); +export function SplitTreatments(props: ISplitTreatmentsProps) { + const { children } = props; + const context = useSplitTreatments(props); - // SplitTreatments only accepts a function as a child, not a React Element (JSX) - return children({ - ...splitContext, treatments, - }); - }} - - ); - } + return ( + + { + children(context) + } + + ); } diff --git a/src/__tests__/SplitClient.test.tsx b/src/__tests__/SplitClient.test.tsx index 641ed97..70d37e1 100644 --- a/src/__tests__/SplitClient.test.tsx +++ b/src/__tests__/SplitClient.test.tsx @@ -2,20 +2,21 @@ import React from 'react'; import { render, act } from '@testing-library/react'; /** Mocks and test utils */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; +import { mockSdk, Event, getLastInstance } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; /** Test target */ -import { ISplitClientChildProps } from '../types'; +import { ISplitClientChildProps, ISplitFactoryChildProps } from '../types'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; -import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; +import { INITIAL_STATUS, testAttributesBinding, TestComponentProps } from './testUtils/utils'; import { IClientWithContext } from '../utils'; +import { EXCEPTION_NO_SFP } from '../constants'; describe('SplitClient', () => { @@ -23,13 +24,12 @@ describe('SplitClient', () => { render( - {({ isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { - expect(isReady).toBe(false); - expect(isReadyFromCache).toBe(false); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe(0); + {(childProps: ISplitClientChildProps) => { + expect(childProps).toEqual({ + ...INITIAL_STATUS, + factory: getLastInstance(SplitFactory), + client: getLastInstance(SplitFactory).client('user1'), + }); return null; }} @@ -39,7 +39,7 @@ describe('SplitClient', () => { }); test('passes ready props to the child if client is ready.', async () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); @@ -49,14 +49,15 @@ describe('SplitClient', () => { {/* Equivalent to */} - {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitClientChildProps) => { - expect(client).toBe(outerFactory.client()); - expect(isReady).toBe(true); - expect(isReadyFromCache).toBe(true); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe((outerFactory.client() as IClientWithContext).__getStatus().lastUpdate); + {(childProps: ISplitClientChildProps) => { + expect(childProps).toEqual({ + ...INITIAL_STATUS, + factory: outerFactory, + client: outerFactory.client(), + isReady: true, + isReadyFromCache: true, + lastUpdate: (outerFactory.client() as IClientWithContext).__getStatus().lastUpdate + }); return null; }} @@ -65,8 +66,8 @@ describe('SplitClient', () => { ); }); - test('rerender child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events.', async () => { - const outerFactory = SplitSdk(sdkBrowser); + test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events as default behavior', async () => { + const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); @@ -77,7 +78,7 @@ describe('SplitClient', () => { render( - + {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; switch (renderTimes) { @@ -117,8 +118,8 @@ describe('SplitClient', () => { expect(renderTimes).toBe(5); }); - test('rerender child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => { - const outerFactory = SplitSdk(sdkBrowser); + test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => { + const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); @@ -161,8 +162,8 @@ describe('SplitClient', () => { expect(renderTimes).toBe(3); }); - test('rerender child only on SDK_READY event, as default behaviour.', async () => { - const outerFactory = SplitSdk(sdkBrowser); + test('rerenders child only on SDK_READY event, when setting updateOnSdkReadyFromCache, updateOnSdkTimedout and updateOnSdkUpdate to false.', async () => { + const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); @@ -173,7 +174,7 @@ describe('SplitClient', () => { render( - + {({ client, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitClientChildProps) => { const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; switch (renderTimes) { @@ -203,7 +204,7 @@ describe('SplitClient', () => { }); test('must update on SDK events between the render and commit phases', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); let count = 0; render( @@ -213,8 +214,7 @@ describe('SplitClient', () => { count++; // side effect in the render phase - if (!(client as IClientWithContext).__getStatus().isReady) { - console.log('emit'); + if (!(client as any).__getStatus().isReady) { (client as any).__emitter__.emit(Event.SDK_READY); } @@ -227,18 +227,19 @@ describe('SplitClient', () => { expect(count).toEqual(2); }); - test('renders a passed JSX.Element with a new SplitContext value.', (done) => { - const outerFactory = SplitSdk(sdkBrowser); + test('renders a passed JSX.Element with a new SplitContext value.', () => { + const outerFactory = SplitFactory(sdkBrowser); const Component = () => { return ( {(value) => { - expect(value.client).toBe(outerFactory.client('user2')); - expect(value.isReady).toBe(false); - expect(value.isTimedout).toBe(false); - expect(value.lastUpdate).toBe(0); - done(); + expect(value).toEqual({ + ...INITIAL_STATUS, + factory: outerFactory, + client: outerFactory.client('user2'), + }); + return null; }} @@ -254,23 +255,19 @@ describe('SplitClient', () => { ); }); - // @TODO Update test in breaking change, following common practice in React libraries, like React-redux and React-query: use a falsy value as default context value, and throw an error – instead of logging it – if components are not wrapped in a SplitContext.Provider, i.e., if the context is falsy. - // test('logs error and passes null client if rendered outside an SplitProvider component.', () => { - // const errorSpy = jest.spyOn(console, 'error'); - // render( - // - // {({ client }) => { - // expect(client).toBe(null); - // return null; - // }} - // - // ); - // expect(errorSpy).toBeCalledWith(ERROR_SC_NO_FACTORY); - // }); + test('throws error if invoked outside of SplitFactoryProvider.', () => { + expect(() => { + render( + + {() => null} + + ); + }).toThrow(EXCEPTION_NO_SFP); + }); test(`passes a new client if re-rendered with a different splitKey. Only updates the state if the new client triggers an event, but not the previous one.`, (done) => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); let renderTimes = 0; class InnerComponent extends React.Component { @@ -350,7 +347,7 @@ describe('SplitClient', () => { function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { return ( - + {() => { testSwitch(done, splitKey); return null; @@ -364,3 +361,254 @@ describe('SplitClient', () => { }); }); + +// Tests to validate the migration from `SplitFactoryProvider` with child as a function in v1, to `SplitFactoryProvider` + `SplitClient` with child as a function in v2. +describe('SplitFactoryProvider + SplitClient', () => { + + test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events as default behavior (config prop)', async () => { + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Ready from cache + expect(statusProps).toStrictEqual([false, true, true, true]); + break; + case 3: // Ready + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + case 4: // Updated + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + default: + fail('Child must not be rerendered'); + } // eslint-disable-next-line no-use-before-define + expect(factory).toBe(getLastInstance(SplitFactory)); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + + ); + + const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value; + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(renderTimes).toBe(5); + }); + + test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events as default behavior (factory prop)', async () => { + const outerFactory = SplitFactory(sdkBrowser); + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Ready from cache + expect(statusProps).toStrictEqual([false, true, true, true]); + break; + case 3: // Ready + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + case 4: // Updated + expect(statusProps).toStrictEqual([true, true, true, false]); + break; + default: + fail('Child must not be rerendered'); + } + expect(factory).toBe(outerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + + ); + + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(renderTimes).toBe(5); + }); + + test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (config prop)', async () => { + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. + expect(statusProps).toStrictEqual([true, false, true, false]); + break; + default: + fail('Child must not be rerendered'); + } // eslint-disable-next-line no-use-before-define + expect(factory).toBe(getLastInstance(SplitFactory)); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + + ); + + const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value; + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(renderTimes).toBe(3); + }); + + test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (factory prop)', async () => { + const outerFactory = SplitFactory(sdkBrowser); + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Timedout + expect(statusProps).toStrictEqual([false, false, true, true]); + break; + case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. + expect(statusProps).toStrictEqual([true, false, true, false]); + break; + default: + fail('Child must not be rerendered'); + } + expect(factory).toBe(outerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + + ); + + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(renderTimes).toBe(3); + }); + + test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event (config prop)', async () => { + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Ready + expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state + break; + default: + fail('Child must not be rerendered'); + } // eslint-disable-next-line no-use-before-define + expect(factory).toBe(getLastInstance(SplitFactory)); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + + ); + + const innerFactory = (SplitFactory as jest.Mock).mock.results.slice(-1)[0].value; + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + expect(renderTimes).toBe(2); + }); + + test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event (factory prop)', async () => { + const outerFactory = SplitFactory(sdkBrowser); + let renderTimes = 0; + let previousLastUpdate = -1; + + render( + + + {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { + const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; + switch (renderTimes) { + case 0: // No ready + expect(statusProps).toStrictEqual([false, false, false, false]); + break; + case 1: // Ready + expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state + break; + default: + fail('Child must not be rerendered'); + } + expect(factory).toBe(outerFactory); + expect(lastUpdate).toBeGreaterThan(previousLastUpdate); + renderTimes++; + previousLastUpdate = lastUpdate; + return null; + }} + + + ); + + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); + act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); + expect(renderTimes).toBe(2); + }); + +}); diff --git a/src/__tests__/SplitContext.test.tsx b/src/__tests__/SplitContext.test.tsx index 0a1b140..5388adb 100644 --- a/src/__tests__/SplitContext.test.tsx +++ b/src/__tests__/SplitContext.test.tsx @@ -1,6 +1,8 @@ import React from 'react'; import { render } from '@testing-library/react'; import { SplitContext } from '../SplitContext'; +import { SplitFactoryProvider } from '../SplitFactoryProvider'; +import { INITIAL_STATUS } from './testUtils/utils'; /** * Test default SplitContext value @@ -9,16 +11,26 @@ test('SplitContext.Consumer shows default value', () => { render( {(value) => { - expect(value.factory).toBe(null); - expect(value.client).toBe(null); - expect(value.isReady).toBe(false); - expect(value.isReadyFromCache).toBe(false); - expect(value.hasTimedout).toBe(false); - expect(value.isTimedout).toBe(false); - expect(value.isDestroyed).toBe(false); - expect(value.lastUpdate).toBe(0); + expect(value).toBe(undefined); return null; }} ); }); + +test('SplitContext.Consumer shows value when wrapped in a SplitFactoryProvider', () => { + render( + + + {(value) => { + expect(value).toEqual({ + ...INITIAL_STATUS, + factory: undefined, + client: undefined + }); + return null; + }} + + + ); +}); diff --git a/src/__tests__/SplitFactory.test.tsx b/src/__tests__/SplitFactory.test.tsx deleted file mode 100644 index 9832cd7..0000000 --- a/src/__tests__/SplitFactory.test.tsx +++ /dev/null @@ -1,287 +0,0 @@ -import React from 'react'; -import { render, act } from '@testing-library/react'; - -/** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; -jest.mock('@splitsoftware/splitio/client', () => { - return { SplitFactory: mockSdk() }; -}); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; -import { sdkBrowser } from './testUtils/sdkConfigs'; -const logSpy = jest.spyOn(console, 'log'); - -/** Test target */ -import { ISplitFactoryChildProps } from '../types'; -import { SplitFactory } from '../SplitFactory'; -import { SplitClient } from '../SplitClient'; -import { SplitContext } from '../SplitContext'; -import { __factories, IClientWithContext } from '../utils'; -import { WARN_SF_CONFIG_AND_FACTORY, ERROR_SF_NO_CONFIG_AND_FACTORY } from '../constants'; - -describe('SplitFactory', () => { - - test('passes no-ready props to the child if initialized with a config.', () => { - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => { - expect(factory).toBeInstanceOf(Object); - expect(isReady).toBe(false); - expect(isReadyFromCache).toBe(false); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe(0); - expect((factory as SplitIO.ISDK).settings.version).toContain('react-'); - return null; - }} - - ); - }); - - test('passes ready props to the child if initialized with a ready factory.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE); - (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); - (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); - await outerFactory.client().ready(); - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => { - expect(factory).toBe(outerFactory); - expect(isReady).toBe(true); - expect(isReadyFromCache).toBe(true); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe((outerFactory.client() as IClientWithContext).__getStatus().lastUpdate); - expect((factory as SplitIO.ISDK).settings.version).toBe(outerFactory.settings.version); - return null; - }} - - ); - }); - - test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Timedout - expect(statusProps).toStrictEqual([false, false, true, true]); - break; - case 2: // Ready from cache - expect(statusProps).toStrictEqual([false, true, true, true]); - break; - case 3: // Ready - expect(statusProps).toStrictEqual([true, true, true, false]); - break; - case 4: // Updated - expect(statusProps).toStrictEqual([true, true, true, false]); - break; - default: - fail('Child must not be rerendered'); - } - expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - - expect(renderTimes).toBe(5); - }); - - test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Timedout - expect(statusProps).toStrictEqual([false, false, true, true]); - break; - case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. - expect(statusProps).toStrictEqual([true, false, true, false]); - break; - default: - fail('Child must not be rerendered'); - } - expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - - expect(renderTimes).toBe(3); - }); - - test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Ready - expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state - break; - default: - fail('Child must not be rerendered'); - } - expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - expect(renderTimes).toBe(2); - }); - - test('renders a passed JSX.Element with a new SplitContext value.', (done) => { - const Component = () => { - return ( - - {(value) => { - expect(value.factory).toBeInstanceOf(Object); - expect(value.client).toBe(value.factory?.client()); - expect(value.isReady).toBe(false); - expect(value.isTimedout).toBe(false); - expect(value.lastUpdate).toBe(0); - done(); - return null; - }} - - ); - }; - - render( - - - - ); - }); - - test('logs warning if both a config and factory are passed as props.', () => { - const outerFactory = SplitSdk(sdkBrowser); - - render( - - {({ factory }) => { - expect(factory).toBe(outerFactory); - return null; - }} - - ); - - expect(logSpy).toBeCalledWith(WARN_SF_CONFIG_AND_FACTORY); - logSpy.mockRestore(); - }); - - test('logs error and passes null factory if rendered without a Split config and factory.', () => { - const errorSpy = jest.spyOn(console, 'error'); - render( - - {({ factory }) => { - expect(factory).toBe(null); - return null; - }} - - ); - expect(errorSpy).toBeCalledWith(ERROR_SF_NO_CONFIG_AND_FACTORY); - }); - - test('cleans up on unmount.', () => { - let destroyMainClientSpy; - let destroySharedClientSpy; - const wrapper = render( - - {({ factory }) => { - expect(__factories.size).toBe(1); - destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy'); - return ( - - {({ client }) => { - destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy'); - return null; - }} - - ); - }} - - ); - wrapper.unmount(); - // the factory created by the component is removed from `factories` cache and its clients are destroyed - expect(__factories.size).toBe(0); - expect(destroyMainClientSpy).toBeCalledTimes(1); - expect(destroySharedClientSpy).toBeCalledTimes(1); - }); - - test('doesn\'t clean up on unmount if the factory is provided as a prop.', () => { - let destroyMainClientSpy; - let destroySharedClientSpy; - const outerFactory = SplitSdk(sdkBrowser); - const wrapper = render( - - {({ factory }) => { - // if factory is provided as a prop, `factories` cache is not modified - expect(__factories.size).toBe(0); - destroyMainClientSpy = jest.spyOn((factory as SplitIO.ISDK).client(), 'destroy'); - return ( - - {({ client }) => { - destroySharedClientSpy = jest.spyOn(client as SplitIO.IClient, 'destroy'); - return null; - }} - - ); - }} - - ); - wrapper.unmount(); - expect(destroyMainClientSpy).not.toBeCalled(); - expect(destroySharedClientSpy).not.toBeCalled(); - }); - -}); diff --git a/src/__tests__/SplitFactoryProvider.test.tsx b/src/__tests__/SplitFactoryProvider.test.tsx index ec094c0..bb6c2f5 100644 --- a/src/__tests__/SplitFactoryProvider.test.tsx +++ b/src/__tests__/SplitFactoryProvider.test.tsx @@ -2,44 +2,42 @@ import React from 'react'; import { render, act } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; +import { mockSdk, Event, getLastInstance } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; const logSpy = jest.spyOn(console, 'log'); /** Test target */ -import { ISplitFactoryChildProps } from '../types'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; -import { SplitClient } from '../SplitClient'; -import { SplitContext } from '../SplitContext'; +import { SplitContext, useSplitContext } from '../SplitContext'; import { __factories, IClientWithContext } from '../utils'; import { WARN_SF_CONFIG_AND_FACTORY } from '../constants'; +import { INITIAL_STATUS } from './testUtils/utils'; +import { useSplitClient } from '../useSplitClient'; describe('SplitFactoryProvider', () => { test('passes no-ready props to the child if initialized with a config.', () => { render( - {({ factory, client, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => { - expect(factory).toBe(null); - expect(client).toBe(null); - expect(isReady).toBe(false); - expect(isReadyFromCache).toBe(false); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe(0); + {React.createElement(() => { + const context = useSplitContext(); + expect(context).toEqual({ + ...INITIAL_STATUS, + factory: getLastInstance(SplitFactory), + client: getLastInstance(SplitFactory).client(), + }); return null; - }} + })} ); }); test('passes ready props to the child if initialized with a ready factory.', async () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); @@ -47,253 +45,21 @@ describe('SplitFactoryProvider', () => { render( - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitFactoryChildProps) => { - expect(factory).toBe(outerFactory); - expect(isReady).toBe(true); - expect(isReadyFromCache).toBe(true); - expect(hasTimedout).toBe(false); - expect(isTimedout).toBe(false); - expect(isDestroyed).toBe(false); - expect(lastUpdate).toBe((outerFactory.client() as IClientWithContext).__getStatus().lastUpdate); - expect((factory as SplitIO.ISDK).settings.version).toBe(outerFactory.settings.version); - return null; - }} - - ); - }); - - test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events (config prop)', async () => { - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Timedout - expect(statusProps).toStrictEqual([false, false, true, true]); - break; - case 2: // Ready from cache - expect(statusProps).toStrictEqual([false, true, true, true]); - break; - case 3: // Ready - expect(statusProps).toStrictEqual([true, true, true, false]); - break; - case 4: // Updated - expect(statusProps).toStrictEqual([true, true, true, false]); - break; - default: - fail('Child must not be rerendered'); - } // eslint-disable-next-line no-use-before-define - if (factory) expect(factory).toBe(innerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - const innerFactory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - - expect(renderTimes).toBe(5); - }); - - test('rerenders child on SDK_READY_TIMEDOUT, SDK_READY_FROM_CACHE, SDK_READY and SDK_UPDATE events (factory prop)', async () => { - const outerFactory = SplitSdk(sdkBrowser); - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Timedout - expect(statusProps).toStrictEqual([false, false, true, true]); - break; - case 2: // Ready from cache - expect(statusProps).toStrictEqual([false, true, true, true]); - break; - case 3: // Ready - expect(statusProps).toStrictEqual([true, true, true, false]); - break; - case 4: // Updated - expect(statusProps).toStrictEqual([true, true, true, false]); - break; - default: - fail('Child must not be rerendered'); - } - expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; + {React.createElement(() => { + const context = useSplitClient(); + + expect(context).toEqual({ + ...INITIAL_STATUS, + factory: outerFactory, + client: outerFactory.client(), + isReady: true, + isReadyFromCache: true, + lastUpdate: (outerFactory.client() as IClientWithContext).__getStatus().lastUpdate + }); return null; - }} + })} ); - - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - - expect(renderTimes).toBe(5); - }); - - test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (config prop)', async () => { - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Timedout - expect(statusProps).toStrictEqual([false, false, true, true]); - break; - case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. - expect(statusProps).toStrictEqual([true, false, true, false]); - break; - default: - fail('Child must not be rerendered'); - } // eslint-disable-next-line no-use-before-define - if (factory) expect(factory).toBe(innerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - const innerFactory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - - expect(renderTimes).toBe(3); - }); - - test('rerenders child on SDK_READY_TIMED_OUT and SDK_UPDATE events, but not on SDK_READY (factory prop)', async () => { - const outerFactory = SplitSdk(sdkBrowser); - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Timedout - expect(statusProps).toStrictEqual([false, false, true, true]); - break; - case 2: // Updated. Although `updateOnSdkReady` is false, status props must reflect the current status of the client. - expect(statusProps).toStrictEqual([true, false, true, false]); - break; - default: - fail('Child must not be rerendered'); - } - expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - - expect(renderTimes).toBe(3); - }); - - test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour (config prop)', async () => { - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Ready - expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state - break; - default: - fail('Child must not be rerendered'); - } // eslint-disable-next-line no-use-before-define - if (factory) expect(factory).toBe(innerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - const innerFactory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (innerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - expect(renderTimes).toBe(2); - }); - - test('rerenders child only on SDK_READY and SDK_READY_FROM_CACHE event, as default behaviour (factory prop)', async () => { - const outerFactory = SplitSdk(sdkBrowser); - let renderTimes = 0; - let previousLastUpdate = -1; - - render( - - {({ factory, isReady, isReadyFromCache, hasTimedout, isTimedout, lastUpdate }: ISplitFactoryChildProps) => { - const statusProps = [isReady, isReadyFromCache, hasTimedout, isTimedout]; - switch (renderTimes) { - case 0: // No ready - expect(statusProps).toStrictEqual([false, false, false, false]); - break; - case 1: // Ready - expect(statusProps).toStrictEqual([true, false, true, false]); // not rerendering on SDK_TIMEOUT, but hasTimedout reflects the current state - break; - default: - fail('Child must not be rerendered'); - } - expect(factory).toBe(outerFactory); - expect(lastUpdate).toBeGreaterThan(previousLastUpdate); - renderTimes++; - previousLastUpdate = lastUpdate; - return null; - }} - - ); - - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_READY)); - act(() => (outerFactory as any).client().__emitter__.emit(Event.SDK_UPDATE)); - expect(renderTimes).toBe(2); }); test('renders a passed JSX.Element with a new SplitContext value.', (done) => { @@ -301,11 +67,11 @@ describe('SplitFactoryProvider', () => { return ( {(value) => { - expect(value.factory).toBe(null); - expect(value.client).toBe(null); - expect(value.isReady).toBe(false); - expect(value.isTimedout).toBe(false); - expect(value.lastUpdate).toBe(0); + expect(value).toEqual({ + ...INITIAL_STATUS, + factory: getLastInstance(SplitFactory), + client: getLastInstance(SplitFactory).client(), + }); done(); return null; }} @@ -321,14 +87,13 @@ describe('SplitFactoryProvider', () => { }); test('logs warning if both a config and factory are passed as props.', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); render( - {({ factory }) => { - expect(factory).toBe(outerFactory); + {React.createElement(() => { return null; - }} + })} ); @@ -338,11 +103,12 @@ describe('SplitFactoryProvider', () => { test('cleans up on update and unmount if config prop is provided.', () => { let renderTimes = 0; - const createdFactories = new Set(); - const clientDestroySpies: jest.SpyInstance[] = []; - const outerFactory = SplitSdk(sdkBrowser); + const createdFactories = new Set(); + const factoryDestroySpies: jest.SpyInstance[] = []; + const outerFactory = SplitFactory(sdkBrowser); - const Component = ({ factory, isReady, hasTimedout }: ISplitFactoryChildProps) => { + const Component = () => { + const { factory, isReady, hasTimedout } = useSplitClient(); renderTimes++; switch (renderTimes) { @@ -353,31 +119,27 @@ describe('SplitFactoryProvider', () => { case 5: expect(isReady).toBe(false); expect(hasTimedout).toBe(false); - expect(factory).toBe(null); + expect(factory).toBe(getLastInstance(SplitFactory)); + if (!createdFactories.has(factory!)) factoryDestroySpies.push(jest.spyOn(factory!, 'destroy')); + createdFactories.add(factory!); return null; case 3: case 4: case 6: expect(isReady).toBe(true); expect(hasTimedout).toBe(true); - expect(factory).not.toBe(null); + expect(factory).toBe(getLastInstance(SplitFactory)); + if (!createdFactories.has(factory!)) factoryDestroySpies.push(jest.spyOn(factory!, 'destroy')); createdFactories.add(factory!); - clientDestroySpies.push(jest.spyOn(factory!.client(), 'destroy')); - return ( - - {({ client }) => { - clientDestroySpies.push(jest.spyOn(client!, 'destroy')); - return null; - }} - - ); + return null; case 7: throw new Error('Must not rerender'); } + return null; }; const emitSdkEvents = () => { - const factory = (SplitSdk as jest.Mock).mock.results.slice(-1)[0].value; + const factory = getLastInstance(SplitFactory); factory.client().__emitter__.emit(Event.SDK_READY_TIMED_OUT) factory.client().__emitter__.emit(Event.SDK_READY) }; @@ -385,72 +147,92 @@ describe('SplitFactoryProvider', () => { // 1st render: factory provided const wrapper = render( - {Component} + ); - // 2nd render: factory created, not ready (null) + // 2nd render: factory created, not ready wrapper.rerender( - {Component} + ); - // 3rd render: SDK ready (timeout is ignored due to updateOnSdkTimedout=false) + // 3rd render: SDK timeout and ready events emitted (only one re-render due to batched state updates in React) act(emitSdkEvents); // 4th render: same config prop -> factory is not recreated wrapper.rerender( - - {Component} + + ); act(emitSdkEvents); // Emitting events again has no effect expect(createdFactories.size).toBe(1); - // 5th render: Update config prop -> factory is recreated, not ready yet (null) + // 5th render: Update config prop -> factory is recreated, not ready yet wrapper.rerender( - - {Component} + + ); - // 6th render: SDK timeout (ready is ignored due to updateOnSdkReady=false) + // 6th render: SDK events emitted act(emitSdkEvents); wrapper.unmount(); - // Created factories are removed from `factories` cache and their clients are destroyed - expect(createdFactories.size).toBe(2); + // Created factories are removed from `factories` cache and `destroy` method is called expect(__factories.size).toBe(0); - clientDestroySpies.forEach(spy => expect(spy).toBeCalledTimes(1)); + expect(createdFactories.size).toBe(2); + expect(factoryDestroySpies.length).toBe(2); + factoryDestroySpies.forEach(spy => expect(spy).toBeCalledTimes(1)); }); test('doesn\'t clean up on unmount if the factory is provided as a prop.', () => { - let destroyMainClientSpy; - let destroySharedClientSpy; - const outerFactory = SplitSdk(sdkBrowser); + let destroySpy; + const outerFactory = SplitFactory(sdkBrowser); const wrapper = render( - {({ factory }) => { + {React.createElement(() => { + const { factory } = useSplitClient(); // if factory is provided as a prop, `factories` cache is not modified expect(__factories.size).toBe(0); - destroyMainClientSpy = jest.spyOn(factory!.client(), 'destroy'); - return ( - - {({ client }) => { - destroySharedClientSpy = jest.spyOn(client!, 'destroy'); - return null; - }} - - ); - }} + destroySpy = jest.spyOn(factory!, 'destroy'); + return null; + })} ); wrapper.unmount(); - expect(destroyMainClientSpy).not.toBeCalled(); - expect(destroySharedClientSpy).not.toBeCalled(); + expect(destroySpy).not.toBeCalled(); + }); + + test('passes attributes to the main client if provided.', () => { + (SplitFactory as jest.Mock).mockClear(); + let client; + + const Component = () => { + client = useSplitContext().client; + return null; + } + + const wrapper = render( + + + + ); + + expect(client.getAttributes()).toEqual({ attr1: 'value1' }); + + wrapper.rerender( + + + + ); + + expect(client.getAttributes()).toEqual({ attr1: 'value2' }); + expect(SplitFactory).toBeCalledTimes(1); }); }); diff --git a/src/__tests__/SplitTreatments.test.tsx b/src/__tests__/SplitTreatments.test.tsx index 6c39c23..b94b477 100644 --- a/src/__tests__/SplitTreatments.test.tsx +++ b/src/__tests__/SplitTreatments.test.tsx @@ -2,15 +2,15 @@ import React from 'react'; import { render, RenderResult, act } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; import { getStatus, IClientWithContext } from '../utils'; import { newSplitFactoryLocalhostInstance } from './testUtils/utils'; -import { CONTROL_WITH_CONFIG } from '../constants'; +import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants'; /** Test target */ import { ISplitTreatmentsChildProps, ISplitTreatmentsProps, ISplitClientProps } from '../types'; @@ -31,78 +31,78 @@ describe('SplitTreatments', () => { it('passes control treatments (empty object if flagSets is provided) if the SDK is not operational.', () => { render( - {() => { - return ( - - - {({ treatments }: ISplitTreatmentsChildProps) => { - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); - return null; - }} - - - {({ treatments }: ISplitTreatmentsChildProps) => { - expect(treatments).toEqual({}); - return null; - }} - - - ); - }} + + {() => { + return ( +
+ + {({ treatments }: ISplitTreatmentsChildProps) => { + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + expect(treatments).toEqual({}); + return null; + }} + +
+ ); + }} +
); }); it('passes as treatments prop the value returned by the method "client.getTreatmentsWithConfig(ByFlagSets)" if the SDK is ready.', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); render( - {({ factory, isReady }) => { - expect(getStatus(outerFactory.client()).isReady).toBe(isReady); - expect(isReady).toBe(true); - return ( - <> - - {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { - const clientMock: any = factory?.client(); - expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1); - expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value); - expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]); - expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, (outerFactory.client() as IClientWithContext).__getStatus().lastUpdate]); - return null; - }} - - - {({ treatments }: ISplitTreatmentsChildProps) => { - const clientMock: any = factory?.client(); - expect(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls.length).toBe(1); - expect(treatments).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.results[0].value); - expect(flagSets).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls[0][0]); - return null; - }} - - - ); - }} + + {({ factory, isReady }) => { + expect(getStatus(outerFactory.client()).isReady).toBe(isReady); + expect(isReady).toBe(true); + return ( + <> + + {({ treatments, isReady: isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + expect(clientMock.getTreatmentsWithConfig.mock.calls.length).toBe(1); + expect(treatments).toBe(clientMock.getTreatmentsWithConfig.mock.results[0].value); + expect(featureFlagNames).toBe(clientMock.getTreatmentsWithConfig.mock.calls[0][0]); + expect([isReady2, isReadyFromCache, hasTimedout, isTimedout, isDestroyed, lastUpdate]).toStrictEqual([true, false, false, false, false, (outerFactory.client() as IClientWithContext).__getStatus().lastUpdate]); + return null; + }} + + + {({ treatments }: ISplitTreatmentsChildProps) => { + const clientMock: any = factory?.client(); + expect(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls.length).toBe(1); + expect(treatments).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.results[0].value); + expect(flagSets).toBe(clientMock.getTreatmentsWithConfigByFlagSets.mock.calls[0][0]); + return null; + }} + + + ); + }} + ); }); - // @TODO Update test in breaking change, following common practice in React libraries, like React-redux and React-query: use a falsy value as default context value, and throw an error – instead of logging it – if components are not wrapped in a SplitContext.Provider, i.e., if the context is falsy. - // it('logs error and passes control treatments if rendered outside an SplitProvider component.', () => { - // render( - // - // {({ treatments }: ISplitTreatmentsChildProps) => { - // expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); - // return null; - // }} - // - // ); - - // expect(logSpy).toBeCalledWith(WARN_ST_NO_CLIENT); - // }); + test('throws error if invoked outside of SplitFactoryProvider.', () => { + expect(() => { + render( + + {() => null} + + ); + }).toThrow(EXCEPTION_NO_SFP); + }); /** * Input validation. Passing invalid feature flag names or attributes while the Sdk @@ -111,33 +111,35 @@ describe('SplitTreatments', () => { it('Input validation: invalid "names" and "attributes" props in SplitTreatments.', (done) => { render( - {() => { - return ( - <> - {/* @ts-expect-error Test error handling */} - - {({ treatments }: ISplitTreatmentsChildProps) => { - expect(treatments).toEqual({}); - return null; - }} - - {/* @ts-expect-error Test error handling */} - - {({ treatments }: ISplitTreatmentsChildProps) => { - expect(treatments).toEqual({}); - return null; - }} - - {/* @ts-expect-error Test error handling */} - - {({ treatments }: ISplitTreatmentsChildProps) => { - expect(treatments).toEqual({}); - return null; - }} - - - ); - }} + + {() => { + return ( + <> + {/* @ts-expect-error Test error handling */} + + {({ treatments }: ISplitTreatmentsChildProps) => { + expect(treatments).toEqual({}); + return null; + }} + + {/* @ts-expect-error Test error handling */} + + {({ treatments }: ISplitTreatmentsChildProps) => { + expect(treatments).toEqual({}); + return null; + }} + + {/* @ts-expect-error Test error handling */} + + {({ treatments }: ISplitTreatmentsChildProps) => { + expect(treatments).toEqual({}); + return null; + }} + + + ); + }} + ); expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.'); @@ -149,17 +151,47 @@ describe('SplitTreatments', () => { test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { render( - // @ts-expect-error flagSets and names are mutually exclusive - - {({ treatments }) => { - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); - return null; - }} - + + {/* @ts-expect-error flagSets and names are mutually exclusive */} + + {({ treatments }) => { + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + return null; + }} + + ); expect(logSpy).toBeCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); }); + + test('returns the treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { + const outerFactory = SplitFactory(sdkBrowser); + const client: any = outerFactory.client('user2'); + const attributes = { att1: 'att1' }; + let treatments: SplitIO.TreatmentsWithConfig; + + render( + + + {React.createElement(() => { + treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + return null; + })} + + + ); + + // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method + expect(client.getTreatmentsWithConfig).not.toBeCalled(); + expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG, split2: CONTROL_WITH_CONFIG }); + + // once operational (SDK_READY_FROM_CACHE), it evaluates feature flags + act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + + expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); + expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); + }); }); let renderTimes = 0; @@ -184,7 +216,7 @@ describe.each([ return null; } ])('SplitTreatments & useSplitTreatments optimization', (InnerComponent) => { - let outerFactory = SplitSdk(sdkBrowser); + let outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); function Component({ names, flagSets, attributes, splitKey, clientAttributes }: { @@ -301,14 +333,14 @@ describe.each([ it('rerenders and re-evaluate feature flags when Split context changes (in both SplitFactoryProvider and SplitClient components).', async () => { // changes in SplitContext implies that either the factory, the client (user key), or its status changed, what might imply a change in treatments - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); let renderTimesComp1 = 0; let renderTimesComp2 = 0; // test context updates on SplitFactoryProvider render( - - + + {() => { renderTimesComp1++; return null; diff --git a/src/__tests__/index.test.ts b/src/__tests__/index.test.ts index b0b3538..a455af6 100644 --- a/src/__tests__/index.test.ts +++ b/src/__tests__/index.test.ts @@ -1,7 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { SplitContext as ExportedSplitContext, - SplitSdk as ExportedSplitSdk, SplitFactory as ExportedSplitFactory, SplitFactoryProvider as ExportedSplitFactoryProvider, SplitClient as ExportedSplitClient, @@ -9,10 +8,7 @@ import { withSplitFactory as exportedWithSplitFactory, withSplitClient as exportedWithSplitClient, withSplitTreatments as exportedWithSplitTreatments, - useClient as exportedUseClient, - useManager as exportedUseManager, useTrack as exportedUseTrack, - useTreatments as exportedUseTreatments, useSplitClient as exportedUseSplitClient, useSplitTreatments as exportedUseSplitTreatments, useSplitManager as exportedUseSplitManager, @@ -22,27 +18,24 @@ import { ISplitClientProps, ISplitContextValues, ISplitFactoryChildProps, - ISplitFactoryProps, + ISplitFactoryProviderProps, ISplitStatus, ISplitTreatmentsChildProps, ISplitTreatmentsProps, IUpdateProps, IUseSplitClientOptions, IUseSplitTreatmentsOptions, + IUseSplitManagerResult } from '../index'; import { SplitContext } from '../SplitContext'; -import { SplitFactory as SplitioEntrypoint } from '@splitsoftware/splitio/client'; -import { SplitFactory } from '../SplitFactory'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; import { SplitClient } from '../SplitClient'; import { SplitTreatments } from '../SplitTreatments'; import { withSplitFactory } from '../withSplitFactory'; import { withSplitClient } from '../withSplitClient'; import { withSplitTreatments } from '../withSplitTreatments'; -import { useClient } from '../useClient'; -import { useManager } from '../useManager'; import { useTrack } from '../useTrack'; -import { useTreatments } from '../useTreatments'; import { useSplitClient } from '../useSplitClient'; import { useSplitTreatments } from '../useSplitTreatments'; import { useSplitManager } from '../useSplitManager'; @@ -50,7 +43,6 @@ import { useSplitManager } from '../useSplitManager'; describe('index', () => { it('should export components', () => { - expect(ExportedSplitFactory).toBe(SplitFactory); expect(ExportedSplitFactoryProvider).toBe(SplitFactoryProvider); expect(ExportedSplitClient).toBe(SplitClient); expect(ExportedSplitTreatments).toBe(SplitTreatments); @@ -63,10 +55,7 @@ describe('index', () => { }); it('should export hooks', () => { - expect(exportedUseClient).toBe(useClient); - expect(exportedUseManager).toBe(useManager); expect(exportedUseTrack).toBe(useTrack); - expect(exportedUseTreatments).toBe(useTreatments); expect(exportedUseSplitClient).toBe(useSplitClient); expect(exportedUseSplitTreatments).toBe(useSplitTreatments); expect(exportedUseSplitManager).toBe(useSplitManager); @@ -76,8 +65,8 @@ describe('index', () => { expect(ExportedSplitContext).toBe(SplitContext); }); - it('should export Splitio entrypoint', () => { - expect(ExportedSplitSdk).toBe(SplitioEntrypoint); + it('should export SplitFactory', () => { + expect(ExportedSplitFactory).toBe(SplitFactory); }); }); diff --git a/src/__tests__/testUtils/mockSplitFactory.ts b/src/__tests__/testUtils/mockSplitFactory.ts new file mode 100644 index 0000000..09dca47 --- /dev/null +++ b/src/__tests__/testUtils/mockSplitFactory.ts @@ -0,0 +1,169 @@ +import { EventEmitter } from 'events'; +import jsSdkPackageJson from '@splitsoftware/splitio/package.json'; +import reactSdkPackageJson from '../../../package.json'; + +export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`; +export const reactSdkVersion = `react-${reactSdkPackageJson.version}`; + +export const Event = { + SDK_READY_TIMED_OUT: 'init::timeout', + SDK_READY: 'init::ready', + SDK_READY_FROM_CACHE: 'init::cache-ready', + SDK_UPDATE: 'state::update', +}; + +function parseKey(key: SplitIO.SplitKey): SplitIO.SplitKey { + if (key && typeof key === 'object' && key.constructor === Object) { + return { + matchingKey: (key as SplitIO.SplitKeyObject).matchingKey, + bucketingKey: (key as SplitIO.SplitKeyObject).bucketingKey, + }; + } else { + return { + matchingKey: (key as string), + bucketingKey: (key as string), + }; + } +} +function buildInstanceId(key: any, trafficType?: string) { + return `${key.matchingKey ? key.matchingKey : key}-${key.bucketingKey ? key.bucketingKey : key}-${trafficType !== undefined ? trafficType : ''}`; +} + +export function mockSdk() { + + return jest.fn((config: SplitIO.IBrowserSettings, __updateModules) => { + + function mockClient(_key: SplitIO.SplitKey) { + // Readiness + let isReady = false; + let isReadyFromCache = false; + let hasTimedout = false; + let isDestroyed = false; + let lastUpdate = 0; + + function syncLastUpdate() { + const dateNow = Date.now(); + lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1; + } + + const __emitter__ = new EventEmitter(); + __emitter__.on(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); }); + __emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); }); + + let attributesCache = {}; + + // Client methods + const track: jest.Mock = jest.fn(() => { + return true; + }); + const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => { + return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => { + result[featureName] = { treatment: 'on', config: null }; + return result; + }, {}); + }); + const getTreatmentsWithConfigByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => { + return flagSets.reduce((result: SplitIO.TreatmentsWithConfig, flagSet: string) => { + result[flagSet + '_feature_flag'] = { treatment: 'on', config: null }; + return result; + }, {}); + }); + const setAttributes: jest.Mock = jest.fn((attributes) => { + attributesCache = Object.assign(attributesCache, attributes); + return true; + }); + const clearAttributes: jest.Mock = jest.fn(() => { + attributesCache = {}; + return true; + }); + const getAttributes: jest.Mock = jest.fn(() => { + return attributesCache; + }); + const ready: jest.Mock = jest.fn(() => { + return new Promise((res, rej) => { + if (isReady) res(); + else { __emitter__.on(Event.SDK_READY, res); } + if (hasTimedout) rej(); + else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } + }); + }); + const __getStatus = () => ({ + isReady, + isReadyFromCache, + isTimedout: hasTimedout && !isReady, + hasTimedout, + isDestroyed, + isOperational: (isReady || isReadyFromCache) && !isDestroyed, + lastUpdate, + }); + const destroy: jest.Mock = jest.fn(() => { + isDestroyed = true; + syncLastUpdate(); + // __emitter__.removeAllListeners(); + return Promise.resolve(); + }); + + return Object.assign(Object.create(__emitter__), { + getTreatmentsWithConfig, + getTreatmentsWithConfigByFlagSets, + track, + ready, + destroy, + Event, + setAttributes, + clearAttributes, + getAttributes, + // EventEmitter exposed to trigger events manually + __emitter__, + // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed) + __getStatus, + // Restore the mock client to its initial NO-READY status. + // Useful when you want to reuse the same mock between tests after emitting events or destroying the instance. + __restore() { + isReady = isReadyFromCache = hasTimedout = isDestroyed = false; + lastUpdate = 0; + } + }); + } + + // Manager + const names: jest.Mock = jest.fn().mockReturnValue([]); + const manager: jest.Mock = jest.fn().mockReturnValue({ names }); + + // Cache of clients + const __clients__: { [instanceId: string]: any } = {}; + const client = jest.fn((key?: string) => { + const clientKey = key || parseKey(config.core.key); + const instanceId = buildInstanceId(clientKey); + return __clients__[instanceId] || (__clients__[instanceId] = mockClient(clientKey)); + }); + + // Factory destroy + const destroy = jest.fn(() => { + return Promise.all(Object.keys(__clients__).map(instanceId => __clients__[instanceId].destroy())); + }); + + // SDK factory + const factory = { + client, + manager, + destroy, + __names__: names, + __clients__, + settings: Object.assign({ + version: jsSdkVersion, + }, config), + }; + + if (__updateModules) __updateModules(factory); + + return factory; + }); + +} + +export function getLastInstance(SplitFactoryMock: any) { + return SplitFactoryMock.mock.results.slice(-1)[0].value; +} diff --git a/src/__tests__/testUtils/mockSplitSdk.ts b/src/__tests__/testUtils/mockSplitSdk.ts deleted file mode 100644 index e0a85ee..0000000 --- a/src/__tests__/testUtils/mockSplitSdk.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { EventEmitter } from 'events'; -import SplitIO from '@splitsoftware/splitio/types/splitio'; -import jsSdkPackageJson from '@splitsoftware/splitio/package.json'; -import reactSdkPackageJson from '../../../package.json'; - -export const jsSdkVersion = `javascript-${jsSdkPackageJson.version}`; -export const reactSdkVersion = `react-${reactSdkPackageJson.version}`; - -export const Event = { - SDK_READY_TIMED_OUT: 'init::timeout', - SDK_READY: 'init::ready', - SDK_READY_FROM_CACHE: 'init::cache-ready', - SDK_UPDATE: 'state::update', -}; - -function parseKey(key: SplitIO.SplitKey): SplitIO.SplitKey { - if (key && typeof key === 'object' && key.constructor === Object) { - return { - matchingKey: (key as SplitIO.SplitKeyObject).matchingKey, - bucketingKey: (key as SplitIO.SplitKeyObject).bucketingKey, - }; - } else { - return { - matchingKey: (key as string), - bucketingKey: (key as string), - }; - } -} -function buildInstanceId(key: any, trafficType: string | undefined) { - return `${key.matchingKey ? key.matchingKey : key}-${key.bucketingKey ? key.bucketingKey : key}-${trafficType !== undefined ? trafficType : ''}`; -} - -function mockClient(_key: SplitIO.SplitKey, _trafficType?: string) { - // Readiness - let isReady = false; - let isReadyFromCache = false; - let hasTimedout = false; - let isDestroyed = false; - let lastUpdate = 0; - - function syncLastUpdate() { - const dateNow = Date.now(); - lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1; - } - - const __emitter__ = new EventEmitter(); - __emitter__.on(Event.SDK_READY, () => { isReady = true; syncLastUpdate(); }); - __emitter__.on(Event.SDK_READY_FROM_CACHE, () => { isReadyFromCache = true; syncLastUpdate(); }); - __emitter__.on(Event.SDK_READY_TIMED_OUT, () => { hasTimedout = true; syncLastUpdate(); }); - __emitter__.on(Event.SDK_UPDATE, () => { syncLastUpdate(); }); - - let attributesCache = {}; - - // Client methods - const track: jest.Mock = jest.fn(() => { - return true; - }); - const getTreatmentsWithConfig: jest.Mock = jest.fn((featureFlagNames: string[]) => { - return featureFlagNames.reduce((result: SplitIO.TreatmentsWithConfig, featureName: string) => { - result[featureName] = { treatment: 'on', config: null }; - return result; - }, {}); - }); - const getTreatmentsWithConfigByFlagSets: jest.Mock = jest.fn((flagSets: string[]) => { - return flagSets.reduce((result: SplitIO.TreatmentsWithConfig, flagSet: string) => { - result[flagSet + '_feature_flag'] = { treatment: 'on', config: null }; - return result; - }, {}); - }); - const setAttributes: jest.Mock = jest.fn((attributes) => { - attributesCache = Object.assign(attributesCache, attributes); - return true; - }); - const clearAttributes: jest.Mock = jest.fn(() => { - attributesCache = {}; - return true; - }); - const getAttributes: jest.Mock = jest.fn(() => { - return attributesCache; - }); - const ready: jest.Mock = jest.fn(() => { - return new Promise((res, rej) => { - if (isReady) res(); - else { __emitter__.on(Event.SDK_READY, res); } - if (hasTimedout) rej(); - else { __emitter__.on(Event.SDK_READY_TIMED_OUT, rej); } - }); - }); - const __getStatus = () => ({ - isReady, - isReadyFromCache, - isTimedout: hasTimedout && !isReady, - hasTimedout, - isDestroyed, - isOperational: (isReady || isReadyFromCache) && !isDestroyed, - lastUpdate, - }); - const destroy: jest.Mock = jest.fn(() => { - isDestroyed = true; - syncLastUpdate(); - // __emitter__.removeAllListeners(); - return Promise.resolve(); - }); - - return Object.assign(Object.create(__emitter__), { - getTreatmentsWithConfig, - getTreatmentsWithConfigByFlagSets, - track, - ready, - destroy, - Event, - setAttributes, - clearAttributes, - getAttributes, - // EventEmitter exposed to trigger events manually - __emitter__, - // Clients expose a `__getStatus` method, that is not considered part of the public API, to get client readiness status (isReady, isReadyFromCache, isOperational, hasTimedout, isDestroyed) - __getStatus, - // Restore the mock client to its initial NO-READY status. - // Useful when you want to reuse the same mock between tests after emitting events or destroying the instance. - __restore() { - isReady = isReadyFromCache = hasTimedout = isDestroyed = false; - lastUpdate = 0; - } - }); -} - -export function mockSdk() { - - return jest.fn((config: SplitIO.IBrowserSettings, __updateModules) => { - - // Manager - const names: jest.Mock = jest.fn().mockReturnValue([]); - const manager: jest.Mock = jest.fn().mockReturnValue({ names }); - - // Cache of clients - const __clients__: { [instanceId: string]: any } = {}; - const client = jest.fn((key?: string, trafficType?: string) => { - const clientKey = key || parseKey(config.core.key); - const clientTT = trafficType || config.core.trafficType; - const instanceId = buildInstanceId(clientKey, clientTT); - return __clients__[instanceId] || (__clients__[instanceId] = mockClient(clientKey, clientTT)); - }); - - // SDK factory - const factory = { - client, - manager, - __names__: names, - __clients__, - settings: Object.assign({ - version: jsSdkVersion, - }, config), - }; - - if (__updateModules) __updateModules(factory); - - return factory; - }); - -} diff --git a/src/__tests__/testUtils/sdkConfigs.ts b/src/__tests__/testUtils/sdkConfigs.ts index a3421a5..0ec97f5 100644 --- a/src/__tests__/testUtils/sdkConfigs.ts +++ b/src/__tests__/testUtils/sdkConfigs.ts @@ -1,5 +1,3 @@ -import SplitIO from '@splitsoftware/splitio/types/splitio'; - export const sdkBrowser: SplitIO.IBrowserSettings = { core: { authorizationKey: 'sdk-key', diff --git a/src/__tests__/testUtils/utils.tsx b/src/__tests__/testUtils/utils.tsx index 443f46e..36a0c5f 100644 --- a/src/__tests__/testUtils/utils.tsx +++ b/src/__tests__/testUtils/utils.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { render } from '@testing-library/react'; +import { ISplitStatus } from '../../types'; const { SplitFactory: originalSplitFactory } = jest.requireActual('@splitsoftware/splitio/client'); export interface TestComponentProps { @@ -114,3 +115,12 @@ export function testAttributesBinding(Component: React.FunctionComponent); wrapper.rerender(); } + +export const INITIAL_STATUS: ISplitStatus = { + isReady: false, + isReadyFromCache: false, + isTimedout: false, + hasTimedout: false, + lastUpdate: 0, + isDestroyed: false, +} diff --git a/src/__tests__/useClient.test.tsx b/src/__tests__/useClient.test.tsx deleted file mode 100644 index 544e843..0000000 --- a/src/__tests__/useClient.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -/** Mocks */ -const useSplitClientMock = jest.fn(); -jest.mock('../useSplitClient', () => ({ - useSplitClient: useSplitClientMock -})); - -/** Test target */ -import { useClient } from '../useClient'; - -describe('useClient', () => { - - test('calls useSplitClient with the correct arguments and returns the client.', () => { - const attributes = { someAttribute: 'someValue' }; - const client = 'client'; - useSplitClientMock.mockReturnValue({ client, isReady: false }); - - expect(useClient('someKey', 'someTrafficType', attributes)).toBe(client); - - expect(useSplitClientMock).toHaveBeenCalledTimes(1); - expect(useSplitClientMock).toHaveBeenCalledWith({ splitKey: 'someKey', trafficType: 'someTrafficType', attributes }); - }); - -}); diff --git a/src/__tests__/useManager.test.tsx b/src/__tests__/useManager.test.tsx deleted file mode 100644 index ba4c36c..0000000 --- a/src/__tests__/useManager.test.tsx +++ /dev/null @@ -1,21 +0,0 @@ -/** Mocks */ -const useSplitManagerMock = jest.fn(); -jest.mock('../useSplitManager', () => ({ - useSplitManager: useSplitManagerMock -})); - -/** Test target */ -import { useManager } from '../useManager'; - -describe('useManager', () => { - - test('calls useSplitManager with the correct arguments and returns the manager.', () => { - const manager = 'manager'; - useSplitManagerMock.mockReturnValue({ manager, isReady: false }); - - expect(useManager()).toBe(manager); - - expect(useSplitManagerMock).toHaveBeenCalledTimes(1); - }); - -}); diff --git a/src/__tests__/useSplitClient.test.tsx b/src/__tests__/useSplitClient.test.tsx index 29b894e..a1e6790 100644 --- a/src/__tests__/useSplitClient.test.tsx +++ b/src/__tests__/useSplitClient.test.tsx @@ -2,24 +2,24 @@ import React from 'react'; import { act, render } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; /** Test target */ import { useSplitClient } from '../useSplitClient'; import { SplitFactoryProvider } from '../SplitFactoryProvider'; -import { SplitClient } from '../SplitClient'; import { SplitContext } from '../SplitContext'; import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; +import { EXCEPTION_NO_SFP } from '../constants'; describe('useSplitClient', () => { test('returns the main client from the context updated by SplitFactoryProvider.', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); let client; render( @@ -32,57 +32,39 @@ describe('useSplitClient', () => { expect(client).toBe(outerFactory.client()); }); - test('returns the client from the context updated by SplitClient.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let client; - render( - - - {React.createElement(() => { - client = useSplitClient().client; - return null; - })} - - - ); - expect(client).toBe(outerFactory.client('user2')); - }); - test('returns a new client from the factory at Split context given a splitKey.', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); let client; render( {React.createElement(() => { (outerFactory.client as jest.Mock).mockClear(); - client = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client; + client = useSplitClient({ splitKey: 'user2' }).client; return null; })} ); - expect(outerFactory.client as jest.Mock).toBeCalledWith('user2', 'user'); + expect(outerFactory.client as jest.Mock).toBeCalledWith('user2'); expect(outerFactory.client as jest.Mock).toHaveReturnedWith(client); }); - test('returns null if invoked outside Split context.', () => { - let client; - let sharedClient; - render( - React.createElement(() => { - client = useSplitClient().client; - sharedClient = useSplitClient({ splitKey: 'user2', trafficType: 'user' }).client; - return null; - }) - ); - expect(client).toBe(null); - expect(sharedClient).toBe(null); + test('throws error if invoked outside of SplitFactoryProvider.', () => { + expect(() => { + render( + React.createElement(() => { + useSplitClient(); + useSplitClient({ splitKey: 'user2' }); + return null; + }) + ); + }).toThrow(EXCEPTION_NO_SFP); }); test('attributes binding test with utility', (done) => { // eslint-disable-next-line react/prop-types const InnerComponent = ({ splitKey, attributesClient, testSwitch }) => { - useSplitClient({ splitKey, trafficType: 'user', attributes: attributesClient }); + useSplitClient({ splitKey, attributes: attributesClient }); testSwitch(done, splitKey); return null; }; @@ -99,13 +81,12 @@ describe('useSplitClient', () => { }); test('must update on SDK events', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); const mainClient = outerFactory.client() as any; const user2Client = outerFactory.client('user_2') as any; - let countSplitContext = 0, countSplitClient = 0, countSplitClientUser2 = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; - let countSplitClientWithUpdate = 0, countUseSplitClientWithUpdate = 0, countSplitClientUser2WithUpdate = 0, countUseSplitClientUser2WithTimeout = 0; - let countNestedComponent = 0; + let countSplitContext = 0, countUseSplitClient = 0, countUseSplitClientUser2 = 0; + let countUseSplitClientWithoutUpdate = 0, countUseSplitClientUser2WithoutTimeout = 0; render( @@ -113,116 +94,81 @@ describe('useSplitClient', () => { {() => countSplitContext++} - - {() => { countSplitClient++; return null }} - {React.createElement(() => { - // Equivalent to - // - Using config key and traffic type: `const { client } = useSplitClient(sdkBrowser.core.key, sdkBrowser.core.trafficType, { att1: 'att1' });` - // - Disabling update props, since the wrapping SplitFactoryProvider has them enabled: `const { client } = useSplitClient(undefined, undefined, { att1: 'att1' }, { updateOnSdkReady: false, updateOnSdkReadyFromCache: false });` + // Equivalent to using config key: `const { client } = useSplitClient({ splitKey: sdkBrowser.core.key, attributes: { att1: 'att1' } });` const { client } = useSplitClient({ attributes: { att1: 'att1' } }); expect(client).toBe(mainClient); // Assert that the main client was retrieved. expect(client!.getAttributes()).toEqual({ att1: 'att1' }); // Assert that the client was retrieved with the provided attributes. countUseSplitClient++; return null; })} - - {() => { countSplitClientUser2++; return null }} - {React.createElement(() => { - const { client } = useSplitClient({ splitKey: 'user_2' }); + const { client, isReady, isReadyFromCache, hasTimedout } = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); expect(client).toBe(user2Client); + countUseSplitClientUser2++; + switch (countUseSplitClientUser2) { + case 1: // initial render + expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, false, false]); + break; + case 2: // SDK_READY_FROM_CACHE + expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, true, false]); + break; + case 3: // SDK_READY_TIMED_OUT + expect([isReady, isReadyFromCache, hasTimedout]).toEqual([false, true, true]); + break; + case 4: // SDK_READY + expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, true]); + break; + case 5: // SDK_UPDATE + expect([isReady, isReadyFromCache, hasTimedout]).toEqual([true, true, true]); + break; + default: + throw new Error('Unexpected render'); + } return null; })} - - {() => { countSplitClientWithUpdate++; return null }} - {React.createElement(() => { - useSplitClient({ splitKey: sdkBrowser.core.key, trafficType: sdkBrowser.core.trafficType, updateOnSdkUpdate: true }).client; - countUseSplitClientWithUpdate++; + useSplitClient({ splitKey: sdkBrowser.core.key, updateOnSdkUpdate: false }).client; + countUseSplitClientWithoutUpdate++; return null; })} - - {() => { countSplitClientUser2WithUpdate++; return null }} - {React.createElement(() => { - useSplitClient({ splitKey: 'user_2', updateOnSdkTimedout: true }); - countUseSplitClientUser2WithTimeout++; + useSplitClient({ splitKey: 'user_2', updateOnSdkTimedout: false }); + countUseSplitClientUser2WithoutTimeout++; return null; })} - - {React.createElement(() => { - const status = useSplitClient({ splitKey: 'user_2', updateOnSdkUpdate: true }); - expect(status.client).toBe(user2Client); - - // useSplitClient doesn't re-render twice if it is in the context of a SplitClient with same user key and there is a SDK event - countNestedComponent++; - switch (countNestedComponent) { - case 1: - expect(status.isReady).toBe(false); - expect(status.isReadyFromCache).toBe(false); - break; - case 2: - expect(status.isReady).toBe(false); - expect(status.isReadyFromCache).toBe(true); - break; - case 3: - expect(status.isReady).toBe(true); - expect(status.isReadyFromCache).toBe(true); - break; - case 4: - break; - default: - throw new Error('Unexpected render'); - } - return null; - })} - ); act(() => mainClient.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - act(() => mainClient.__emitter__.emit(Event.SDK_READY)); - act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); act(() => user2Client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => mainClient.__emitter__.emit(Event.SDK_READY)); act(() => user2Client.__emitter__.emit(Event.SDK_READY_TIMED_OUT)); act(() => user2Client.__emitter__.emit(Event.SDK_READY)); + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); - // SplitContext renders 3 times: initially, when ready from cache, and when ready. - expect(countSplitContext).toEqual(3); - - // If SplitClient and useSplitClient retrieve the same client than the context and have default update options, - // they render when the context renders. - expect(countSplitClient).toEqual(countSplitContext); - expect(countUseSplitClient).toEqual(countSplitContext); + // SplitFactoryProvider renders once + expect(countSplitContext).toEqual(1); - // If SplitClient and useSplitClient retrieve a different client than the context and have default update options, - // they render when the context renders and when the new client is ready and ready from cache. - expect(countSplitClientUser2).toEqual(countSplitContext + 2); - expect(countUseSplitClientUser2).toEqual(countSplitContext + 2); + // If useSplitClient retrieves the main client and have default update options, it re-renders for each main client event. + expect(countUseSplitClient).toEqual(4); - // If SplitClient and useSplitClient retrieve the same client than the context and have updateOnSdkUpdate = true, - // they render when the context renders and when the client updates. - expect(countSplitClientWithUpdate).toEqual(countSplitContext + 1); - expect(countUseSplitClientWithUpdate).toEqual(countSplitContext + 1); + // If useSplitClient retrieves a different client and have default update options, it re-renders for each event of the new client. + expect(countUseSplitClientUser2).toEqual(5); - // If SplitClient and useSplitClient retrieve a different client than the context and have updateOnSdkUpdate = true, - // they render when the context renders and when the new client is ready, ready from cache and updates. - expect(countSplitClientUser2WithUpdate).toEqual(countSplitContext + 3); - expect(countUseSplitClientUser2WithTimeout).toEqual(countSplitContext + 3); + // If useSplitClient retrieves the main client and have updateOnSdkUpdate = false, it doesn't render when the main client updates. + expect(countUseSplitClientWithoutUpdate).toEqual(3); - expect(countNestedComponent).toEqual(4); + // If useSplitClient retrieves a different client and have updateOnSdkTimedout = false, it doesn't render when the the new client times out. + expect(countUseSplitClientUser2WithoutTimeout).toEqual(4); }); // Remove this test once side effects are moved to the useSplitClient effect. - test('must update on SDK events between the render phase (hook call) and commit phase (effect call)', () =>{ - const outerFactory = SplitSdk(sdkBrowser); + test('must update on SDK events between the render phase (hook call) and commit phase (effect call)', () => { + const outerFactory = SplitFactory(sdkBrowser); let count = 0; render( @@ -244,7 +190,7 @@ describe('useSplitClient', () => { }); test('must support changes in update props', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); const mainClient = outerFactory.client() as any; let rendersCount = 0; @@ -263,13 +209,13 @@ describe('useSplitClient', () => { ) } - const wrapper = render(); + const wrapper = render(); expect(rendersCount).toBe(1); act(() => mainClient.__emitter__.emit(Event.SDK_READY)); // trigger re-render expect(rendersCount).toBe(2); - act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false by default + act(() => mainClient.__emitter__.emit(Event.SDK_UPDATE)); // do not trigger re-render because updateOnSdkUpdate is false expect(rendersCount).toBe(2); wrapper.rerender(); // trigger re-render diff --git a/src/__tests__/useSplitManager.test.tsx b/src/__tests__/useSplitManager.test.tsx index 4705a48..cd6d497 100644 --- a/src/__tests__/useSplitManager.test.tsx +++ b/src/__tests__/useSplitManager.test.tsx @@ -2,22 +2,25 @@ import React from 'react'; import { act, render } from '@testing-library/react'; /** Mocks */ -import { Event, mockSdk } from './testUtils/mockSplitSdk'; +import { Event, mockSdk } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; import { getStatus } from '../utils'; /** Test target */ import { SplitFactoryProvider } from '../SplitFactoryProvider'; +import { SplitClient } from '../SplitClient'; import { useSplitManager } from '../useSplitManager'; +import { EXCEPTION_NO_SFP } from '../constants'; +import { INITIAL_STATUS } from './testUtils/utils'; describe('useSplitManager', () => { - test('returns the factory manager from the Split context, and updates when the context changes.', () => { - const outerFactory = SplitSdk(sdkBrowser); + test('returns the factory manager and its status, and updates on SDK events.', () => { + const outerFactory = SplitFactory(sdkBrowser); let hookResult; render( @@ -32,12 +35,7 @@ describe('useSplitManager', () => { manager: outerFactory.manager(), client: outerFactory.client(), factory: outerFactory, - hasTimedout: false, - isDestroyed: false, - isReady: false, - isReadyFromCache: false, - isTimedout: false, - lastUpdate: 0, + ...INITIAL_STATUS, }); act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY)); @@ -55,25 +53,52 @@ describe('useSplitManager', () => { }); }); - test('returns null if invoked outside Split context.', () => { + test('throws error if invoked outside of SplitFactoryProvider.', () => { + expect(() => { + render( + React.createElement(() => { + useSplitManager(); + return null; + }) + ); + }).toThrow(EXCEPTION_NO_SFP); + }); + + // @TODO remove next test case when `SplitClient` is removed. + test('returns the factory manager and its status, even if the Split context was updated by an SplitClient component', () => { + const outerFactory = SplitFactory(sdkBrowser); let hookResult; render( - React.createElement(() => { - hookResult = useSplitManager(); - return null; - }) + + + {React.createElement(() => { + hookResult = useSplitManager(); + return null; + })} + + ); expect(hookResult).toStrictEqual({ - manager: null, - client: null, - factory: null, + manager: outerFactory.manager(), + client: outerFactory.client(), + factory: outerFactory, + ...INITIAL_STATUS, + }); + + act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY)); + // act(() => (outerFactory.client() as any).__emitter__.emit(Event.SDK_READY)); + + expect(hookResult).toStrictEqual({ + manager: outerFactory.manager(), + client: outerFactory.client(), + factory: outerFactory, hasTimedout: false, isDestroyed: false, - isReady: false, + isReady: true, isReadyFromCache: false, isTimedout: false, - lastUpdate: 0, + lastUpdate: getStatus(outerFactory.client()).lastUpdate, }); }); diff --git a/src/__tests__/useSplitTreatments.test.tsx b/src/__tests__/useSplitTreatments.test.tsx index 13a54b6..06b57b2 100644 --- a/src/__tests__/useSplitTreatments.test.tsx +++ b/src/__tests__/useSplitTreatments.test.tsx @@ -2,19 +2,17 @@ import React from 'react'; import { act, render } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; -import { CONTROL_WITH_CONFIG } from '../constants'; +import { CONTROL_WITH_CONFIG, EXCEPTION_NO_SFP } from '../constants'; /** Test target */ import { SplitFactoryProvider } from '../SplitFactoryProvider'; -import { SplitClient } from '../SplitClient'; import { useSplitTreatments } from '../useSplitTreatments'; -import { SplitTreatments } from '../SplitTreatments'; import { SplitContext } from '../SplitContext'; import { ISplitTreatmentsChildProps } from '../types'; @@ -26,8 +24,8 @@ describe('useSplitTreatments', () => { const flagSets = ['set1']; const attributes = { att1: 'att1' }; - test('returns the treatments evaluated by the client at Split context, or control if the client is not operational.', () => { - const outerFactory = SplitSdk(sdkBrowser); + test('returns the treatments evaluated by the main client of the factory at Split context, or control if the client is not operational.', () => { + const outerFactory = SplitFactory(sdkBrowser); const client: any = outerFactory.client(); let treatments: SplitIO.TreatmentsWithConfig; let treatmentsByFlagSets: SplitIO.TreatmentsWithConfig; @@ -63,42 +61,15 @@ describe('useSplitTreatments', () => { expect(client.getTreatmentsWithConfigByFlagSets).toHaveReturnedWith(treatmentsByFlagSets); }); - test('returns the treatments from the client at Split context updated by SplitClient, or control if the client is not operational.', async () => { - const outerFactory = SplitSdk(sdkBrowser); - const client: any = outerFactory.client('user2'); - let treatments: SplitIO.TreatmentsWithConfig; - - render( - - - {React.createElement(() => { - treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; - return null; - })} - - - ); - - // returns control treatment if not operational (SDK not ready or destroyed), without calling `getTreatmentsWithConfig` method - expect(client.getTreatmentsWithConfig).not.toBeCalled(); - expect(treatments!).toEqual({ split1: CONTROL_WITH_CONFIG }); - - // once operational (SDK_READY_FROM_CACHE), it evaluates feature flags - act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); - - expect(client.getTreatmentsWithConfig).toBeCalledWith(featureFlagNames, attributes); - expect(client.getTreatmentsWithConfig).toHaveReturnedWith(treatments); - }); - test('returns the treatments from a new client given a splitKey, and re-evaluates on SDK events.', () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); const client: any = outerFactory.client('user2'); let renderTimes = 0; render( {React.createElement(() => { - const treatments = useSplitTreatments({ names: featureFlagNames, attributes, splitKey: 'user2' }).treatments; + const treatments = useSplitTreatments({ names: featureFlagNames, attributes, splitKey: 'user2', updateOnSdkUpdate: false }).treatments; renderTimes++; switch (renderTimes) { @@ -124,22 +95,20 @@ describe('useSplitTreatments', () => { act(() => client.__emitter__.emit(Event.SDK_READY_FROM_CACHE)); act(() => client.__emitter__.emit(Event.SDK_READY)); - act(() => client.__emitter__.emit(Event.SDK_UPDATE)); // should not trigger a re-render by default + act(() => client.__emitter__.emit(Event.SDK_UPDATE)); expect(client.getTreatmentsWithConfig).toBeCalledTimes(2); }); - // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' - test('returns control treatments (empty object if flagSets is provided) if invoked outside Split context.', () => { - render( - React.createElement(() => { - const treatments = useSplitTreatments({ names: featureFlagNames, attributes }).treatments; - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); - - const treatmentsByFlagSets = useSplitTreatments({ flagSets: featureFlagNames }).treatments; - expect(treatmentsByFlagSets).toEqual({}); - return null; - }) - ); + test('throws error if invoked outside of SplitFactoryProvider.', () => { + expect(() => { + render( + React.createElement(() => { + useSplitTreatments({ names: featureFlagNames, attributes }).treatments; + useSplitTreatments({ flagSets: featureFlagNames }).treatments; + return null; + }) + ); + }).toThrow(EXCEPTION_NO_SFP); }); /** @@ -148,27 +117,33 @@ describe('useSplitTreatments', () => { */ test('Input validation: invalid "names" and "attributes" params in useSplitTreatments.', () => { render( - React.createElement(() => { - // @ts-expect-error Test error handling - let treatments = useSplitTreatments('split1').treatments; - expect(treatments).toEqual({}); - // @ts-expect-error Test error handling - treatments = useSplitTreatments({ names: [true] }).treatments; - expect(treatments).toEqual({}); - - return null; - }) + + { + React.createElement(() => { + // @ts-expect-error Test error handling + let treatments = useSplitTreatments('split1').treatments; + expect(treatments).toEqual({}); + // @ts-expect-error Test error handling + treatments = useSplitTreatments({ names: [true] }).treatments; + expect(treatments).toEqual({}); + + return null; + }) + } + ); expect(logSpy).toBeCalledWith('[ERROR] feature flag names must be a non-empty array.'); expect(logSpy).toBeCalledWith('[ERROR] you passed an invalid feature flag name, feature flag name must be a non-empty string.'); }); test('useSplitTreatments must update on SDK events', async () => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); const mainClient = outerFactory.client() as any; const user2Client = outerFactory.client('user_2') as any; - let countSplitContext = 0, countSplitTreatments = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithUpdate = 0; + let countSplitContext = 0, countUseSplitTreatments = 0, countUseSplitTreatmentsUser2 = 0, countUseSplitTreatmentsUser2WithoutUpdate = 0; + const lastUpdateSetUser2 = new Set(); + const lastUpdateSetUser2WithUpdate = new Set(); function validateTreatments({ treatments, isReady, isReadyFromCache }: ISplitTreatmentsChildProps) { if (isReady || isReadyFromCache) { @@ -194,9 +169,6 @@ describe('useSplitTreatments', () => { {() => countSplitContext++} - - {() => { countSplitTreatments++; return null }} - {React.createElement(() => { const context = useSplitTreatments({ names: ['split_test'], attributes: { att1: 'att1' } }); expect(context.client).toBe(mainClient); // Assert that the main client was retrieved. @@ -208,14 +180,16 @@ describe('useSplitTreatments', () => { const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2' }); expect(context.client).toBe(user2Client); validateTreatments(context); + lastUpdateSetUser2.add(context.lastUpdate); countUseSplitTreatmentsUser2++; return null; })} {React.createElement(() => { - const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: true }); + const context = useSplitTreatments({ names: ['split_test'], splitKey: 'user_2', updateOnSdkUpdate: false }); expect(context.client).toBe(user2Client); validateTreatments(context); - countUseSplitTreatmentsUser2WithUpdate++; + lastUpdateSetUser2WithUpdate.add(context.lastUpdate); + countUseSplitTreatmentsUser2WithoutUpdate++; return null; })} @@ -229,31 +203,36 @@ describe('useSplitTreatments', () => { act(() => user2Client.__emitter__.emit(Event.SDK_READY)); act(() => user2Client.__emitter__.emit(Event.SDK_UPDATE)); - // SplitContext renders 3 times: initially, when ready from cache, and when ready. - expect(countSplitContext).toEqual(3); + // SplitFactoryProvider renders once + expect(countSplitContext).toEqual(1); - // SplitTreatments and useSplitTreatments render when the context renders. - expect(countSplitTreatments).toEqual(countSplitContext); - expect(countUseSplitTreatments).toEqual(countSplitContext); - expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(4); + // If useSplitTreatments evaluates with the main client and have default update options, it re-renders for each main client event. + expect(countUseSplitTreatments).toEqual(4); + expect(mainClient.getTreatmentsWithConfig).toHaveBeenCalledTimes(3); // when ready from cache, ready and update expect(mainClient.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], { att1: 'att1' }); - // If useSplitTreatments uses a different client than the context one, it renders when the context renders and when the new client is ready and ready from cache. - expect(countUseSplitTreatmentsUser2).toEqual(countSplitContext + 2); - // If it is used with `updateOnSdkUpdate: true`, it also renders when the client emits an SDK_UPDATE event. - expect(countUseSplitTreatmentsUser2WithUpdate).toEqual(countSplitContext + 3); - expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); + // If useSplitTreatments evaluates with a different client and have default update options, it re-renders for each event of the new client. + expect(countUseSplitTreatmentsUser2).toEqual(4); + expect(lastUpdateSetUser2.size).toEqual(4); + // If it is used with `updateOnSdkUpdate: false`, it doesn't render when the client emits an SDK_UPDATE event. + expect(countUseSplitTreatmentsUser2WithoutUpdate).toEqual(3); + expect(lastUpdateSetUser2WithUpdate.size).toEqual(3); + expect(user2Client.getTreatmentsWithConfig).toHaveBeenCalledTimes(5); // when ready from cache x2, ready x2 and update x1 expect(user2Client.getTreatmentsWithConfig).toHaveBeenLastCalledWith(['split_test'], undefined); }); test('ignores flagSets and logs a warning if both names and flagSets params are provided.', () => { render( - React.createElement(() => { - // @ts-expect-error names and flagSets are mutually exclusive - const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; - expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); - return null; - }) + + { + React.createElement(() => { + // @ts-expect-error names and flagSets are mutually exclusive + const treatments = useSplitTreatments({ names: featureFlagNames, flagSets, attributes }).treatments; + expect(treatments).toEqual({ split1: CONTROL_WITH_CONFIG }); + return null; + }) + } + ); expect(logSpy).toHaveBeenLastCalledWith('[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'); diff --git a/src/__tests__/useTrack.test.tsx b/src/__tests__/useTrack.test.tsx index 4447189..28a035c 100644 --- a/src/__tests__/useTrack.test.tsx +++ b/src/__tests__/useTrack.test.tsx @@ -1,18 +1,19 @@ -import React from 'react'; -import { render } from '@testing-library/react'; +import React, { useEffect } from 'react'; +import { render, act } from '@testing-library/react'; /** Mocks */ -import { mockSdk } from './testUtils/mockSplitSdk'; +import { mockSdk, Event, getLastInstance } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; /** Test target */ import { SplitFactoryProvider } from '../SplitFactoryProvider'; -import { SplitClient } from '../SplitClient'; import { useTrack } from '../useTrack'; +import { useSplitClient } from '../useSplitClient'; +import { EXCEPTION_NO_SFP } from '../constants'; describe('useTrack', () => { @@ -21,76 +22,114 @@ describe('useTrack', () => { const value = 10; const properties = { prop1: 'prop1' }; - test('returns the track method bound to the client at Split context updated by SplitFactoryProvider.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let boundTrack; + test('returns the track method of the main client of the factory at Split context provided by SplitFactoryProvider.', () => { + const outerFactory = SplitFactory(sdkBrowser); + let clientTrack; let trackResult; render( {React.createElement(() => { - boundTrack = useTrack(); - trackResult = boundTrack(tt, eventType, value, properties); + clientTrack = useTrack(); + trackResult = clientTrack(tt, eventType, value, properties); + + const sameTrack = useTrack(sdkBrowser.core.key); + expect(clientTrack).toBe(sameTrack); return null; })} , ); - const track = outerFactory.client().track as jest.Mock; + const track = outerFactory.client().track; + expect(track).toBe(clientTrack); expect(track).toBeCalledWith(tt, eventType, value, properties); expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method bound to the client at Split context updated by SplitClient.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let boundTrack; + test('returns the track method of a new client given a splitKey.', () => { + const outerFactory = SplitFactory(sdkBrowser); let trackResult; render( - - {React.createElement(() => { - boundTrack = useTrack(); - trackResult = boundTrack(tt, eventType, value, properties); - return null; - })} - - + {React.createElement(() => { + const clientTrack = useTrack('user2'); + trackResult = clientTrack(tt, eventType, value, properties); + return null; + })} + , ); - const track = outerFactory.client('user2').track as jest.Mock; + const track = outerFactory.client('user2').track; expect(track).toBeCalledWith(tt, eventType, value, properties); expect(track).toHaveReturnedWith(trackResult); }); - test('returns the track method bound to a new client given a splitKey and optional trafficType.', () => { - const outerFactory = SplitSdk(sdkBrowser); - let boundTrack; - let trackResult; + test('throws error if invoked outside of SplitFactoryProvider.', () => { + expect(() => { + render( + React.createElement(() => { + const track = useTrack('user2'); + track(tt, eventType, value, properties); + return null; + }), + ); + }).toThrow(EXCEPTION_NO_SFP); + }); + + test('returns the track method of the client at Split context updated by SplitFactoryProvider (config prop).', () => { + const InnerComponent = ({ splitKey }: { splitKey?: string }) => { + const clientTrack = useTrack(splitKey); + + const { client } = useSplitClient({ splitKey }); + expect(clientTrack).toBe(client!.track); + + clientTrack(tt, eventType, value, properties); + + useEffect(() => { + clientTrack(tt, eventType, value, properties); + }, [clientTrack]); + return null; + } + const App = ({ splitKey }: { splitKey?: string }) => { + return ( + + ) + }; + const wrapper = render(); + + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY)); + + wrapper.rerender(); // `clientTrack` dependency changed + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_UPDATE)); + + let track = getLastInstance(SplitFactory).client().track; + expect(track).toBeCalledWith(tt, eventType, value, properties); + expect(track).toBeCalledTimes(4); // 3 from render + 1 from useEffect + + track = getLastInstance(SplitFactory).client('user2').track; + expect(track).toBeCalledWith(tt, eventType, value, properties); + expect(track).toBeCalledTimes(2); // 1 from render + 1 from useEffect (`clientTrack` dependency changed) + }); + + test('does not re-render on SDK events', () => { render( - + {React.createElement(() => { - boundTrack = useTrack('user2', tt); - trackResult = boundTrack(eventType, value, properties); + const clientTrack = useTrack(); + clientTrack(tt, eventType, value, properties); + return null; })} , ); - const track = outerFactory.client('user2', tt).track as jest.Mock; - expect(track).toBeCalledWith(eventType, value, properties); - expect(track).toHaveReturnedWith(trackResult); - }); - // THE FOLLOWING TEST WILL PROBABLE BE CHANGED BY 'return a null value or throw an error if it is not inside an SplitProvider' - test('returns a false function (`() => false`) if invoked outside Split context.', () => { - let trackResult; - render( - React.createElement(() => { - const track = useTrack('user2', tt); - trackResult = track(eventType, value, properties); - return null; - }), - ); - expect(trackResult).toBe(false); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY_TIMED_OUT)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY_FROM_CACHE)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_READY)); + act(() => getLastInstance(SplitFactory).client().__emitter__.emit(Event.SDK_UPDATE)); + + expect(getLastInstance(SplitFactory).client().track).toBeCalledTimes(1); }); }); diff --git a/src/__tests__/useTreatments.test.tsx b/src/__tests__/useTreatments.test.tsx deleted file mode 100644 index b375d05..0000000 --- a/src/__tests__/useTreatments.test.tsx +++ /dev/null @@ -1,24 +0,0 @@ -/** Mocks */ -const useSplitTreatmentsMock = jest.fn(); -jest.mock('../useSplitTreatments', () => ({ - useSplitTreatments: useSplitTreatmentsMock -})); - -/** Test target */ -import { useTreatments } from '../useTreatments'; - -describe('useTreatments', () => { - - test('calls useSplitTreatments with the correct arguments and returns the treatments.', () => { - const names = ['someFeature']; - const attributes = { someAttribute: 'someValue' }; - const treatments = { someFeature: { treatment: 'on', config: null } }; - useSplitTreatmentsMock.mockReturnValue({ treatments, isReady: false }); - - expect(useTreatments(names, attributes, 'someKey')).toBe(treatments); - - expect(useSplitTreatmentsMock).toHaveBeenCalledTimes(1); - expect(useSplitTreatmentsMock).toHaveBeenCalledWith({ names, attributes, splitKey: 'someKey' }); - }); - -}); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index b5ef021..3d7e32e 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -1,6 +1,5 @@ import { CONTROL_WITH_CONFIG } from '../constants'; import { getControlTreatmentsWithConfig } from '../utils'; -import SplitIO from '@splitsoftware/splitio/types/splitio'; describe('getControlTreatmentsWithConfig', () => { diff --git a/src/__tests__/withSplitClient.test.tsx b/src/__tests__/withSplitClient.test.tsx index 85ba3ad..d818389 100644 --- a/src/__tests__/withSplitClient.test.tsx +++ b/src/__tests__/withSplitClient.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); @@ -16,7 +16,7 @@ import { testAttributesBinding, TestComponentProps } from './testUtils/utils'; import { withSplitFactory } from '../withSplitFactory'; import { withSplitClient } from '../withSplitClient'; -describe('SplitClient', () => { +describe('withSplitClient', () => { test('passes no-ready props to the child if client is not ready.', () => { const Component = withSplitFactory(sdkBrowser)( @@ -70,8 +70,10 @@ describe('SplitClient', () => { const updateOnSdkTimedout = false; const updateOnSdkReady = true; const updateOnSdkReadyFromCache = false; - const Component = withSplitClient('user1')<{ outerProp1: string, outerProp2: number }>( - () => null, updateOnSdkUpdate, updateOnSdkTimedout, updateOnSdkReady, updateOnSdkReadyFromCache + const Component = withSplitFactory(sdkBrowser)( + withSplitClient('user1')<{ outerProp1: string, outerProp2: number }>( + () => null, updateOnSdkUpdate, updateOnSdkTimedout, updateOnSdkReady, updateOnSdkReadyFromCache + ) ); render(); @@ -91,7 +93,7 @@ describe('SplitClient', () => { function Component({ attributesFactory, attributesClient, splitKey, testSwitch, factory }: TestComponentProps) { const FactoryComponent = withSplitFactory(undefined, factory, attributesFactory)<{ attributesClient: SplitIO.Attributes, splitKey: any }>( ({ attributesClient, splitKey }) => { - const ClientComponent = withSplitClient(splitKey, 'user', attributesClient)( + const ClientComponent = withSplitClient(splitKey, attributesClient)( () => { testSwitch(done, splitKey); return null; diff --git a/src/__tests__/withSplitFactory.test.tsx b/src/__tests__/withSplitFactory.test.tsx index 6df93f6..98e765a 100644 --- a/src/__tests__/withSplitFactory.test.tsx +++ b/src/__tests__/withSplitFactory.test.tsx @@ -2,14 +2,14 @@ import React from 'react'; import { render } from '@testing-library/react'; /** Mocks */ -import { mockSdk, Event } from './testUtils/mockSplitSdk'; +import { mockSdk, Event } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { sdkBrowser } from './testUtils/sdkConfigs'; -import { SplitFactory } from '../SplitFactory'; -jest.mock('../SplitFactory'); +import { SplitClient } from '../SplitClient'; +jest.mock('../SplitClient'); /** Test target */ import { ISplitFactoryChildProps } from '../types'; @@ -29,7 +29,7 @@ describe('withSplitFactory', () => { }); test('passes ready props to the child if initialized with a ready factory.', (done) => { - const outerFactory = SplitSdk(sdkBrowser); + const outerFactory = SplitFactory(sdkBrowser); (outerFactory as any).client().__emitter__.emit(Event.SDK_READY); (outerFactory.manager().names as jest.Mock).mockReturnValue(['split1']); outerFactory.client().ready().then(() => { @@ -69,7 +69,7 @@ describe('withSplitFactory', () => { render(); - expect(SplitFactory).toHaveBeenLastCalledWith( + expect(SplitClient).toHaveBeenLastCalledWith( expect.objectContaining({ updateOnSdkUpdate, updateOnSdkTimedout, diff --git a/src/__tests__/withSplitTreatments.test.tsx b/src/__tests__/withSplitTreatments.test.tsx index f5d523b..b4f5d2e 100644 --- a/src/__tests__/withSplitTreatments.test.tsx +++ b/src/__tests__/withSplitTreatments.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { render } from '@testing-library/react'; /** Mocks */ -import { mockSdk } from './testUtils/mockSplitSdk'; +import { mockSdk } from './testUtils/mockSplitFactory'; jest.mock('@splitsoftware/splitio/client', () => { return { SplitFactory: mockSdk() }; }); diff --git a/src/constants.ts b/src/constants.ts index c29d802..b6716d0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -16,9 +16,6 @@ export const CONTROL_WITH_CONFIG: SplitIO.TreatmentWithConfig = { // Warning and error messages export const WARN_SF_CONFIG_AND_FACTORY: string = '[WARN] Both a config and factory props were provided to SplitFactoryProvider. Config prop will be ignored.'; -// @TODO remove with SplitFactory component in next major version. SplitFactoryProvider can accept no props and eventually only an initialState -export const ERROR_SF_NO_CONFIG_AND_FACTORY: string = '[ERROR] SplitFactory must receive either a Split config or a Split factory as props.'; - -export const EXCEPTION_NO_REACT_OR_CREATECONTEXT: string = 'React library is not available or its version is not supported. Check that it is properly installed or imported. Split SDK requires version 16.3.0+ of React.'; +export const EXCEPTION_NO_SFP: string = 'No SplitContext was set. Please ensure the component is wrapped in a SplitFactoryProvider.'; export const WARN_NAMES_AND_FLAGSETS: string = '[WARN] Both names and flagSets properties were provided. flagSets will be ignored.'; diff --git a/src/index.ts b/src/index.ts index 10a5161..431fe6e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,5 @@ // Split SDK factory (Renamed to avoid name conflict with SplitFactory component) -export { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +export { SplitFactory } from '@splitsoftware/splitio/client'; // HOC functions export { withSplitFactory } from './withSplitFactory'; @@ -9,14 +9,10 @@ export { withSplitTreatments } from './withSplitTreatments'; // Components export { SplitTreatments } from './SplitTreatments'; export { SplitClient } from './SplitClient'; -export { SplitFactory } from './SplitFactory'; export { SplitFactoryProvider } from './SplitFactoryProvider'; // Hooks -export { useClient } from './useClient'; -export { useTreatments } from './useTreatments'; export { useTrack } from './useTrack'; -export { useManager } from './useManager'; export { useSplitClient } from './useSplitClient'; export { useSplitTreatments } from './useSplitTreatments'; export { useSplitManager } from './useSplitManager'; @@ -31,11 +27,12 @@ export type { ISplitClientProps, ISplitContextValues, ISplitFactoryChildProps, - ISplitFactoryProps, + ISplitFactoryProviderProps, ISplitStatus, ISplitTreatmentsChildProps, ISplitTreatmentsProps, IUpdateProps, IUseSplitClientOptions, - IUseSplitTreatmentsOptions + IUseSplitTreatmentsOptions, + IUseSplitManagerResult } from './types'; diff --git a/src/types.ts b/src/types.ts index c6ff17d..eb8f2cd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,41 +1,40 @@ -import SplitIO from '@splitsoftware/splitio/types/splitio'; import type { ReactNode } from 'react'; /** - * Split Status interface. It represents the current readiness state of the SDK. + * Split Status interface. It represents the readiness state of an SDK client. */ export interface ISplitStatus { /** - * isReady indicates if the Split SDK client has triggered an SDK_READY event and thus is ready to be consumed. + * `isReady` indicates if the Split SDK client has triggered an `SDK_READY` event and thus is ready to be consumed. */ isReady: boolean; /** - * isReadyFromCache indicates if the Split SDK client has triggered an SDK_READY_FROM_CACHE event and thus is ready to be consumed, + * `isReadyFromCache` indicates if the Split SDK client has triggered an `SDK_READY_FROM_CACHE` event and thus is ready to be consumed, * although the data in cache might be stale. */ isReadyFromCache: boolean; /** - * isTimedout indicates if the Split SDK client has triggered an SDK_READY_TIMED_OUT event and is not ready to be consumed. + * `isTimedout` indicates if the Split SDK client has triggered an `SDK_READY_TIMED_OUT` event and is not ready to be consumed. * In other words, `isTimedout` is equivalent to `hasTimeout && !isReady`. */ isTimedout: boolean; /** - * hasTimedout indicates if the Split SDK client has ever triggered an SDK_READY_TIMED_OUT event. + * `hasTimedout` indicates if the Split SDK client has ever triggered an `SDK_READY_TIMED_OUT` event. * It's meant to keep a reference that the SDK emitted a timeout at some point, not the current state. */ hasTimedout: boolean; /** - * isDestroyed indicates if the Split SDK client has been destroyed. + * `isDestroyed` indicates if the Split SDK client has been destroyed. */ isDestroyed: boolean; /** - * Indicates when was the last status event, either SDK_READY, SDK_READY_FROM_CACHE, SDK_READY_TIMED_OUT or SDK_UPDATE. + * Indicates when was the last status event, either `SDK_READY`, `SDK_READY_FROM_CACHE`, `SDK_READY_TIMED_OUT` or `SDK_UPDATE`. */ lastUpdate: number; } @@ -48,80 +47,83 @@ export interface ISplitContextValues extends ISplitStatus { /** * Split factory instance. * - * NOTE: This property is not recommended for direct use, as better alternatives are available. + * NOTE: This property is available for accessing factory methods not covered by the library hooks, + * such as Logging configuration and User Consent. + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#logging}), + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#user-consent} */ - factory: SplitIO.IBrowserSDK | null; + factory?: SplitIO.IBrowserSDK; /** * Split client instance. * - * NOTE: This property is not recommended for direct use, as better alternatives are available. + * NOTE: This property is not recommended for direct use, as better alternatives are available: + * - `useSplitTreatments` hook to evaluate feature flags. + * - `useTrack` hook to track events. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#2-instantiate-the-sdk-and-create-a-new-split-client} */ - client: SplitIO.IBrowserClient | null; + client?: SplitIO.IBrowserClient; } /** - * Update Props interface. It defines the props used to configure what SDK events are listened to update the Split context. - * Only `SDK_UPDATE` and `SDK_READY_TIMED_OUT` are configurable. - * The `SDK_READY` event is always listened to update the Split context value 'isReady'. + * Update Props interface. It defines the props used to configure what SDK events are listened to update the component. */ export interface IUpdateProps { /** - * updateOnSdkUpdate indicates if the component will update the `SplitContext` in case of a `SDK_UPDATE` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_UPDATE. - * It's value is false by default. + * `updateOnSdkUpdate` indicates if the component will update (i.e., re-render) in case of an `SDK_UPDATE` event. + * If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_UPDATE`. + * It's value is `true` by default. */ updateOnSdkUpdate?: boolean; /** - * updateOnSdkTimedout indicates if the component will update the `SplitContext` in case of a `SDK_READY_TIMED_OUT` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_READY_TIMED_OUT. - * It's value is false by default. + * `updateOnSdkTimedout` indicates if the component will update (i.e., re-render) in case of a `SDK_READY_TIMED_OUT` event. + * If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_READY_TIMED_OUT`. + * It's value is `true` by default. */ updateOnSdkTimedout?: boolean; /** - * updateOnSdkReady indicates if the component will update the `SplitContext` in case of a `SDK_READY` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_READY. - * It's value is true by default. + * `updateOnSdkReady` indicates if the component will update (i.e., re-render) in case of a `SDK_READY` event. + * If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_READY`. + * It's value is `true` by default. */ updateOnSdkReady?: boolean; /** - * updateOnSdkReadyFromCache indicates if the component will update the `SplitContext` in case of a `SDK_READY_FROM_CACHE` event. - * If true, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on SDK_READY_FROM_CACHE. - * This params is only relevant when using 'LOCALSTORAGE' as storage type, since otherwise the event is never emitted. - * It's value is true by default. + * `updateOnSdkReadyFromCache` indicates if the component will update (i.e., re-render) in case of a `SDK_READY_FROM_CACHE` event. + * If `true`, components consuming the context (such as `SplitClient` and `SplitTreatments`) will re-render on `SDK_READY_FROM_CACHE`. + * This params is only relevant when using `'LOCALSTORAGE'` as storage type, since otherwise the event is never emitted. + * It's value is `true` by default. */ updateOnSdkReadyFromCache?: boolean; } /** - * SplitFactoryProvider Child Props interface. These are the props that the child component receives from the 'SplitFactoryProvider' component. + * Props interface for components wrapped by the `withSplitFactory` HOC. These props are provided by the HOC to the wrapped component. + * + * @deprecated `withSplitFactory` will be removed in a future major release. We recommend replacing it with the `SplitFactoryProvider` component. */ -// @TODO rename/remove next type (breaking-change) -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ISplitFactoryChildProps extends ISplitContextValues { } /** - * SplitFactoryProvider Props interface. These are the props accepted by SplitFactoryProvider component, - * used to instantiate a factory and client instance, update the Split context, and listen for SDK events. + * SplitFactoryProvider Props interface. These are the props accepted by the `SplitFactoryProvider` component, + * used to instantiate a factory and provide it to the Split Context. */ -export interface ISplitFactoryProps extends IUpdateProps { +export interface ISplitFactoryProviderProps { /** - * Config object used to instantiate a Split factory + * Config object used to instantiate a Split factory. * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#configuration} */ config?: SplitIO.IBrowserSettings; /** - * Split factory instance to use instead of creating a new one with the config object. + * Split factory instance to use instead of creating a new one with the `config` object. * - * If both `factory` and `config` are provided, the `config` option is ignored. + * If both `factory` and `config` are provided, the `config` prop is ignored. */ factory?: SplitIO.IBrowserSDK; @@ -131,28 +133,22 @@ export interface ISplitFactoryProps extends IUpdateProps { attributes?: SplitIO.Attributes; /** - * Children of the SplitFactoryProvider component. It can be a functional component (child as a function) or a React element. + * Children of the `SplitFactoryProvider` component. */ - children: ((props: ISplitFactoryChildProps) => ReactNode) | ReactNode; + children: ReactNode; } /** - * useSplitClient options interface. This is the options object accepted by useSplitClient hook, - * used to retrieve a client instance with the Split context, and listen for SDK events. + * Options object accepted by the `useSplitClient` hook, used to retrieve a client instance and update the component on SDK events. */ export interface IUseSplitClientOptions extends IUpdateProps { /** - * The customer identifier. + * The customer identifier. If not provided, the hook will use the client available in the Split context, which is the default client by default (i.e., `factory.client()`), + * except the hook is wrapped by a `SplitClient` component, in which case the Split context might be updated with a different client. */ splitKey?: SplitIO.SplitKey; - /** - * Traffic type associated with the customer identifier. - * If no provided here or at the config object, it will be required on the client.track() calls. - */ - trafficType?: string; - /** * An object of type Attributes used to evaluate the feature flags. */ @@ -160,10 +156,8 @@ export interface IUseSplitClientOptions extends IUpdateProps { } /** - * SplitClient Child Props interface. These are the props that the child component receives from the 'SplitClient' component. + * SplitClient Child Props interface. These are the props that the child as a function receives from the 'SplitClient' component. */ -// @TODO remove next type (breaking-change) -// eslint-disable-next-line @typescript-eslint/no-empty-interface export interface ISplitClientChildProps extends ISplitContextValues { } /** @@ -178,6 +172,15 @@ export interface ISplitClientProps extends IUseSplitClientOptions { children: ((props: ISplitClientChildProps) => ReactNode) | ReactNode; } +export interface IUseSplitManagerResult extends ISplitContextValues { + /** + * Split manager instance. + * + * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} + */ + manager?: SplitIO.IManager; +} + export type GetTreatmentsOptions = ({ /** @@ -201,7 +204,7 @@ export type GetTreatmentsOptions = ({ } /** - * useSplitTreatments options interface. This is the options object accepted by useSplitTreatments hook, used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', + * Options object accepted by the `useSplitTreatments` hook, used to call `client.getTreatmentsWithConfig()`, or `client.getTreatmentsWithConfigByFlagSets()`, * depending on whether `names` or `flagSets` options are provided, and to retrieve the result along with the Split context. */ export type IUseSplitTreatmentsOptions = GetTreatmentsOptions & IUseSplitClientOptions; @@ -229,7 +232,7 @@ export interface ISplitTreatmentsChildProps extends ISplitContextValues { * SplitTreatments Props interface. These are the props accepted by SplitTreatments component, used to call 'client.getTreatmentsWithConfig()', or 'client.getTreatmentsWithConfigByFlagSets()', * depending on whether `names` or `flagSets` props are provided, and to pass the result to the child component. */ -export type ISplitTreatmentsProps = GetTreatmentsOptions & { +export type ISplitTreatmentsProps = IUseSplitTreatmentsOptions & { /** * Children of the SplitTreatments component. It must be a functional component (child as a function) you want to show. diff --git a/src/useClient.ts b/src/useClient.ts deleted file mode 100644 index 7428b60..0000000 --- a/src/useClient.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useSplitClient } from './useSplitClient'; - -/** - * 'useClient' is a hook that returns a client from the Split context. - * It uses the 'useContext' hook to access the context, which is updated by - * SplitFactoryProvider and SplitClient components in the hierarchy of components. - * - * @returns A Split Client instance, or null if used outside the scope of SplitFactoryProvider or factory is not ready. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#advanced-instantiate-multiple-sdk-clients} - * - * @deprecated Replace with the new `useSplitClient` hook. - */ -export function useClient(splitKey?: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes): SplitIO.IBrowserClient | null { - return useSplitClient({ splitKey, trafficType, attributes }).client; -} diff --git a/src/useManager.ts b/src/useManager.ts deleted file mode 100644 index 094ec99..0000000 --- a/src/useManager.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useSplitManager } from './useSplitManager'; - -/** - * 'useManager' is a hook that returns the Manager instance from the Split factory. - * It uses the 'useContext' hook to access the factory at Split context, which is updated by - * the SplitFactoryProvider component. - * - * @returns A Split Manager instance, or null if used outside the scope of SplitFactoryProvider or factory is not ready. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} - * - * @deprecated Replace with the new `useSplitManager` hook. - */ -export function useManager(): SplitIO.IManager | null { - return useSplitManager().manager; -} diff --git a/src/useSplitClient.ts b/src/useSplitClient.ts index 640af92..74e26d6 100644 --- a/src/useSplitClient.ts +++ b/src/useSplitClient.ts @@ -1,20 +1,20 @@ import React from 'react'; -import { SplitContext } from './SplitContext'; +import { useSplitContext } from './SplitContext'; import { getSplitClient, initAttributes, IClientWithContext, getStatus } from './utils'; import { ISplitContextValues, IUseSplitClientOptions } from './types'; export const DEFAULT_UPDATE_OPTIONS = { - updateOnSdkUpdate: false, - updateOnSdkTimedout: false, + updateOnSdkUpdate: true, + updateOnSdkTimedout: true, updateOnSdkReady: true, updateOnSdkReadyFromCache: true, }; /** - * 'useSplitClient' is a hook that returns an Split Context object with the client and its status corresponding to the provided key and trafficType. - * It uses the 'useContext' hook to access the context, which is updated by SplitFactoryProvider and SplitClient components in the hierarchy of components. + * `useSplitClient` is a hook that returns an Split Context object with the client and its status corresponding to the provided key. * - * @returns A Split Context object + * @param options - An options object with an optional `splitKey` to retrieve the client, optional `attributes` to configure the client, and update options to control on which SDK events the hook should update. + * @returns A Split Context object merged with the client and its status. * * @example * ```js @@ -25,17 +25,16 @@ export const DEFAULT_UPDATE_OPTIONS = { */ export function useSplitClient(options?: IUseSplitClientOptions): ISplitContextValues { const { - updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, trafficType, attributes + updateOnSdkReady, updateOnSdkReadyFromCache, updateOnSdkTimedout, updateOnSdkUpdate, splitKey, attributes } = { ...DEFAULT_UPDATE_OPTIONS, ...options }; - const context = React.useContext(SplitContext); + const context = useSplitContext(); const { client: contextClient, factory } = context; - let client = contextClient as IClientWithContext; - if (splitKey && factory) { - // @TODO `getSplitClient` starts client sync. Move side effects to useEffect - client = getSplitClient(factory, splitKey, trafficType); - } + // @TODO Move `getSplitClient` side effects + // @TODO Once `SplitClient` is removed, which updates the context, simplify next line as `const client = factory ? getSplitClient(factory, splitKey) : undefined;` + const client = factory && splitKey ? getSplitClient(factory, splitKey) : contextClient as IClientWithContext; + initAttributes(client, attributes); const status = getStatus(client); diff --git a/src/useSplitManager.ts b/src/useSplitManager.ts index 13aa9b1..c797290 100644 --- a/src/useSplitManager.ts +++ b/src/useSplitManager.ts @@ -1,25 +1,29 @@ -import React from 'react'; -import { SplitContext } from './SplitContext'; -import { ISplitContextValues } from './types'; +import { useSplitClient } from './useSplitClient'; +import { IUseSplitManagerResult } from './types'; +import { useSplitContext } from './SplitContext'; /** - * 'useSplitManager' is a hook that returns an Split Context object with the Manager instance from the Split factory. - * It uses the 'useContext' hook to access the factory at Split context, which is updated by the SplitFactoryProvider component. + * `useSplitManager` is a hook that returns an Split Context object with the manager instance from the Split factory. * - * @returns An object containing the Split context and the Split Manager instance, which is null if used outside the scope of SplitFactoryProvider or factory is not ready. + * @returns A Split Context object merged with the manager and its status. * * @example * ```js - * const { manager, isReady } = useSplitManager(); + * const { manager, isReady, isReadyFromCache, lastUpdate, ... } = useSplitManager(); * ``` * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#manager} */ -export function useSplitManager(): ISplitContextValues & { manager: SplitIO.IManager | null } { - // Update options are not supported, because updates can be controlled at the SplitFactoryProvider component. - const context = React.useContext(SplitContext); +export function useSplitManager(): IUseSplitManagerResult { + // @TODO refactor next lines to `const context = useSplitClient();` when `SplitClient` is removed + // This is required to avoid retrieving the status of a non-default client if context was updated by a `SplitClient` component. + const { factory } = useSplitContext(); + const context = useSplitClient({ splitKey: factory?.settings.core.key }); + + const manager = factory ? factory.manager() : undefined; + return { ...context, - manager: context.factory ? context.factory.manager() : null + manager, }; } diff --git a/src/useSplitTreatments.ts b/src/useSplitTreatments.ts index d5dbf6d..d9b0f38 100644 --- a/src/useSplitTreatments.ts +++ b/src/useSplitTreatments.ts @@ -4,10 +4,11 @@ import { ISplitTreatmentsChildProps, IUseSplitTreatmentsOptions } from './types' import { useSplitClient } from './useSplitClient'; /** - * 'useSplitTreatments' is a hook that returns an SplitContext object extended with a `treatments` property object that contains feature flag evaluations. - * It uses the 'useSplitClient' hook to access the client from the Split context, and invokes the 'client.getTreatmentsWithConfig()' method if the `names` option is provided, - * or the 'client.getTreatmentsWithConfigByFlagSets()' method if the `flagSets` option is provided. + * `useSplitTreatments` is a hook that returns an Split Context object extended with a `treatments` property object that contains feature flag evaluations. + * It uses the `useSplitClient` hook to access the client, and invokes the `client.getTreatmentsWithConfig()` method if the `names` option is provided, + * or the `client.getTreatmentsWithConfigByFlagSets()` method if the `flagSets` option is provided. * + * @param options - An options object with a list of feature flag names or flag sets to evaluate, and an optional `attributes` and `splitKey` values to configure the client. * @returns A Split Context object extended with a TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. * * @example diff --git a/src/useTrack.ts b/src/useTrack.ts index b511018..2af449b 100644 --- a/src/useTrack.ts +++ b/src/useTrack.ts @@ -4,15 +4,14 @@ import { useSplitClient } from './useSplitClient'; const noOpFalse = () => false; /** - * 'useTrack' is a hook that returns the track method from a Split client. - * It uses the 'useContext' hook to access the client from the Split context. + * `useTrack` is a hook that retrieves the track method from a Split client. * - * @returns A track function bound to a Split client. If the client is not available, the result is a no-op function that returns false. + * @returns The track method of the Split client for the provided user key. If the client is not available, the result is a no-op function that returns false. * * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#track} */ -export function useTrack(splitKey?: SplitIO.SplitKey, trafficType?: string): SplitIO.IBrowserClient['track'] { +export function useTrack(splitKey?: SplitIO.SplitKey): SplitIO.IBrowserClient['track'] { // All update options are false to avoid re-renders. The track method doesn't need the client to be operational. - const { client } = useSplitClient({ splitKey, trafficType, updateOnSdkReady: false, updateOnSdkReadyFromCache: false }); - return client ? client.track.bind(client) : noOpFalse; + const { client } = useSplitClient({ splitKey, updateOnSdkReady: false, updateOnSdkReadyFromCache: false, updateOnSdkTimedout: false, updateOnSdkUpdate: false }); + return client ? client.track : noOpFalse; } diff --git a/src/useTreatments.ts b/src/useTreatments.ts deleted file mode 100644 index bd673fa..0000000 --- a/src/useTreatments.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { useSplitTreatments } from './useSplitTreatments'; - -/** - * 'useTreatments' is a hook that returns an object of feature flag evaluations (i.e., treatments). - * It uses the 'useContext' hook to access the client from the Split context, - * and invokes the 'getTreatmentsWithConfig' method. - * - * @returns A TreatmentsWithConfig instance, that might contain control treatments if the client is not available or ready, or if feature flag names do not exist. - * - * @see {@link https://help.split.io/hc/en-us/articles/360020448791-JavaScript-SDK#get-treatments-with-configurations} - * - * @deprecated Replace with the new `useSplitTreatments` hook. - */ -export function useTreatments(featureFlagNames: string[], attributes?: SplitIO.Attributes, splitKey?: SplitIO.SplitKey): SplitIO.TreatmentsWithConfig { - return useSplitTreatments({ names: featureFlagNames, attributes, splitKey }).treatments; -} diff --git a/src/utils.ts b/src/utils.ts index efcb17e..f7a8f33 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,6 +1,6 @@ import memoizeOne from 'memoize-one'; import shallowEqual from 'shallowequal'; -import { SplitFactory as SplitSdk } from '@splitsoftware/splitio/client'; +import { SplitFactory } from '@splitsoftware/splitio/client'; import { CONTROL_WITH_CONFIG, VERSION, WARN_NAMES_AND_FLAGSETS } from './constants'; import { ISplitStatus } from './types'; @@ -21,60 +21,49 @@ export interface IClientWithContext extends SplitIO.IBrowserClient { }; } -/** - * FactoryWithClientInstances interface. - */ -export interface IFactoryWithClients extends SplitIO.IBrowserSDK { - clientInstances: Set; +export interface IFactoryWithLazyInit extends SplitIO.IBrowserSDK { config: SplitIO.IBrowserSettings; + init(): void; } // exported for testing purposes -export const __factories: Map = new Map(); +export const __factories: Map = new Map(); // idempotent operation -export function getSplitFactory(config: SplitIO.IBrowserSettings): IFactoryWithClients { +export function getSplitFactory(config: SplitIO.IBrowserSettings) { if (!__factories.has(config)) { - // SplitSDK is not an idempotent operation + // SplitFactory is not an idempotent operation // @ts-expect-error. 2nd param is not part of type definitions. Used to overwrite the SDK version - const newFactory = SplitSdk(config, (modules) => { + const newFactory = SplitFactory(config, (modules) => { modules.settings.version = VERSION; - }) as IFactoryWithClients; - newFactory.clientInstances = new Set(); + modules.lazyInit = true; + }) as IFactoryWithLazyInit; newFactory.config = config; __factories.set(config, newFactory); } - return __factories.get(config) as IFactoryWithClients; + return __factories.get(config) as IFactoryWithLazyInit; } // idempotent operation -export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey, trafficType?: string): IClientWithContext { +export function getSplitClient(factory: SplitIO.IBrowserSDK, key?: SplitIO.SplitKey): IClientWithContext { // factory.client is an idempotent operation - const client = (key !== undefined ? factory.client(key, trafficType) : factory.client()) as IClientWithContext; + const client = (key !== undefined ? factory.client(key) : factory.client()) as IClientWithContext; // Remove EventEmitter warning emitted when using multiple SDK hooks or components. // Unlike JS SDK, users don't need to access the client directly, making the warning irrelevant. - client.setMaxListeners(0); + client.setMaxListeners && client.setMaxListeners(0); - if ((factory as IFactoryWithClients).clientInstances) { - (factory as IFactoryWithClients).clientInstances.add(client); - } return client; } -export function destroySplitFactory(factory: IFactoryWithClients): Promise { - // call destroy of clients - const destroyPromises: Promise[] = []; - factory.clientInstances.forEach((client) => destroyPromises.push(client.destroy())); - // remove references to release allocated memory - factory.clientInstances.clear(); +export function destroySplitFactory(factory: IFactoryWithLazyInit): Promise | undefined { __factories.delete(factory.config); - return Promise.all(destroyPromises); + return factory.destroy(); } // Util used to get client status. // It might be removed in the future, if the JS SDK extends its public API with a `getStatus` method -export function getStatus(client: SplitIO.IBrowserClient | null): ISplitStatus { +export function getStatus(client?: SplitIO.IBrowserClient): ISplitStatus { const status = client && (client as IClientWithContext).__getStatus(); return { @@ -90,7 +79,7 @@ export function getStatus(client: SplitIO.IBrowserClient | null): ISplitStatus { /** * Manage client attributes binding */ -export function initAttributes(client: SplitIO.IBrowserClient | null, attributes?: SplitIO.Attributes) { +export function initAttributes(client?: SplitIO.IBrowserClient, attributes?: SplitIO.Attributes) { if (client && attributes) client.setAttributes(attributes); } @@ -184,7 +173,7 @@ function argsAreEqual(newArgs: any[], lastArgs: any[]): boolean { shallowEqual(newArgs[5], lastArgs[5]); // flagSets } -function evaluateFeatureFlags(client: SplitIO.IBrowserClient | null, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { +function evaluateFeatureFlags(client: SplitIO.IBrowserClient | undefined, _lastUpdate: number, names?: SplitIO.SplitNames, attributes?: SplitIO.Attributes, _clientAttributes?: SplitIO.Attributes, flagSets?: string[]) { if (names && flagSets) console.log(WARN_NAMES_AND_FLAGSETS); return client && (client as IClientWithContext).__getStatus().isOperational && (names || flagSets) ? diff --git a/src/withSplitClient.tsx b/src/withSplitClient.tsx index f6689f4..3528b98 100644 --- a/src/withSplitClient.tsx +++ b/src/withSplitClient.tsx @@ -7,10 +7,12 @@ import { SplitClient } from './SplitClient'; * The wrapped component receives all the props of the container, * along with the passed props from SplitClient (see ISplitClientChildProps). * - * @param splitKey The customer identifier. - * @param trafficType Traffic type associated with the customer identifier. If no provided here or at the config object, it will be required on the client.track() calls. + * @param splitKey - The customer identifier. + * @param attributes - An object of type Attributes used to evaluate the feature flags. + * + * @deprecated `withSplitClient` will be removed in a future major release. We recommend replacing it with the `useSplitClient` hook. */ -export function withSplitClient(splitKey: SplitIO.SplitKey, trafficType?: string, attributes?: SplitIO.Attributes) { +export function withSplitClient(splitKey: SplitIO.SplitKey, attributes?: SplitIO.Attributes) { return function withSplitClientHoc( WrappedComponent: React.ComponentType, @@ -24,7 +26,6 @@ export function withSplitClient(splitKey: SplitIO.SplitKey, trafficType?: string return ( ` props of the component). This differs from the previous behavior where `factory`, `client`, and `manager` were immediately available. - * - Updating the `config` prop in `SplitFactoryProvider` reinitializes the SDK with the new configuration, while `SplitFactory` does not reinitialize the SDK. You should pass a - * reference to the configuration object (e.g., via a global variable, `useState`, or `useMemo`) rather than a new instance on each render, to avoid unnecessary reinitializations. - * - Updating the `factory` prop in `SplitFactoryProvider` replaces the current SDK instance, unlike `SplitFactory` where it is ignored. + * @deprecated `withSplitFactory` will be removed in a future major release. We recommend replacing it with the `SplitFactoryProvider` component. */ export function withSplitFactory(config?: SplitIO.IBrowserSettings, factory?: SplitIO.IBrowserSDK, attributes?: SplitIO.Attributes) { @@ -36,21 +26,24 @@ export function withSplitFactory(config?: SplitIO.IBrowserSettings, factory?: Sp return function wrapper(props: OuterProps) { return ( - - {(splitProps) => { - return ( - - ); - }} - + factory={factory}> + + + {(splitProps) => { + return ( + + ); + }} + + ); }; }; diff --git a/src/withSplitTreatments.tsx b/src/withSplitTreatments.tsx index 6d0607a..be452b8 100644 --- a/src/withSplitTreatments.tsx +++ b/src/withSplitTreatments.tsx @@ -7,8 +7,10 @@ import { SplitTreatments } from './SplitTreatments'; * The wrapped component receives all the props of the container, * along with the passed props from SplitTreatments (see ISplitTreatmentsChildProps). * - * @param names list of feature flag names - * @param attributes An object of type Attributes used to evaluate the feature flags. + * @param names - list of feature flag names + * @param attributes - An object of type Attributes used to evaluate the feature flags. + * + * @deprecated `withSplitTreatments` will be removed in a future major release. We recommend replacing it with the `useSplitTreatments` hook. */ export function withSplitTreatments(names: string[], attributes?: SplitIO.Attributes) { diff --git a/tsconfig.json b/tsconfig.json index a409207..aae24e6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,7 +7,7 @@ "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ "declaration": false, /* Generates corresponding '.d.ts' file. */ "sourceMap": false, /* Generates corresponding '.map' file. */ - "outDir": "./es", /* Redirect output structure to the directory. */ + "outDir": "./esm", /* Redirect output structure to the directory. */ "resolveJsonModule": true, /* Include modules imported with .json extension. */ "importHelpers": true, /* Import emit helpers from 'tslib'. */ diff --git a/umd.ts b/umd.ts index 6beea58..351c5d3 100644 --- a/umd.ts +++ b/umd.ts @@ -1,15 +1,15 @@ import { - SplitSdk, + SplitFactory, withSplitFactory, withSplitClient, withSplitTreatments, - SplitFactory, SplitFactoryProvider, SplitClient, SplitTreatments, - useClient, useSplitClient, useTreatments, useSplitTreatments, useTrack, useManager, useSplitManager, + SplitFactoryProvider, SplitClient, SplitTreatments, + useSplitClient, useSplitTreatments, useTrack, useSplitManager, SplitContext, } from './src/index'; export default { - SplitSdk, + SplitFactory, withSplitFactory, withSplitClient, withSplitTreatments, - SplitFactory, SplitFactoryProvider, SplitClient, SplitTreatments, - useClient, useSplitClient, useTreatments, useSplitTreatments, useTrack, useManager, useSplitManager, + SplitFactoryProvider, SplitClient, SplitTreatments, + useSplitClient, useSplitTreatments, useTrack, useSplitManager, SplitContext, };