react-component-snapshot 0.0.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 ADDED
@@ -0,0 +1,18 @@
1
+ # react-component-snapshot
2
+
3
+ Transform a React component to match its snapshot in Jest
4
+
5
+ ```javascript
6
+ import snapshotOf from 'react-component-snapshot'
7
+
8
+ const {
9
+ container: {
10
+ firstElementChild
11
+ }
12
+ } = render(
13
+ <Component />
14
+ )
15
+
16
+ expect(snapshotOf(element))
17
+ .toMatchSnapshot()
18
+ ```
package/index.d.mts ADDED
@@ -0,0 +1,74 @@
1
+ import type React from 'react'
2
+
3
+ declare global {
4
+ namespace ReactComponentSnapshotTypes {
5
+ /**
6
+ * https://github.com/bendtherules/react-fiber-traverse
7
+ */
8
+
9
+ export type FiberNode =
10
+ | FiberNodeForClassComponent
11
+ | FiberNodeForFunctionComponent
12
+ | FiberNodeForInstrinsicElement
13
+ | FiberNodeForTextNode
14
+
15
+ export interface FiberRootNode {
16
+ child: FiberNode
17
+ sibling: FiberNode | null
18
+
19
+ elementType: React.FunctionComponent
20
+ type: React.FunctionComponent
21
+
22
+ stateNode: {
23
+ current: FiberRootNode
24
+ }
25
+ return?: FiberRootNode // | null
26
+ }
27
+
28
+ export interface FiberNodeForFunctionComponent {
29
+ child: FiberNode | null
30
+ sibling: FiberNode | null
31
+
32
+ elementType: React.FunctionComponent
33
+ type: React.FunctionComponent
34
+
35
+ stateNode: null
36
+ return?: FiberNode // | null
37
+ }
38
+
39
+ export interface FiberNodeForClassComponent {
40
+ child: FiberNode | null
41
+ sibling: FiberNode | null
42
+
43
+ elementType: React.ComponentClass
44
+ type: React.ComponentClass
45
+
46
+ stateNode: React.Component
47
+ return?: FiberNode // | null
48
+ }
49
+
50
+ export interface FiberNodeForInstrinsicElement {
51
+ child: FiberNode | null
52
+ sibling: FiberNode | null
53
+
54
+ elementType: keyof React.JSX.IntrinsicElements
55
+ type: keyof React.JSX.IntrinsicElements
56
+
57
+ stateNode: HTMLElement
58
+ return?: FiberNode
59
+ }
60
+
61
+ export interface FiberNodeForTextNode {
62
+ child: null
63
+ sibling: FiberNode | null
64
+
65
+ elementType: null
66
+ type: null
67
+
68
+ stateNode: Text
69
+ return?: FiberNode
70
+ }
71
+ }
72
+ }
73
+
74
+ export {}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "react-component-snapshot",
3
+ "version": "0.0.0",
4
+ "description": "Transform a React component to match its snapshot in Jest",
5
+ "keywords": [
6
+ "react",
7
+ "component",
8
+ "snapshot"
9
+ ],
10
+ "main": "./src/index.mjs",
11
+ "type": "module",
12
+ "types": "./index.d.mts",
13
+ "author": {
14
+ "name": "Jonathan Perry for Modern Poacher Limited",
15
+ "email": "jonathanperry@modernpoacher.com",
16
+ "url": "https://modernpoacher.com"
17
+ },
18
+ "license": "ISC",
19
+ "engines": {
20
+ "node": ">=18.12.0 <=21.7.3 || >=22.2.0"
21
+ },
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git://github.com/modernpoacher/react-component-snapshot.git"
25
+ },
26
+ "scripts": {
27
+ "lint": "eslint",
28
+ "lint:fix": "npm run lint -- --fix",
29
+ "test": "NODE_ENV=test jest"
30
+ },
31
+ "dependencies": {
32
+ "debug": "^4.4.0"
33
+ },
34
+ "devDependencies": {
35
+ "@sequencemedia/eslint-config-standard": "^0.2.43",
36
+ "@sequencemedia/eslint-config-typescript": "^0.1.62",
37
+ "@types/debug": "^4.1.12",
38
+ "@types/react": "19.0.11",
39
+ "@typescript-eslint/parser": "^8.26.1",
40
+ "eslint": "^9.22.0",
41
+ "globals": "^16.0.0",
42
+ "react": "19.0.0"
43
+ },
44
+ "imports": {
45
+ "#react-component-snapshot": "./src/index.mjs"
46
+ },
47
+ "exports": {
48
+ ".": "./src/index.mjs"
49
+ }
50
+ }
package/src/index.mjs ADDED
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @typedef {ReactComponentSnapshotTypes.FiberNode} FiberNode
3
+ */
4
+
5
+ import {
6
+ getType
7
+ } from './react.mjs'
8
+
9
+ const REACT_COMPONENT_SNAPSHOT = Symbol.for('react.test.json')
10
+
11
+ /**
12
+ * @param {PropertyKey} key
13
+ * @returns {boolean}
14
+ */
15
+ export function isReactFiberKey (key) {
16
+ return String(key).startsWith('__reactFiber$')
17
+ }
18
+
19
+ /**
20
+ * @param {PropertyKey} key
21
+ * @returns {boolean}
22
+ */
23
+ export function isReactPropsKey (key) {
24
+ return String(key).startsWith('__reactProps$')
25
+ }
26
+
27
+ /**
28
+ * @param {Element | Text | React.Component<any, any, any>} stateNode
29
+ * @returns {string | undefined}
30
+ */
31
+ export function getReactFiberKey (stateNode) {
32
+ return (
33
+ Object.keys(stateNode)
34
+ .find(isReactFiberKey)
35
+ )
36
+ }
37
+
38
+ /**
39
+ * @param {Element | Text | React.Component<any, any, any>} stateNode
40
+ * @returns {string | undefined}
41
+ */
42
+ export function getReactPropsKey (stateNode) {
43
+ return (
44
+ Object.keys(stateNode)
45
+ .find(isReactPropsKey)
46
+ )
47
+ }
48
+
49
+ /**
50
+ * @param {Element | Text | React.Component<any, any, any>} stateNode
51
+ * @returns {Record<PropertyKey, unknown> | null}
52
+ */
53
+ export function getStateNodeFiber (stateNode) {
54
+ const key = getReactFiberKey(stateNode)
55
+
56
+ if (key) {
57
+ const { // @ts-expect-error
58
+ [key]: fiber = null
59
+ } = stateNode
60
+
61
+ return fiber
62
+ }
63
+
64
+ return null
65
+ }
66
+
67
+ /**
68
+ * @param {React.Component<any, any, any> | Element | Text} stateNode
69
+ * @returns {Record<PropertyKey, unknown> | null}
70
+ */
71
+ export function getStateNodeProps (stateNode) {
72
+ const key = getReactPropsKey(stateNode)
73
+
74
+ if (key) {
75
+ const { // @ts-expect-error
76
+ [key]: props = null
77
+ } = stateNode
78
+
79
+ return props
80
+ }
81
+
82
+ return null
83
+ }
84
+
85
+ /**
86
+ * @param {Element} element
87
+ * @returns {FiberNode | null}
88
+ */
89
+ function getFiber (element) {
90
+ const key = getReactFiberKey(element)
91
+
92
+ if (key) {
93
+ const { // @ts-expect-error
94
+ [key]: fiber = null
95
+ } = element
96
+
97
+ return fiber
98
+ }
99
+
100
+ return null
101
+ }
102
+
103
+ /**
104
+ * @param {FiberNode} fiberNode
105
+ * @returns {FiberNode[]}
106
+ */
107
+ function getSiblings (fiberNode) {
108
+ const siblings = []
109
+
110
+ let sibling = fiberNode.sibling
111
+ while (sibling) {
112
+ siblings.push(sibling)
113
+ sibling = sibling.sibling
114
+ }
115
+
116
+ return siblings
117
+ }
118
+
119
+ /**
120
+ * @param {FiberNode} fiberNode
121
+ * @returns {Record<PropertyKey, unknown>}
122
+ */
123
+ function getProps (fiberNode) {
124
+ const stateNode = fiberNode.stateNode
125
+
126
+ /**
127
+ * State node may be null on class components
128
+ */
129
+ if (stateNode) {
130
+ const props = getStateNodeProps(stateNode) ?? {}
131
+
132
+ return (
133
+ Object.fromEntries(
134
+ Object.entries(props)
135
+ .filter(([key]) => key !== 'children')
136
+ .filter(([key, value]) => { // @ts-expect-error
137
+ if ((value || false) instanceof Object && !Array.isArray(value)) return !('current' in value)
138
+ return true
139
+ })
140
+ )
141
+ )
142
+ }
143
+
144
+ return {}
145
+ }
146
+
147
+ /**
148
+ * @param {FiberNode} fiberNode
149
+ * @returns {Array<string | FiberNode>}
150
+ */
151
+ function getChildren (fiberNode) {
152
+ const child = fiberNode.child
153
+
154
+ if (child) {
155
+ const siblings = getSiblings(child)
156
+
157
+ return [child].concat(siblings).map((child) => {
158
+ const stateNode = child.stateNode
159
+
160
+ if (stateNode instanceof Text) {
161
+ return stateNode.data
162
+ }
163
+
164
+ return child
165
+ })
166
+ }
167
+
168
+ const stateNode = fiberNode.stateNode
169
+
170
+ if (stateNode) {
171
+ /**
172
+ * Get the text as a prop from the instance
173
+ */
174
+ const {
175
+ children
176
+ } = getStateNodeProps(stateNode) ?? {}
177
+
178
+ if (typeof children === 'string') {
179
+ return [children]
180
+ }
181
+ }
182
+
183
+ return []
184
+ }
185
+
186
+ /**
187
+ * @param {FiberNode | string} child
188
+ * @returns {string | Record<PropertyKey, unknown>}
189
+ */
190
+ function toChildren (child) {
191
+ if (typeof child === 'string') return child
192
+
193
+ return (
194
+ transform(child)
195
+ )
196
+ }
197
+
198
+ /**
199
+ * @param {FiberNode} fiberNode
200
+ * @returns {Record<PropertyKey, unknown>}
201
+ */
202
+ function transform (fiberNode) {
203
+ const type = getType(fiberNode)
204
+ const props = getProps(fiberNode)
205
+ const children = (
206
+ getChildren(fiberNode)
207
+ .map(toChildren)
208
+ )
209
+
210
+ return (
211
+ Object.defineProperty({
212
+ type,
213
+ props,
214
+ children
215
+ },
216
+ '$$typeof', {
217
+ value: REACT_COMPONENT_SNAPSHOT
218
+ })
219
+ )
220
+ }
221
+
222
+ /**
223
+ * @param {Element | null | undefined} element
224
+ * @returns {Record<PropertyKey, unknown> | null}
225
+ */
226
+ export default function snapshotOf (element) {
227
+ if (element instanceof HTMLElement) {
228
+ const fiberNode = getFiber(element)
229
+
230
+ if (fiberNode) {
231
+ return (
232
+ transform(fiberNode)
233
+ )
234
+ }
235
+ }
236
+
237
+ return null
238
+ }
package/src/react.mjs ADDED
@@ -0,0 +1,185 @@
1
+ // @ts-nocheck
2
+
3
+ import debug from 'debug'
4
+
5
+ const log = debug('react-component-snapshot')
6
+
7
+ // export const REACT_LEGACY_ELEMENT_TYPE = Symbol.for('react.element')
8
+ // export const REACT_ELEMENT_TYPE = Symbol.for('react.transitional.element')
9
+ export const REACT_PORTAL_TYPE = Symbol.for('react.portal')
10
+ export const REACT_FRAGMENT_TYPE = Symbol.for('react.fragment')
11
+ export const REACT_STRICT_MODE_TYPE = Symbol.for('react.strict_mode')
12
+ export const REACT_PROFILER_TYPE = Symbol.for('react.profiler')
13
+ // export const REACT_PROVIDER_TYPE = Symbol.for('react.provider')
14
+ export const REACT_CONSUMER_TYPE = Symbol.for('react.consumer')
15
+ export const REACT_CONTEXT_TYPE = Symbol.for('react.context')
16
+ export const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref')
17
+ export const REACT_SUSPENSE_TYPE = Symbol.for('react.suspense')
18
+ export const REACT_SUSPENSE_LIST_TYPE = Symbol.for('react.suspense_list')
19
+ export const REACT_MEMO_TYPE = Symbol.for('react.memo')
20
+ export const REACT_LAZY_TYPE = Symbol.for('react.lazy')
21
+ // Symbol.for('react.scope')
22
+ // Symbol.for('react.debug_trace_mode')
23
+ export const REACT_OFFSCREEN_TYPE = Symbol.for('react.offscreen')
24
+ // Symbol.for('react.legacy_hidden')
25
+ // Symbol.for('react.tracing_marker')
26
+ // export const REACT_MEMO_CACHE_SENTINEL = Symbol.for('react.memo_cache_sentinel')
27
+ // export const MAYBE_ITERATOR_SYMBOL = Symbol.iterator
28
+ export const REACT_CLIENT_REFERENCE = Symbol.for('react.client.reference')
29
+
30
+ export function getNameFromType (type) {
31
+ if (type == null) return null
32
+
33
+ if (typeof type === 'function') {
34
+ return (
35
+ type.$$typeof === REACT_CLIENT_REFERENCE
36
+ ? null
37
+ : type.displayName || type.name || null
38
+ )
39
+ }
40
+
41
+ if (typeof type === 'string') return type
42
+
43
+ switch (type) {
44
+ case REACT_FRAGMENT_TYPE:
45
+ return 'Fragment'
46
+ case REACT_PORTAL_TYPE:
47
+ return 'Portal'
48
+ case REACT_PROFILER_TYPE:
49
+ return 'Profiler'
50
+ case REACT_STRICT_MODE_TYPE:
51
+ return 'StrictMode'
52
+ case REACT_SUSPENSE_TYPE:
53
+ return 'Suspense'
54
+ case REACT_SUSPENSE_LIST_TYPE:
55
+ return 'SuspenseList'
56
+ }
57
+
58
+ if (typeof type === 'object') {
59
+ switch (type.$$typeof) {
60
+ case REACT_CONTEXT_TYPE:
61
+ return (type.displayName || 'Context') + '.Provider'
62
+ case REACT_CONSUMER_TYPE:
63
+ return (type._context.displayName || 'Context') + '.Consumer'
64
+ case REACT_FORWARD_REF_TYPE:
65
+ {
66
+ const innerType = type.render
67
+ type = type.displayName
68
+
69
+ if (type) return type
70
+
71
+ type = innerType.displayName || innerType.name || ''
72
+
73
+ return (
74
+ type !== ''
75
+ ? `ForwardRef(${type})`
76
+ : 'ForwardRef'
77
+ )
78
+ }
79
+ case REACT_MEMO_TYPE:
80
+ {
81
+ const innerType = type.displayName || null
82
+
83
+ return (
84
+ innerType !== null
85
+ ? innerType
86
+ : getNameFromType(type.type) || 'Memo'
87
+ )
88
+ }
89
+ case REACT_LAZY_TYPE:
90
+ {
91
+ const innerType = type._payload
92
+ type = type._init
93
+
94
+ try {
95
+ return getNameFromType(type(innerType))
96
+ } catch (message) {
97
+ log(`Lazy error. Message was "${message}"`)
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return null
104
+ }
105
+
106
+ export function getType (fiberNode) {
107
+ const type = fiberNode.type
108
+
109
+ switch (fiberNode.tag) {
110
+ case 24:
111
+ return 'Cache'
112
+ case 9:
113
+ return (type._context.displayName || 'Context') + '.Consumer'
114
+ case 10:
115
+ return (type.displayName || 'Context') + '.Provider'
116
+ case 18:
117
+ return 'DehydratedFragment'
118
+ case 11:
119
+ {
120
+ return (
121
+ (fiberNode = type.render),
122
+ (fiberNode = fiberNode.displayName || fiberNode.name || ''),
123
+ type.displayName ||
124
+ (fiberNode !== '' ? `ForwardRef(${fiberNode})` : 'ForwardRef')
125
+ )
126
+ }
127
+ case 7:
128
+ return 'Fragment'
129
+ case 26:
130
+ case 27:
131
+ case 5:
132
+ return type
133
+ case 4:
134
+ return 'Portal'
135
+ case 3:
136
+ return 'Root'
137
+ case 6:
138
+ return 'Text'
139
+ case 16:
140
+ return getNameFromType(type)
141
+ case 8:
142
+ return (
143
+ type === REACT_STRICT_MODE_TYPE
144
+ ? 'StrictMode'
145
+ : 'Mode'
146
+ )
147
+ case 22:
148
+ return 'Offscreen'
149
+ case 12:
150
+ return 'Profiler'
151
+ case 21:
152
+ return 'Scope'
153
+ case 13:
154
+ return 'Suspense'
155
+ case 19:
156
+ return 'SuspenseList'
157
+ case 25:
158
+ return 'TracingMarker'
159
+ case 1:
160
+ case 0:
161
+ case 14:
162
+ case 15:
163
+ if (typeof type === 'function') return type.displayName || type.name || null
164
+ if (typeof type === 'string') return type
165
+ break
166
+ case 29:
167
+ {
168
+ const type = fiberNode._debugInfo || null
169
+
170
+ if (type != null) {
171
+ let i = type.length - 1
172
+
173
+ for (i; i >= 0; i--) {
174
+ const t = type[i]
175
+ const n = t.name
176
+
177
+ if (typeof n === 'string') return n
178
+ }
179
+ }
180
+
181
+ if (fiberNode.return !== null) return getType(fiberNode.return)
182
+ }
183
+ }
184
+ return null
185
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "NodeNext",
4
+ "target": "ESNext",
5
+ "allowJs": true,
6
+ "checkJs": true,
7
+ "noEmit": true,
8
+ "strict": true,
9
+ "jsx": "react"
10
+ },
11
+ "include": [
12
+ "."
13
+ ]
14
+ }