universal-test-renderer 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,16 +1,15 @@
1
1
  # Universal Test Renderer for React
2
2
 
3
- ## The problem
3
+ A lightweight, JavaScript-only replacement for the deprecated React Test Renderer.
4
4
 
5
- Many React renderers require additional setup in order to work properly. For example, React Native requires a native code to run setup, and React DOM requires a DOM environment. While it's feasible to run a React DOM in simulated JS DOM environment, it is not possible for React Native.
5
+ ## Why Use It?
6
6
 
7
- Traditionally, in such case developers used React Test Renderer, but it has been deprecated with React 18.3.1, in it will be removed in the future.
8
-
9
- ## The solution
10
-
11
- This is a universal test renderer that can be used to test React components in pure JavaScript environments like Jest or Vitest. It's build on top of React Reconciler and renders React components on host component level, exposing the resulting host component tree for various types of inspection: tree structure, props checking, etc.
12
-
13
- This renderer is created to be used as replacement for React Test Renderer.
7
+ - **Pure JavaScript Testing** - Test React components in Jest or Vitest without browser or native dependencies
8
+ - **Universal** - Can be used to simulate React Native or any other React renderer
9
+ - **React 19 Ready** - Modern alternative as React Test Renderer is now deprecated
10
+ - **Lightweight** - Minimal dependencies and small bundle size
11
+ - **Type-safe** - Written in TypeScript with full type definitions
12
+ - **Flexible Configuration** - Customizable reconciler options for different use cases
14
13
 
15
14
  ## Installation
16
15
 
@@ -18,53 +17,28 @@ This renderer is created to be used as replacement for React Test Renderer.
18
17
  npm install -D universal-test-renderer
19
18
  ```
20
19
 
21
- Note: this package is now compatible with React 18.3. In the near future I will add React 19 support, as well.
22
-
23
- ## Usage
20
+ ## Basic Usage
24
21
 
25
22
  ```tsx
26
23
  import { act } from "react";
27
24
  import { createRoot } from "universal-test-renderer";
28
25
 
29
- test("basic renderer usage", () => {
26
+ test("example", () => {
30
27
  const renderer = createRoot();
31
28
  act(() => {
32
29
  renderer.render(<div>Hello!</div>);
33
30
  });
34
31
 
35
- expect(renderer.root?.toJSON()).toMatchInlineSnapshot(`
36
- <div>
37
- Hello!
38
- </div>
39
- `);
40
- });
41
- ```
42
-
43
- ## React Native support
44
-
45
- This packages includes a version of renderer configured to be compatible with React Native.
46
-
47
- ```tsx
48
- import { act } from "react";
49
- import { Text } from "react-native";
50
- import { createRoot } from "universal-test-renderer/react-native";
51
-
52
- test("basic renderer usage", () => {
53
- const renderer = createRoot();
54
- act(() => {
55
- renderer.render(<Text>Hello!</Text>);
56
- });
57
-
58
- expect(renderer.root?.toJSON()).toMatchInlineSnapshot(`
59
- <Text>
60
- Hello!
61
- </Text>
62
- `);
32
+ expect(renderer.root).toMatchInlineSnapshot(`
33
+ <div>
34
+ Hello!
35
+ </div>
36
+ `);
63
37
  });
64
38
  ```
65
39
 
66
- ## Differences from React Test Renderer
40
+ ## Key Differences from React Test Renderer
67
41
 
68
- - This renderer operates on host components level, it does not expose composite components unlike React Test Renderer, which exposed both host components and composite components.
69
- - This renderer offers more flexible renderer setup, allowing for passing various options to the underlying React Reconciler, like concurrent mode, specifying allowed text components, etc.
70
- - This renderer does not re-export `act` function, you should use one provided by `react` package instead.
42
+ - Works at host component level only (no composite components)
43
+ - More flexible reconciler configuration options
44
+ - Uses `act` from the React package directly
package/dist/index.cjs CHANGED
@@ -57,12 +57,124 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
57
57
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
58
58
 
59
59
  // src/index.ts
60
- var src_exports = {};
61
- __export(src_exports, {
62
- CONTAINER_TYPE: () => CONTAINER_TYPE,
60
+ var index_exports = {};
61
+ __export(index_exports, {
63
62
  createRoot: () => createRoot
64
63
  });
65
- module.exports = __toCommonJS(src_exports);
64
+ module.exports = __toCommonJS(index_exports);
65
+
66
+ // src/renderer.ts
67
+ var import_constants2 = require("react-reconciler/constants");
68
+
69
+ // src/find-all.ts
70
+ function findAll(root, predicate, options) {
71
+ const results = [];
72
+ const matchingDescendants = [];
73
+ root.children.forEach((child) => {
74
+ if (typeof child === "string") {
75
+ return;
76
+ }
77
+ matchingDescendants.push(...findAll(child, predicate, options));
78
+ });
79
+ if (
80
+ // When matchDeepestOnly = true: add current element only if no descendants match
81
+ (!(options == null ? void 0 : options.matchDeepestOnly) || matchingDescendants.length === 0) && predicate(root)
82
+ ) {
83
+ results.push(root);
84
+ }
85
+ results.push(...matchingDescendants);
86
+ return results;
87
+ }
88
+
89
+ // src/render-to-json.ts
90
+ function renderToJson(instance) {
91
+ if (instance.isHidden) {
92
+ return null;
93
+ }
94
+ switch (instance.tag) {
95
+ case "TEXT":
96
+ return instance.text;
97
+ case "INSTANCE": {
98
+ const _a = instance.props, { children } = _a, props = __objRest(_a, ["children"]);
99
+ let renderedChildren = null;
100
+ if (instance.children.length) {
101
+ for (let i = 0; i < instance.children.length; i++) {
102
+ const renderedChild = renderToJson(instance.children[i]);
103
+ if (renderedChild !== null) {
104
+ if (renderedChildren === null) {
105
+ renderedChildren = [renderedChild];
106
+ } else {
107
+ renderedChildren.push(renderedChild);
108
+ }
109
+ }
110
+ }
111
+ }
112
+ const result = {
113
+ type: instance.type,
114
+ props,
115
+ children: renderedChildren,
116
+ $$typeof: /* @__PURE__ */ Symbol.for("react.test.json")
117
+ };
118
+ Object.defineProperty(result, "$$typeof", {
119
+ value: /* @__PURE__ */ Symbol.for("react.test.json")
120
+ });
121
+ return result;
122
+ }
123
+ }
124
+ }
125
+
126
+ // src/host-element.ts
127
+ var instanceMap = /* @__PURE__ */ new WeakMap();
128
+ var HostElement = class _HostElement {
129
+ constructor(instance) {
130
+ this.instance = instance;
131
+ }
132
+ get type() {
133
+ return this.instance.type;
134
+ }
135
+ get props() {
136
+ const _a = this.instance.props, { children } = _a, restProps = __objRest(_a, ["children"]);
137
+ return restProps;
138
+ }
139
+ get children() {
140
+ const result = this.instance.children.filter((child) => !child.isHidden).map((child) => getHostNodeForInstance(child));
141
+ return result;
142
+ }
143
+ get parent() {
144
+ const parentInstance = this.instance.parent;
145
+ if (parentInstance == null || parentInstance.tag === "CONTAINER") {
146
+ return null;
147
+ }
148
+ return _HostElement.fromInstance(parentInstance);
149
+ }
150
+ get unstable_fiber() {
151
+ return this.instance.unstable_fiber;
152
+ }
153
+ get $$typeof() {
154
+ return /* @__PURE__ */ Symbol.for("react.test.json");
155
+ }
156
+ toJSON() {
157
+ return renderToJson(this.instance);
158
+ }
159
+ /** @internal */
160
+ static fromInstance(instance) {
161
+ const hostElement = instanceMap.get(instance);
162
+ if (hostElement) {
163
+ return hostElement;
164
+ }
165
+ const result = new _HostElement(instance);
166
+ instanceMap.set(instance, result);
167
+ return result;
168
+ }
169
+ };
170
+ function getHostNodeForInstance(instance) {
171
+ switch (instance.tag) {
172
+ case "TEXT":
173
+ return instance.text;
174
+ case "INSTANCE":
175
+ return HostElement.fromInstance(instance);
176
+ }
177
+ }
66
178
 
67
179
  // src/reconciler.ts
68
180
  var import_react_reconciler = __toESM(require("react-reconciler"), 1);
@@ -157,7 +269,7 @@ var hostConfig = {
157
269
  children: [],
158
270
  parent: null,
159
271
  rootContainer,
160
- internalHandle
272
+ unstable_fiber: internalHandle
161
273
  };
162
274
  },
163
275
  /**
@@ -275,9 +387,7 @@ var hostConfig = {
275
387
  */
276
388
  getChildHostContext(parentHostContext, type) {
277
389
  var _a;
278
- const isInsideText = Boolean(
279
- (_a = parentHostContext.config.textComponents) == null ? void 0 : _a.includes(type)
280
- );
390
+ const isInsideText = Boolean((_a = parentHostContext.config.textComponents) == null ? void 0 : _a.includes(type));
281
391
  return __spreadProps(__spreadValues({}, parentHostContext), { type, isInsideText });
282
392
  },
283
393
  /**
@@ -382,7 +492,7 @@ var hostConfig = {
382
492
  getInstanceFromNode(node) {
383
493
  const instance = nodeToInstanceMap.get(node);
384
494
  if (instance !== void 0) {
385
- return instance.internalHandle;
495
+ return instance.unstable_fiber;
386
496
  }
387
497
  return null;
388
498
  },
@@ -511,7 +621,7 @@ var hostConfig = {
511
621
  commitUpdate(instance, type, _prevProps, nextProps, internalHandle) {
512
622
  instance.type = type;
513
623
  instance.props = nextProps;
514
- instance.internalHandle = internalHandle;
624
+ instance.unstable_fiber = internalHandle;
515
625
  },
516
626
  /**
517
627
  * #### `hideInstance(instance)`
@@ -645,139 +755,7 @@ function removeChild(parentInstance, child) {
645
755
  child.parent = null;
646
756
  }
647
757
 
648
- // src/constants.ts
649
- var CONTAINER_TYPE = "CONTAINER";
650
-
651
- // src/render-to-json.ts
652
- function renderToJson(instance) {
653
- if (`isHidden` in instance && instance.isHidden) {
654
- return null;
655
- }
656
- switch (instance.tag) {
657
- case "TEXT":
658
- return instance.text;
659
- case "INSTANCE": {
660
- const _a = instance.props, { children } = _a, props = __objRest(_a, ["children"]);
661
- let renderedChildren = null;
662
- if (instance.children.length) {
663
- for (let i = 0; i < instance.children.length; i++) {
664
- const renderedChild = renderToJson(instance.children[i]);
665
- if (renderedChild !== null) {
666
- if (renderedChildren === null) {
667
- renderedChildren = [renderedChild];
668
- } else {
669
- renderedChildren.push(renderedChild);
670
- }
671
- }
672
- }
673
- }
674
- const result = {
675
- type: instance.type,
676
- props,
677
- children: renderedChildren,
678
- $$typeof: Symbol.for("react.test.json")
679
- };
680
- Object.defineProperty(result, "$$typeof", {
681
- value: Symbol.for("react.test.json")
682
- });
683
- return result;
684
- }
685
- case "CONTAINER": {
686
- let renderedChildren = null;
687
- if (instance.children.length) {
688
- for (let i = 0; i < instance.children.length; i++) {
689
- const renderedChild = renderToJson(instance.children[i]);
690
- if (renderedChild !== null) {
691
- if (renderedChildren === null) {
692
- renderedChildren = [renderedChild];
693
- } else {
694
- renderedChildren.push(renderedChild);
695
- }
696
- }
697
- }
698
- }
699
- const result = {
700
- type: CONTAINER_TYPE,
701
- props: {},
702
- children: renderedChildren,
703
- $$typeof: Symbol.for("react.test.json")
704
- };
705
- Object.defineProperty(result, "$$typeof", {
706
- value: Symbol.for("react.test.json")
707
- });
708
- return result;
709
- }
710
- }
711
- }
712
-
713
- // src/host-element.ts
714
- var instanceToHostElementMap = /* @__PURE__ */ new WeakMap();
715
- var HostElement = class _HostElement {
716
- constructor(instance) {
717
- this.instance = instance;
718
- }
719
- get type() {
720
- return this.instance.tag === "INSTANCE" ? this.instance.type : CONTAINER_TYPE;
721
- }
722
- get props() {
723
- if (this.instance.tag === "CONTAINER") {
724
- return {};
725
- }
726
- const _a = this.instance.props, { children } = _a, restProps = __objRest(_a, ["children"]);
727
- return restProps;
728
- }
729
- get children() {
730
- const result = this.instance.children.filter((child) => !child.isHidden).map((child) => _HostElement.fromInstance(child));
731
- return result;
732
- }
733
- get parent() {
734
- const parentInstance = this.instance.parent;
735
- if (parentInstance == null) {
736
- return null;
737
- }
738
- switch (parentInstance.tag) {
739
- case "INSTANCE":
740
- return _HostElement.fromInstance(parentInstance);
741
- case "CONTAINER":
742
- return _HostElement.fromContainer(parentInstance);
743
- }
744
- }
745
- get $$typeof() {
746
- return Symbol.for("react.test.json");
747
- }
748
- toJSON() {
749
- return renderToJson(this.instance);
750
- }
751
- /** @internal */
752
- static fromContainer(container) {
753
- const hostElement = instanceToHostElementMap.get(container);
754
- if (hostElement) {
755
- return hostElement;
756
- }
757
- const result = new _HostElement(container);
758
- instanceToHostElementMap.set(container, result);
759
- return result;
760
- }
761
- /** @internal */
762
- static fromInstance(instance) {
763
- switch (instance.tag) {
764
- case "TEXT":
765
- return instance.text;
766
- case "INSTANCE": {
767
- const hostElement = instanceToHostElementMap.get(instance);
768
- if (hostElement) {
769
- return hostElement;
770
- }
771
- const result = new _HostElement(instance);
772
- instanceToHostElementMap.set(instance, result);
773
- return result;
774
- }
775
- }
776
- }
777
- };
778
-
779
758
  // src/renderer.ts
780
- var import_constants4 = require("react-reconciler/constants");
781
759
  function createRoot(options) {
782
760
  var _a;
783
761
  let container = {
@@ -786,12 +764,12 @@ function createRoot(options) {
786
764
  parent: null,
787
765
  config: {
788
766
  textComponents: options == null ? void 0 : options.textComponents,
789
- createNodeMock: (_a = options == null ? void 0 : options.createNodeMock) != null ? _a : () => ({})
767
+ createNodeMock: (_a = options == null ? void 0 : options.createNodeMock) != null ? _a : (() => ({}))
790
768
  }
791
769
  };
792
770
  let containerFiber = TestReconciler.createContainer(
793
771
  container,
794
- import_constants4.ConcurrentRoot,
772
+ import_constants2.ConcurrentRoot,
795
773
  null,
796
774
  // hydration callbacks
797
775
  false,
@@ -824,33 +802,33 @@ function createRoot(options) {
824
802
  container = null;
825
803
  containerFiber = null;
826
804
  };
805
+ const getRoot = () => {
806
+ if (containerFiber == null || container == null) {
807
+ throw new Error("Can't access .root on unmounted test renderer");
808
+ }
809
+ if (container.children.length === 0) {
810
+ return null;
811
+ }
812
+ const firstChild = container.children[0];
813
+ if (firstChild.tag === "TEXT") {
814
+ throw new Error("Cannot render text as root element");
815
+ }
816
+ return HostElement.fromInstance(firstChild);
817
+ };
827
818
  return {
828
819
  render,
829
820
  unmount,
830
821
  get root() {
831
- if (containerFiber == null || container == null) {
832
- throw new Error("Can't access .root on unmounted test renderer");
833
- }
834
- if (container.children.length === 0) {
835
- return null;
836
- }
837
- const root = HostElement.fromInstance(container.children[0]);
838
- if (typeof root === "string") {
839
- throw new Error("Cannot render string as root element");
840
- }
841
- return root;
822
+ return getRoot();
842
823
  },
843
- get container() {
844
- if (containerFiber == null || container == null) {
845
- throw new Error("Can't access .container on unmounted test renderer");
846
- }
847
- return HostElement.fromContainer(container);
824
+ findAll: (predicate, options2) => {
825
+ const root = getRoot();
826
+ return root != null ? findAll(root, predicate, options2) : [];
848
827
  }
849
828
  };
850
829
  }
851
830
  // Annotate the CommonJS export names for ESM import in node:
852
831
  0 && (module.exports = {
853
- CONTAINER_TYPE,
854
832
  createRoot
855
833
  });
856
834
  //# sourceMappingURL=index.cjs.map