fix: re-render w/ useWhenProviderReady, add tests (#901)

This PR:

- brings react-sdk test coverage from 0% to ~95%
  - adds DOM-based testing (tests based on asserting DOM entity states)
    - tests for query-style, basic, and detailed evaluation APIs
- tests for suspense functionality and re-rendering on context change
  - tests for some util functions and hooks
  - `renderHook` tests for non suspending hooks
- fixes a bug where `useWhenProviderReady` didn't cause re-render after
the provider is ready if suspense wasn't used (leading to an out-of-date
return value for provider readiness)


![image](https://github.com/open-feature/js-sdk/assets/25272906/e8ee420f-0167-4048-94e3-53176bd883b9)

---------

Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
Todd Baert 2024-04-11 13:41:13 -04:00 committed by GitHub
parent 240a46165d
commit 0f2094e236
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 1095 additions and 9 deletions

View File

@ -171,6 +171,24 @@ export default {
],
},
},
{
displayName: 'react',
testEnvironment: 'jsdom',
preset: 'ts-jest',
testMatch: ['<rootDir>/packages/react/test/**/*.spec.ts*'],
moduleNameMapper: {
'@openfeature/core': '<rootDir>/packages/shared/src',
'@openfeature/web-sdk': '<rootDir>/packages/client/src',
},
transform: {
'^.+\\.tsx$': [
'ts-jest',
{
tsconfig: '<rootDir>/packages/react/test/tsconfig.json',
},
],
},
},
],
// Use this configuration option to add custom reporters to Jest

460
package-lock.json generated
View File

@ -17,6 +17,8 @@
],
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.6",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.16",
"@types/react": "^18.2.55",
@ -61,6 +63,12 @@
"node": ">=0.10.0"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.3.3",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz",
"integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==",
"dev": true
},
"node_modules/@ampproject/remapping": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz",
@ -659,6 +667,18 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.24.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.4.tgz",
"integrity": "sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA==",
"dev": true,
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.23.9",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.23.9.tgz",
@ -2067,6 +2087,139 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@testing-library/dom": {
"version": "9.3.4",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz",
"integrity": "sha512-FlS4ZWlp97iiNWig0Muq8p+3rVDjRiYE+YKGbAqXOu9nwJFFOdL00kFpz42M+4huzYi86vAK1sOOfyOG45muIQ==",
"dev": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
"@types/aria-query": "^5.0.1",
"aria-query": "5.1.3",
"chalk": "^4.1.0",
"dom-accessibility-api": "^0.5.9",
"lz-string": "^1.5.0",
"pretty-format": "^27.0.2"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@testing-library/dom/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@testing-library/dom/node_modules/pretty-format": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
"react-is": "^17.0.1"
},
"engines": {
"node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
}
},
"node_modules/@testing-library/dom/node_modules/react-is": {
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true
},
"node_modules/@testing-library/jest-dom": {
"version": "6.4.2",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz",
"integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==",
"dev": true,
"dependencies": {
"@adobe/css-tools": "^4.3.2",
"@babel/runtime": "^7.9.2",
"aria-query": "^5.0.0",
"chalk": "^3.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"lodash": "^4.17.15",
"redent": "^3.0.0"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1"
},
"peerDependencies": {
"@jest/globals": ">= 28",
"@types/bun": "latest",
"@types/jest": ">= 28",
"jest": ">= 28",
"vitest": ">= 0.32"
},
"peerDependenciesMeta": {
"@jest/globals": {
"optional": true
},
"@types/bun": {
"optional": true
},
"@types/jest": {
"optional": true
},
"jest": {
"optional": true
},
"vitest": {
"optional": true
}
}
},
"node_modules/@testing-library/jest-dom/node_modules/chalk": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
"dev": true,
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
"dev": true
},
"node_modules/@testing-library/react": {
"version": "14.2.2",
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.2.2.tgz",
"integrity": "sha512-SOUuM2ysCvjUWBXTNfQ/ztmnKDmqaiPV3SvoIuyxMUca45rbSWWAT/qB8CUs/JQ/ux/8JFs9DNdFQ3f6jH3crA==",
"dev": true,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@testing-library/dom": "^9.0.0",
"@types/react-dom": "^18.0.0"
},
"engines": {
"node": ">=14"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@tootallnate/once": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
@ -2100,6 +2253,12 @@
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
"dev": true
},
"node_modules/@types/aria-query": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@ -2285,6 +2444,15 @@
"csstype": "^3.0.2"
}
},
"node_modules/@types/react-dom": {
"version": "18.2.24",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.24.tgz",
"integrity": "sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==",
"dev": true,
"dependencies": {
"@types/react": "*"
}
},
"node_modules/@types/scheduler": {
"version": "0.16.8",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz",
@ -2732,6 +2900,15 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/aria-query": {
"version": "5.1.3",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz",
"integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==",
"dev": true,
"dependencies": {
"deep-equal": "^2.0.5"
}
},
"node_modules/arr-diff": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz",
@ -3302,15 +3479,16 @@
}
},
"node_modules/call-bind": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.6.tgz",
"integrity": "sha512-Mj50FLHtlsoVfRfnHaZvyrooHcrlceNZdL/QBvJJVd9Ta55qCQK0gs4ss2oZDeV9zFCs6ewzYgVE5yfVmfFpVg==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
"integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
"dev": true,
"dependencies": {
"es-define-property": "^1.0.0",
"es-errors": "^1.3.0",
"function-bind": "^1.1.2",
"get-intrinsic": "^1.2.3",
"set-function-length": "^1.2.0"
"get-intrinsic": "^1.2.4",
"set-function-length": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
@ -3699,6 +3877,12 @@
"node": ">= 8"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
"dev": true
},
"node_modules/cssom": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz",
@ -3820,6 +4004,38 @@
}
}
},
"node_modules/deep-equal": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz",
"integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==",
"dev": true,
"dependencies": {
"array-buffer-byte-length": "^1.0.0",
"call-bind": "^1.0.5",
"es-get-iterator": "^1.1.3",
"get-intrinsic": "^1.2.2",
"is-arguments": "^1.1.1",
"is-array-buffer": "^3.0.2",
"is-date-object": "^1.0.5",
"is-regex": "^1.1.4",
"is-shared-array-buffer": "^1.0.2",
"isarray": "^2.0.5",
"object-is": "^1.1.5",
"object-keys": "^1.1.1",
"object.assign": "^4.1.4",
"regexp.prototype.flags": "^1.5.1",
"side-channel": "^1.0.4",
"which-boxed-primitive": "^1.0.2",
"which-collection": "^1.0.1",
"which-typed-array": "^1.1.13"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@ -3969,6 +4185,12 @@
"node": ">=6.0.0"
}
},
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true
},
"node_modules/domexception": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz",
@ -4110,6 +4332,18 @@
"integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
"dev": true
},
"node_modules/es-define-property": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
"integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
"dev": true,
"dependencies": {
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
@ -4119,6 +4353,26 @@
"node": ">= 0.4"
}
},
"node_modules/es-get-iterator": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz",
"integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"get-intrinsic": "^1.1.3",
"has-symbols": "^1.0.3",
"is-arguments": "^1.1.1",
"is-map": "^2.0.2",
"is-set": "^2.0.2",
"is-string": "^1.0.7",
"isarray": "^2.0.5",
"stop-iteration-iterator": "^1.0.0"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.2.tgz",
@ -5871,6 +6125,15 @@
"node": ">=0.8.19"
}
},
"node_modules/indent-string": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
"dev": true,
"engines": {
"node": ">=8"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@ -5931,6 +6194,22 @@
"node": ">= 0.10"
}
},
"node_modules/is-arguments": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz",
"integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.2",
"has-tostringtag": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-array-buffer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@ -6151,6 +6430,18 @@
"node": ">=0.10.0"
}
},
"node_modules/is-map": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
"integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-negative-zero": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz",
@ -6230,6 +6521,18 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-set": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
"integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-shared-array-buffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz",
@ -6305,6 +6608,18 @@
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"dev": true
},
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
"integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
"dev": true,
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-weakref": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
@ -6317,6 +6632,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-weakset": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
"integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"get-intrinsic": "^1.2.4"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-windows": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz",
@ -9876,6 +10207,15 @@
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"node_modules/lz-string": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": {
"version": "0.30.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz",
@ -10045,6 +10385,15 @@
"node": ">=6"
}
},
"node_modules/min-indent": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
"dev": true,
"engines": {
"node": ">=4"
}
},
"node_modules/minimatch": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
@ -10369,6 +10718,22 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-is": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
"integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
"dev": true,
"dependencies": {
"call-bind": "^1.0.7",
"define-properties": "^1.2.1"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/object-keys": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
@ -11025,6 +11390,20 @@
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
},
"peerDependencies": {
"react": "^18.2.0"
}
},
"node_modules/react-is": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz",
@ -11172,6 +11551,19 @@
"node": ">= 0.10"
}
},
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
"dev": true,
"dependencies": {
"indent-string": "^4.0.0",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/reflect-metadata": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.1.tgz",
@ -11179,6 +11571,12 @@
"dev": true,
"peer": true
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true
},
"node_modules/regex-not": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz",
@ -11812,6 +12210,16 @@
"node": ">=v12.22.7"
}
},
"node_modules/scheduler": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
"dev": true,
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
},
"node_modules/semver": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
@ -12403,6 +12811,18 @@
"node": ">= 0.8"
}
},
"node_modules/stop-iteration-iterator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz",
"integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==",
"dev": true,
"dependencies": {
"internal-slot": "^1.0.4"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/streamsearch": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
@ -12538,6 +12958,18 @@
"node": ">=6"
}
},
"node_modules/strip-indent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
"dev": true,
"dependencies": {
"min-indent": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@ -13505,6 +13937,24 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-collection": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
"integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
"dev": true,
"dependencies": {
"is-map": "^2.0.3",
"is-set": "^2.0.3",
"is-weakmap": "^2.0.2",
"is-weakset": "^2.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/which-module": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz",

View File

@ -4,7 +4,7 @@
"private": true,
"description": "OpenFeature SDK for JavaScript",
"scripts": {
"test": "jest --selectProjects=shared --selectProjects=server --selectProjects=client --silent",
"test": "jest --selectProjects=shared --selectProjects=server --selectProjects=client --selectProjects=react --silent",
"e2e-server": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/server/e2e/features && jest --selectProjects=server-e2e --verbose",
"e2e-client": "git submodule update --init --recursive && shx cp test-harness/features/evaluation.feature packages/client/e2e/features && jest --selectProjects=client-e2e --verbose",
"e2e": "npm run e2e-server && npm run e2e-client",
@ -37,6 +37,8 @@
},
"devDependencies": {
"@rollup/plugin-typescript": "^11.1.6",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.2",
"@types/jest": "^29.5.12",
"@types/node": "^20.11.16",
"@types/react": "^18.2.55",

View File

@ -18,12 +18,24 @@ export function useWhenProviderReady(options?: Options): boolean {
const [, updateState] = useState<object | undefined>();
const client = useOpenFeatureClient();
// highest priority > evaluation hook options > provider options > default options > lowest priority
const defaultedOptions = { ...DEFAULT_OPTIONS, ...useProviderOptions(), ...normalizeOptions(options)};
const defaultedOptions = { ...DEFAULT_OPTIONS, ...useProviderOptions(), ...normalizeOptions(options) };
const updateStateRef = () => {
updateState({});
};
useEffect(() => {
if (defaultedOptions.suspendUntilReady && client.providerStatus === ProviderStatus.NOT_READY) {
suspend(client, updateState, ProviderEvents.Ready);
if (client.providerStatus === ProviderStatus.NOT_READY) {
// re-render when provider is ready
client.addHandler(ProviderEvents.Ready, updateStateRef);
if (defaultedOptions.suspendUntilReady) {
// suspend and update when the provider is ready
suspend(client, updateState, ProviderEvents.Ready);
}
}
return () => {
// cleanup the handler
client.removeHandler(ProviderEvents.Ready, updateStateRef);
};
}, []);
return client.providerStatus === ProviderStatus.READY;

View File

@ -0,0 +1,404 @@
import { EvaluationContext, InMemoryProvider, OpenFeature, StandardResolutionReasons } from '@openfeature/web-sdk';
import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup
import { act, render, screen, waitFor } from '@testing-library/react';
import * as React from 'react';
import {
OpenFeatureProvider,
useBooleanFlagDetails,
useBooleanFlagValue,
useFlag,
useNumberFlagDetails,
useNumberFlagValue,
useObjectFlagDetails,
useObjectFlagValue,
useStringFlagDetails,
useStringFlagValue,
} from '../src/';
import { TestingProvider } from './test.utils';
describe('evaluation', () => {
const EVALUATION = 'evaluation';
const BOOL_FLAG_KEY = 'boolean-flag';
const BOOL_FLAG_VARIANT = 'on';
const BOOL_FLAG_VALUE = true;
const STRING_FLAG_KEY = 'string-flag';
const STRING_FLAG_VARIANT = 'greeting';
const STRING_FLAG_VALUE = 'hi';
const NUMBER_FLAG_KEY = 'number-flag';
const NUMBER_FLAG_VARIANT = '2^10';
const NUMBER_FLAG_VALUE = 1024;
const OBJECT_FLAG_KEY = 'object-flag';
const OBJECT_FLAG_VARIANT = 'template';
const OBJECT_FLAG_VALUE = { factor: 'x1000' };
const VARIANT_ATTR = 'data-variant';
const REASON_ATTR = 'data-reason';
const REASON_ATTR_VALUE = StandardResolutionReasons.STATIC;
const TYPE_ATTR = 'data-type';
const provider = new InMemoryProvider({
[BOOL_FLAG_KEY]: {
disabled: false,
variants: {
[BOOL_FLAG_VARIANT]: BOOL_FLAG_VALUE,
off: false,
},
defaultVariant: BOOL_FLAG_VARIANT,
},
[STRING_FLAG_KEY]: {
disabled: false,
variants: {
[STRING_FLAG_VARIANT]: STRING_FLAG_VALUE,
parting: 'bye',
},
defaultVariant: STRING_FLAG_VARIANT,
},
[NUMBER_FLAG_KEY]: {
disabled: false,
variants: {
[NUMBER_FLAG_VARIANT]: NUMBER_FLAG_VALUE,
'2^1': 2,
},
defaultVariant: NUMBER_FLAG_VARIANT,
},
[OBJECT_FLAG_KEY]: {
disabled: false,
variants: {
[OBJECT_FLAG_VARIANT]: OBJECT_FLAG_VALUE,
empty: {},
},
defaultVariant: OBJECT_FLAG_VARIANT,
},
});
OpenFeature.setProvider(EVALUATION, provider);
describe('useFlag hook', () => {
function TestComponent() {
const {
value: booleanVal,
reason: boolReason,
variant: boolVariant,
type: booleanType,
} = useFlag(BOOL_FLAG_KEY, false);
const {
value: stringVal,
reason: stringReason,
variant: stringVariant,
type: stringType,
} = useFlag(STRING_FLAG_KEY, 'default');
const {
value: numberVal,
reason: numberReason,
variant: numberVariant,
type: numberType,
} = useFlag(NUMBER_FLAG_KEY, 0);
const {
value: objectVal,
reason: objectReason,
variant: objectVariant,
type: objectType,
} = useFlag(OBJECT_FLAG_KEY, {});
return (
<>
<div data-type={booleanType} data-variant={boolVariant} data-reason={boolReason}>{`${booleanVal}`}</div>
<div data-type={stringType} data-variant={stringVariant} data-reason={stringReason}>
{stringVal}
</div>
<div data-type={numberType} data-variant={numberVariant} data-reason={numberReason}>{`${numberVal}`}</div>
<div data-type={objectType} data-variant={objectVariant} data-reason={objectReason}>
{JSON.stringify(objectVal)}
</div>
</>
);
}
it('should evaluate flags', () => {
render(
<OpenFeatureProvider domain={EVALUATION}>
<TestComponent></TestComponent>
</OpenFeatureProvider>,
);
const boolElement = screen.queryByText(`${BOOL_FLAG_VALUE}`);
const stringElement = screen.queryByText(STRING_FLAG_VALUE);
const numberElement = screen.queryByText(`${NUMBER_FLAG_VALUE}`);
const objectElement = screen.queryByText(JSON.stringify(OBJECT_FLAG_VALUE));
expect(boolElement).toBeInTheDocument();
expect(boolElement).toHaveAttribute(VARIANT_ATTR, BOOL_FLAG_VARIANT);
expect(boolElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
expect(boolElement).toHaveAttribute(TYPE_ATTR, 'boolean');
expect(stringElement).toBeInTheDocument();
expect(stringElement).toHaveAttribute(VARIANT_ATTR, STRING_FLAG_VARIANT);
expect(stringElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
expect(stringElement).toHaveAttribute(TYPE_ATTR, 'string');
expect(numberElement).toBeInTheDocument();
expect(numberElement).toHaveAttribute(VARIANT_ATTR, NUMBER_FLAG_VARIANT);
expect(numberElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
expect(numberElement).toHaveAttribute(TYPE_ATTR, 'number');
expect(objectElement).toBeInTheDocument();
expect(objectElement).toHaveAttribute(VARIANT_ATTR, OBJECT_FLAG_VARIANT);
expect(objectElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
expect(objectElement).toHaveAttribute(TYPE_ATTR, 'object');
});
});
describe('useFlagValue hooks', () => {
function TestComponent() {
const booleanVal = useBooleanFlagValue(BOOL_FLAG_KEY, false);
const stringVal = useStringFlagValue(STRING_FLAG_KEY, 'default');
const numberVal = useNumberFlagValue(NUMBER_FLAG_KEY, 0);
const objectVal = useObjectFlagValue(OBJECT_FLAG_KEY, {});
return (
<>
<div>{`${booleanVal}`}</div>
<div>{stringVal}</div>
<div>{numberVal}</div>
<div>{JSON.stringify(objectVal)}</div>
</>
);
}
it('should evaluate flags', () => {
render(
<OpenFeatureProvider domain={EVALUATION}>
<TestComponent></TestComponent>
</OpenFeatureProvider>,
);
expect(screen.queryByText(STRING_FLAG_VALUE)).toBeInTheDocument();
});
});
describe('useFlagDetails hooks', () => {
function TestComponent() {
const booleanValDetails = useBooleanFlagDetails(BOOL_FLAG_KEY, false);
const stringValDetails = useStringFlagDetails(STRING_FLAG_KEY, 'default');
const numberValDetails = useNumberFlagDetails(NUMBER_FLAG_KEY, 0);
const objectValDetails = useObjectFlagDetails(OBJECT_FLAG_KEY, {});
return (
<>
<div
data-variant={booleanValDetails.variant}
data-reason={booleanValDetails.reason}
>{`${booleanValDetails.value}`}</div>
<div data-variant={stringValDetails.variant} data-reason={stringValDetails.reason}>
{stringValDetails.value}
</div>
<div
data-variant={numberValDetails.variant}
data-reason={numberValDetails.reason}
>{`${numberValDetails.value}`}</div>
<div data-variant={objectValDetails.variant} data-reason={objectValDetails.reason}>
{JSON.stringify(objectValDetails.value)}
</div>
</>
);
}
it('should evaluate flags', () => {
render(
<OpenFeatureProvider domain={EVALUATION}>
<TestComponent></TestComponent>
</OpenFeatureProvider>,
);
const boolElement = screen.queryByText(`${BOOL_FLAG_VALUE}`);
const stringElement = screen.queryByText(STRING_FLAG_VALUE);
const numberElement = screen.queryByText(`${NUMBER_FLAG_VALUE}`);
const objectElement = screen.queryByText(JSON.stringify(OBJECT_FLAG_VALUE));
expect(boolElement).toBeInTheDocument();
expect(boolElement).toHaveAttribute(VARIANT_ATTR, BOOL_FLAG_VARIANT);
expect(boolElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
expect(stringElement).toBeInTheDocument();
expect(stringElement).toHaveAttribute(VARIANT_ATTR, STRING_FLAG_VARIANT);
expect(stringElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
expect(numberElement).toBeInTheDocument();
expect(numberElement).toHaveAttribute(VARIANT_ATTR, NUMBER_FLAG_VARIANT);
expect(numberElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
expect(objectElement).toBeInTheDocument();
expect(objectElement).toHaveAttribute(VARIANT_ATTR, OBJECT_FLAG_VARIANT);
expect(objectElement).toHaveAttribute(REASON_ATTR, REASON_ATTR_VALUE);
});
});
});
describe('re-rending and suspense', () => {
/**
* artificial delay for various async operations for our provider,
* multiples of it are used in assertions as well
*/
const DELAY = 100;
const SUSPENSE_ON = 'suspense';
const SUSPENSE_OFF = 'suspense-off';
const CONFIG_UPDATE = 'config-update';
const SUSPENSE_FLAG_KEY = 'delayed-flag';
const FLAG_VARIANT_A = 'greeting';
const STATIC_FLAG_VALUE_A = 'hi';
const FLAG_VARIANT_B = 'parting';
const STATIC_FLAG_VALUE_B = 'bye';
const TARGETED_FLAG_VALUE = 'aloha';
const FALLBACK = 'fallback';
const DEFAULT = 'default';
const TARGETED_USER = 'bob@flags.com';
const CONFIG = {
[SUSPENSE_FLAG_KEY]: {
disabled: false,
variants: {
[FLAG_VARIANT_A]: STATIC_FLAG_VALUE_A,
[FLAG_VARIANT_B]: STATIC_FLAG_VALUE_B,
both: TARGETED_FLAG_VALUE,
},
defaultVariant: 'greeting',
contextEvaluator: (context: EvaluationContext) => {
if (context.user == 'bob@flags.com') {
return 'both';
}
return 'greeting';
},
},
};
const suspendingProvider = () => {
return new TestingProvider(CONFIG, DELAY); // delay init by 100ms
};
function TestComponent() {
const { value } = useFlag(SUSPENSE_FLAG_KEY, DEFAULT);
return (
<>
<div>{value}</div>
</>
);
}
describe('updateOnConfigurationChanged=true (default)', () => {
it('should re-render after flag config changes', async () => {
const provider = suspendingProvider();
OpenFeature.setProvider(CONFIG_UPDATE, provider);
render(
<OpenFeatureProvider domain={CONFIG_UPDATE}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent></TestComponent>
</React.Suspense>
</OpenFeatureProvider>,
);
// first we should see the old value
await waitFor(() => expect(screen.queryByText(STATIC_FLAG_VALUE_A)).toBeInTheDocument(), { timeout: DELAY * 2 });
// change our flag config
await act(async () => {
await provider.putConfiguration({
[SUSPENSE_FLAG_KEY]: {
...CONFIG[SUSPENSE_FLAG_KEY],
...{ defaultVariant: FLAG_VARIANT_B, contextEvaluator: undefined },
},
});
});
// eventually we should see the new value
await waitFor(() => expect(screen.queryByText(STATIC_FLAG_VALUE_B)).toBeInTheDocument(), { timeout: DELAY * 2 });
});
});
describe('suspendUntilReady=true (default)', () => {
it('should suspend until ready and then render', async () => {
OpenFeature.setProvider(SUSPENSE_ON, suspendingProvider());
render(
<OpenFeatureProvider domain={SUSPENSE_ON}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent></TestComponent>
</React.Suspense>
</OpenFeatureProvider>,
);
// should see fallback initially
expect(screen.queryByText(STATIC_FLAG_VALUE_A)).toBeNull();
expect(screen.queryByText(FALLBACK)).toBeInTheDocument();
// eventually we should see the value
await waitFor(() => expect(screen.queryByText(STATIC_FLAG_VALUE_A)).toBeInTheDocument(), { timeout: DELAY * 2 });
});
});
describe('suspendWhileReconciling=true (default)', () => {
it('should suspend until reconciled and then render', async () => {
await OpenFeature.setContext(SUSPENSE_OFF, {});
OpenFeature.setProvider(SUSPENSE_ON, suspendingProvider());
render(
// disable suspendUntilReady, we are only testing reconcile suspense.
<OpenFeatureProvider domain={SUSPENSE_ON} suspendUntilReady={false}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent></TestComponent>
</React.Suspense>
</OpenFeatureProvider>,
);
// initially should be default, because suspendUntilReady={false}
expect(screen.queryByText(DEFAULT)).toBeInTheDocument();
// update the context without awaiting
act(() => {
OpenFeature.setContext(SUSPENSE_ON, { user: TARGETED_USER });
});
// expect to see fallback while we are reconciling
await waitFor(() => expect(screen.queryByText(FALLBACK)).toBeInTheDocument(), { timeout: DELAY / 2 });
// make sure we updated after reconciling
await waitFor(() => expect(screen.queryByText(TARGETED_FLAG_VALUE)).toBeInTheDocument(), {
timeout: DELAY * 2,
});
});
});
describe('suspend=false', () => {
it('should not suspend until reconciled and then render', async () => {
await OpenFeature.setContext(SUSPENSE_OFF, {});
OpenFeature.setProvider(SUSPENSE_OFF, suspendingProvider());
render(
// disable suspendUntilReady, we are only testing reconcile suspense.
<OpenFeatureProvider domain={SUSPENSE_OFF} suspend={false}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent></TestComponent>
</React.Suspense>
</OpenFeatureProvider>,
);
// assert no suspense
expect(screen.queryByText(DEFAULT)).toBeInTheDocument();
expect(screen.queryByText(FALLBACK)).toBeNull();
// expect to see static value after we are ready
await waitFor(() => expect(screen.queryByText(STATIC_FLAG_VALUE_A)).toBeInTheDocument(), { timeout: DELAY * 2 });
// update the context without awaiting
act(() => {
OpenFeature.setContext(SUSPENSE_OFF, { user: TARGETED_USER });
});
// expect to see static value until we reconcile
await waitFor(() => expect(screen.queryByText(STATIC_FLAG_VALUE_A)).toBeInTheDocument(), { timeout: DELAY / 2 });
// make sure we updated after reconciling
await waitFor(() => expect(screen.queryByText(TARGETED_FLAG_VALUE)).toBeInTheDocument(), {
timeout: DELAY * 2,
});
});
});
});

View File

@ -0,0 +1,47 @@
import { normalizeOptions } from '../src/common/options';
describe('normalizeOptions', () => {
// we spread results from this function, so we never want to return null
describe('undefined options', () => {
it('should return empty object', () => {
const normalized = normalizeOptions();
expect(normalized).toEqual({});
});
});
// we spread results from this function, so we want to remove anything but explicit booleans
describe('undefined removal', () => {
it('should remove undefined props and maintain boolean props', () => {
const normalized = normalizeOptions({
suspendUntilReady: undefined,
suspendWhileReconciling: false,
updateOnConfigurationChanged: undefined,
updateOnContextChanged: true,
});
expect(normalized).not.toHaveProperty('suspendUntilReady');
expect(normalized).toHaveProperty('suspendWhileReconciling');
expect(normalized.suspendWhileReconciling).toEqual(false);
expect(normalized).not.toHaveProperty('updateOnConfigurationChanged');
expect(normalized).toHaveProperty('updateOnContextChanged');
expect(normalized.updateOnContextChanged).toEqual(true);
});
});
// we fallback the more specific suspense props (`ssuspendUntilReady` and `suspendWhileReconciling`) to `suspend`
describe('suspend fallback', () => {
it('should fallback to true suspend value', () => {
const normalized = normalizeOptions({
suspend: true,
});
expect(normalized.suspendUntilReady).toEqual(true);
expect(normalized.suspendWhileReconciling).toEqual(true);
});
it('should fallback to false suspend value', () => {
const normalized = normalizeOptions({
suspend: false,
});
expect(normalized.suspendUntilReady).toEqual(false);
expect(normalized.suspendWhileReconciling).toEqual(false);
});
});
});

View File

@ -0,0 +1,132 @@
import { EvaluationContext, OpenFeature } from '@openfeature/web-sdk';
import '@testing-library/jest-dom'; // see: https://testing-library.com/docs/react-testing-library/setup
import { render, renderHook, screen, waitFor } from '@testing-library/react';
import * as React from 'react';
import { OpenFeatureProvider, useOpenFeatureClient, useWhenProviderReady } from '../src';
import { TestingProvider } from './test.utils';
describe('provider', () => {
/**
* artificial delay for various async operations for our provider,
* multiples of it are used in assertions as well
*/
const DELAY = 100;
const SUSPENSE_ON = 'suspense';
const SUSPENSE_OFF = 'suspense';
const SUSPENSE_FLAG_KEY = 'delayed-flag';
const STATIC_FLAG_VALUE = 'hi';
const TARGETED_FLAG_VALUE = 'aloha';
const FALLBACK = 'fallback';
const suspendingProvider = () => {
return new TestingProvider(
{
[SUSPENSE_FLAG_KEY]: {
disabled: false,
variants: {
greeting: STATIC_FLAG_VALUE,
parting: 'bye',
both: TARGETED_FLAG_VALUE,
},
defaultVariant: 'greeting',
contextEvaluator: (context: EvaluationContext) => {
if (context.user == 'bob@flags.com') {
return 'both';
}
return 'greeting';
},
},
},
DELAY,
); // delay init by 100ms
};
describe('useOpenFeatureClient', () => {
const DOMAIN = 'useOpenFeatureClient';
describe('client specified', () => {
it('should return client from provider', () => {
const client = OpenFeature.getClient(DOMAIN);
const wrapper = ({ children }: Parameters<typeof OpenFeatureProvider>[0]) => (
<OpenFeatureProvider client={client}>{children}</OpenFeatureProvider>
);
const { result } = renderHook(() => useOpenFeatureClient(), { wrapper });
expect(result.current).toEqual(client);
});
});
describe('domain specified', () => {
it('should return client with domain', () => {
const wrapper = ({ children }: Parameters<typeof OpenFeatureProvider>[0]) => (
<OpenFeatureProvider domain={DOMAIN}>{children}</OpenFeatureProvider>
);
const { result } = renderHook(() => useOpenFeatureClient(), { wrapper });
expect(result.current.metadata.domain).toEqual(DOMAIN);
});
});
});
describe('useWhenProviderReady', () => {
describe('suspendUntilReady=true (default)', () => {
function TestComponent() {
const isReady = useWhenProviderReady();
return (
<>
<div>{isReady ? '👍' : '👎'}</div>
</>
);
}
it('should suspend until ready and then return provider status', async () => {
OpenFeature.setProvider(SUSPENSE_ON, suspendingProvider());
render(
<OpenFeatureProvider domain={SUSPENSE_ON}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent></TestComponent>
</React.Suspense>
</OpenFeatureProvider>,
);
// should see fallback initially
expect(screen.queryByText('👎')).not.toBeVisible();
expect(screen.queryByText(FALLBACK)).toBeInTheDocument();
// eventually we should the value
await waitFor(() => expect(screen.queryByText('👍')).toBeVisible(), { timeout: DELAY * 2 });
});
});
describe('suspendUntilReady=false', () => {
function TestComponent() {
const isReady = useWhenProviderReady({ suspendUntilReady: false });
return (
<>
<div>{isReady ? '👍' : '👎'}</div>
</>
);
}
it('should not suspend, should return provider status', async () => {
OpenFeature.setProvider(SUSPENSE_OFF, suspendingProvider());
render(
<OpenFeatureProvider domain={SUSPENSE_OFF}>
<React.Suspense fallback={<div>{FALLBACK}</div>}>
<TestComponent></TestComponent>
</React.Suspense>
</OpenFeatureProvider>,
);
// should see falsy value initially
expect(screen.queryByText('👎')).toBeInTheDocument();
// eventually we should the value
await waitFor(() => expect(screen.queryByText('👍')).toBeInTheDocument(), { timeout: DELAY * 2 });
});
});
});
});

View File

@ -0,0 +1,21 @@
import { EvaluationContext, InMemoryProvider } from '@openfeature/web-sdk';
export class TestingProvider extends InMemoryProvider {
constructor(
flagConfiguration: ConstructorParameters<typeof InMemoryProvider>[0],
private delay: number,
) {
super(flagConfiguration);
}
// artificially delay our init (delaying PROVIDER_READY event)
async initialize(context?: EvaluationContext | undefined): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, this.delay));
return super.initialize(context);
}
// artificially delay context changes
async onContextChange(): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, this.delay));
}
}