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)

---------
Signed-off-by: Todd Baert <todd.baert@dynatrace.com>
This commit is contained in:
parent
240a46165d
commit
0f2094e236
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue