@react-three/postprocessing 3.0.3 → 3.0.5

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.
Files changed (93) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +81 -81
  3. package/dist/EffectComposer.d.ts +28 -0
  4. package/dist/Selection.d.ts +17 -0
  5. package/dist/effects/ASCII.d.ts +19 -0
  6. package/dist/effects/Autofocus.d.ts +23 -0
  7. package/dist/effects/Bloom.d.ts +2 -0
  8. package/dist/effects/BrightnessContrast.d.ts +2 -0
  9. package/dist/effects/ChromaticAberration.d.ts +8 -0
  10. package/dist/effects/ColorAverage.d.ts +7 -0
  11. package/dist/effects/ColorDepth.d.ts +2 -0
  12. package/dist/effects/Depth.d.ts +2 -0
  13. package/dist/effects/DepthOfField.d.ts +14 -0
  14. package/dist/effects/DotScreen.d.ts +2 -0
  15. package/dist/effects/FXAA.d.ts +2 -0
  16. package/dist/effects/Glitch.d.ts +13 -0
  17. package/dist/effects/GodRays.d.ts +9 -0
  18. package/dist/effects/Grid.d.ts +11 -0
  19. package/dist/effects/HueSaturation.d.ts +2 -0
  20. package/dist/effects/LUT.d.ts +10 -0
  21. package/dist/effects/LensFlare.d.ts +54 -0
  22. package/dist/effects/N8AO.d.ts +19 -0
  23. package/dist/effects/Noise.d.ts +2 -0
  24. package/dist/effects/Outline.d.ts +11 -0
  25. package/dist/effects/Pixelation.d.ts +7 -0
  26. package/dist/effects/Ramp.d.ts +74 -0
  27. package/dist/effects/SMAA.d.ts +2 -0
  28. package/dist/effects/SSAO.d.ts +7 -0
  29. package/dist/effects/ScanlineEffect.d.ts +2 -0
  30. package/dist/effects/SelectiveBloom.d.ts +15 -0
  31. package/dist/effects/Sepia.d.ts +2 -0
  32. package/dist/effects/ShockWave.d.ts +2 -0
  33. package/dist/effects/Texture.d.ts +10 -0
  34. package/dist/effects/TiltShift.d.ts +2 -0
  35. package/dist/effects/TiltShift2.d.ts +18 -0
  36. package/dist/effects/ToneMapping.d.ts +4 -0
  37. package/dist/effects/Vignette.d.ts +2 -0
  38. package/dist/effects/Water.d.ts +8 -0
  39. package/dist/index.d.ts +38 -1
  40. package/dist/index.js +855 -551
  41. package/dist/index.js.map +1 -1
  42. package/dist/tests/test-utils.d.ts +12 -0
  43. package/dist/util.d.ts +25 -0
  44. package/dist/wrapEffect.d.ts +12 -0
  45. package/package.json +73 -66
  46. package/src/EffectComposer.tsx +278 -200
  47. package/src/Selection.tsx +86 -46
  48. package/src/effects/ASCII.tsx +136 -135
  49. package/src/effects/Autofocus.tsx +175 -153
  50. package/src/effects/Bloom.tsx +6 -6
  51. package/src/effects/BrightnessContrast.tsx +4 -4
  52. package/src/effects/ChromaticAberration.tsx +30 -5
  53. package/src/effects/ColorAverage.tsx +17 -15
  54. package/src/effects/ColorDepth.tsx +4 -4
  55. package/src/effects/Depth.tsx +4 -4
  56. package/src/effects/DepthOfField.tsx +87 -89
  57. package/src/effects/DotScreen.tsx +4 -4
  58. package/src/effects/FXAA.tsx +4 -4
  59. package/src/effects/Glitch.tsx +37 -40
  60. package/src/effects/GodRays.tsx +20 -16
  61. package/src/effects/Grid.tsx +28 -21
  62. package/src/effects/HueSaturation.tsx +4 -4
  63. package/src/effects/LUT.tsx +27 -26
  64. package/src/effects/LensFlare.tsx +611 -611
  65. package/src/effects/N8AO.tsx +81 -81
  66. package/src/effects/Noise.tsx +4 -4
  67. package/src/effects/Outline.tsx +85 -117
  68. package/src/effects/Pixelation.tsx +17 -15
  69. package/src/effects/Ramp.tsx +150 -150
  70. package/src/effects/SMAA.tsx +4 -4
  71. package/src/effects/SSAO.tsx +44 -41
  72. package/src/effects/ScanlineEffect.tsx +7 -7
  73. package/src/effects/SelectiveBloom.tsx +116 -126
  74. package/src/effects/Sepia.tsx +4 -4
  75. package/src/effects/ShockWave.tsx +4 -4
  76. package/src/effects/Texture.tsx +27 -23
  77. package/src/effects/TiltShift.tsx +4 -4
  78. package/src/effects/TiltShift2.tsx +90 -90
  79. package/src/effects/ToneMapping.tsx +6 -6
  80. package/src/effects/Vignette.tsx +4 -4
  81. package/src/effects/Water.tsx +35 -35
  82. package/src/index.ts +41 -40
  83. package/src/tests/ChromaticAberration.test.tsx +31 -0
  84. package/src/tests/EffectComposer.test.tsx +911 -0
  85. package/src/tests/Outline.test.tsx +89 -0
  86. package/src/tests/Selection.test.tsx +324 -0
  87. package/src/tests/SelectiveBloom.test.tsx +118 -0
  88. package/src/tests/effects.smoke.test.tsx +234 -0
  89. package/src/tests/test-utils.tsx +96 -0
  90. package/src/tests/wrapEffect.test.tsx +209 -0
  91. package/src/util.tsx +96 -55
  92. package/src/wrapEffect.tsx +122 -0
  93. package/src/EffectComposer.test.tsx +0 -126
@@ -0,0 +1,89 @@
1
+ import { EffectComposer as EffectComposerImpl, OutlineEffect, Selection as PPSelection } from 'postprocessing'
2
+ import * as React from 'react'
3
+ import { Mesh, Object3D } from 'three'
4
+ import { afterEach, describe, expect, it, vi } from 'vitest'
5
+ import { EffectComposer } from '../EffectComposer'
6
+ import { Outline } from '../effects/Outline'
7
+ import { Select, Selection } from '../Selection'
8
+ import { flush, root, waitForComposer } from './test-utils'
9
+
10
+ afterEach(async () => {
11
+ await React.act(async () => {
12
+ root.render(null)
13
+ })
14
+ })
15
+
16
+ describe('Outline', () => {
17
+ it('does not re-set its (empty, declarative-mode) selection on unrelated re-renders', async () => {
18
+ const setSpy = vi.spyOn(PPSelection.prototype, 'set')
19
+ const composerRef = React.createRef<EffectComposerImpl>()
20
+
21
+ const render = (tick: number) =>
22
+ root.render(
23
+ <EffectComposer ref={composerRef}>
24
+ <Outline />
25
+ <group name={`tick-${tick}`} />
26
+ </EffectComposer>
27
+ )
28
+
29
+ await React.act(async () => render(0))
30
+ await waitForComposer(composerRef)
31
+ await flush()
32
+ setSpy.mockClear()
33
+
34
+ for (let t = 1; t <= 5; t++) {
35
+ await React.act(async () => render(t))
36
+ await flush()
37
+ }
38
+
39
+ expect(setSpy).not.toHaveBeenCalled()
40
+ setSpy.mockRestore()
41
+ })
42
+
43
+ it('sets its selection from the Selection/Select API and clears it when the object deselects', async () => {
44
+ const effectRef = React.createRef<OutlineEffect>()
45
+ const meshRef = React.createRef<Mesh>()
46
+
47
+ const render = (enabled: boolean) =>
48
+ root.render(
49
+ <EffectComposer>
50
+ <Selection>
51
+ <Select enabled={enabled}>
52
+ <mesh ref={meshRef}>
53
+ <boxGeometry />
54
+ <meshBasicMaterial />
55
+ </mesh>
56
+ </Select>
57
+ <Outline ref={effectRef} />
58
+ </Selection>
59
+ </EffectComposer>
60
+ )
61
+
62
+ await React.act(async () => render(true))
63
+ await flush()
64
+ await flush()
65
+
66
+ expect(Array.from(effectRef.current!.selection)).toContain(meshRef.current)
67
+
68
+ await React.act(async () => render(false))
69
+ await flush()
70
+ await flush()
71
+
72
+ expect(Array.from(effectRef.current!.selection)).not.toContain(meshRef.current)
73
+ })
74
+
75
+ it('does not throw when a selection ref has not attached yet', async () => {
76
+ const composerRef = React.createRef<EffectComposerImpl>()
77
+ const unattachedRef = React.createRef<Object3D>()
78
+
79
+ await React.act(async () =>
80
+ root.render(
81
+ <EffectComposer ref={composerRef}>
82
+ <Outline selection={[unattachedRef]} />
83
+ </EffectComposer>
84
+ )
85
+ )
86
+ await waitForComposer(composerRef)
87
+ await expect(flush()).resolves.not.toThrow()
88
+ })
89
+ })
@@ -0,0 +1,324 @@
1
+ import * as React from 'react'
2
+ import * as THREE from 'three'
3
+ import { afterEach, describe, expect, it } from 'vitest'
4
+ import { Select, Selection, selectionContext } from '../Selection'
5
+ import { flush, root } from './test-utils'
6
+
7
+ afterEach(async () => {
8
+ await React.act(async () => {
9
+ root.render(null)
10
+ })
11
+ })
12
+
13
+ function Capture({ onSnapshot }: { onSnapshot: (selected: THREE.Object3D[]) => void }) {
14
+ const api = React.useContext(selectionContext)
15
+ React.useEffect(() => {
16
+ onSnapshot(api?.selected ?? [])
17
+ })
18
+ return null
19
+ }
20
+
21
+ describe('Selection/Select', () => {
22
+ it('settles to a stable selected array without looping', async () => {
23
+ const snapshots: THREE.Object3D[][] = []
24
+ const meshRef = React.createRef<THREE.Mesh>()
25
+
26
+ await React.act(async () =>
27
+ root.render(
28
+ <Selection>
29
+ <Capture onSnapshot={(s) => snapshots.push(s)} />
30
+ <Select enabled>
31
+ <mesh ref={meshRef}>
32
+ <boxGeometry />
33
+ <meshBasicMaterial />
34
+ </mesh>
35
+ </Select>
36
+ </Selection>
37
+ )
38
+ )
39
+ await flush()
40
+ await flush()
41
+
42
+ // Exactly one render for the initial (empty) commit, one for the
43
+ // single necessary addition - the original bug kept re-triggering
44
+ // itself, so this would grow unbounded (and eventually throw) instead
45
+ // of stopping at 2.
46
+ expect(snapshots).toEqual([[], [meshRef.current]])
47
+ })
48
+
49
+ it('selects a Line and a Points object, not just Mesh', async () => {
50
+ const snapshots: THREE.Object3D[][] = []
51
+ const lineRef = React.createRef<THREE.Line>()
52
+ const pointsRef = React.createRef<THREE.Points>()
53
+
54
+ await React.act(async () =>
55
+ root.render(
56
+ <Selection>
57
+ <Capture onSnapshot={(s) => snapshots.push(s)} />
58
+ <Select enabled>
59
+ <threeLine ref={lineRef}>
60
+ <bufferGeometry />
61
+ <lineBasicMaterial />
62
+ </threeLine>
63
+ </Select>
64
+ <Select enabled>
65
+ <points ref={pointsRef}>
66
+ <bufferGeometry />
67
+ <pointsMaterial />
68
+ </points>
69
+ </Select>
70
+ </Selection>
71
+ )
72
+ )
73
+ await flush()
74
+ await flush()
75
+
76
+ const last = snapshots[snapshots.length - 1]
77
+ expect(last).toContain(lineRef.current)
78
+ expect(last).toContain(pointsRef.current)
79
+ })
80
+
81
+ it('removes its objects once unmounted, leaving a sibling Select untouched', async () => {
82
+ const snapshots: THREE.Object3D[][] = []
83
+ const meshARef = React.createRef<THREE.Mesh>()
84
+ const meshBRef = React.createRef<THREE.Mesh>()
85
+
86
+ const render = (mountA: boolean) =>
87
+ root.render(
88
+ <Selection>
89
+ <Capture onSnapshot={(s) => snapshots.push(s)} />
90
+ {mountA && (
91
+ <Select key="a" enabled>
92
+ <mesh ref={meshARef}>
93
+ <boxGeometry />
94
+ <meshBasicMaterial />
95
+ </mesh>
96
+ </Select>
97
+ )}
98
+ <Select key="b" enabled>
99
+ <mesh ref={meshBRef}>
100
+ <boxGeometry />
101
+ <meshBasicMaterial />
102
+ </mesh>
103
+ </Select>
104
+ </Selection>
105
+ )
106
+
107
+ await React.act(async () => render(true))
108
+ await flush()
109
+ await flush()
110
+ expect(snapshots[snapshots.length - 1]).toEqual(expect.arrayContaining([meshARef.current, meshBRef.current]))
111
+
112
+ await React.act(async () => render(false))
113
+ await flush()
114
+ await flush()
115
+
116
+ const last = snapshots[snapshots.length - 1]
117
+ expect(last).not.toContain(meshARef.current)
118
+ expect(last).toContain(meshBRef.current)
119
+ })
120
+
121
+ it('removes its objects when enabled toggles to false', async () => {
122
+ const snapshots: THREE.Object3D[][] = []
123
+ const meshRef = React.createRef<THREE.Mesh>()
124
+
125
+ const render = (enabled: boolean) =>
126
+ root.render(
127
+ <Selection>
128
+ <Capture onSnapshot={(s) => snapshots.push(s)} />
129
+ <Select enabled={enabled}>
130
+ <mesh ref={meshRef}>
131
+ <boxGeometry />
132
+ <meshBasicMaterial />
133
+ </mesh>
134
+ </Select>
135
+ </Selection>
136
+ )
137
+
138
+ await React.act(async () => render(true))
139
+ await flush()
140
+ await flush()
141
+ expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
142
+
143
+ await React.act(async () => render(false))
144
+ await flush()
145
+ await flush()
146
+ expect(snapshots[snapshots.length - 1]).not.toContain(meshRef.current)
147
+ })
148
+
149
+ it('selects a SkinnedMesh - isMesh is inherited, not just the exact type string', async () => {
150
+ const snapshots: THREE.Object3D[][] = []
151
+ const meshRef = React.createRef<THREE.SkinnedMesh>()
152
+
153
+ await React.act(async () =>
154
+ root.render(
155
+ <Selection>
156
+ <Capture onSnapshot={(s) => snapshots.push(s)} />
157
+ <Select enabled>
158
+ <skinnedMesh ref={meshRef} />
159
+ </Select>
160
+ </Selection>
161
+ )
162
+ )
163
+ await flush()
164
+ await flush()
165
+
166
+ expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
167
+ })
168
+
169
+ it('does not duplicate an object claimed by nested Selects', async () => {
170
+ const snapshots: THREE.Object3D[][] = []
171
+ const meshRef = React.createRef<THREE.Mesh>()
172
+
173
+ await React.act(async () =>
174
+ root.render(
175
+ <Selection>
176
+ <Capture onSnapshot={(s) => snapshots.push(s)} />
177
+ <Select enabled>
178
+ <Select enabled>
179
+ <mesh ref={meshRef}>
180
+ <boxGeometry />
181
+ <meshBasicMaterial />
182
+ </mesh>
183
+ </Select>
184
+ </Select>
185
+ </Selection>
186
+ )
187
+ )
188
+ await flush()
189
+ await flush()
190
+ await flush()
191
+
192
+ const last = snapshots[snapshots.length - 1]
193
+ expect(last.filter((o) => o === meshRef.current)).toHaveLength(1)
194
+ })
195
+
196
+ it('keeps an object selected via the outer Select after the inner one disables', async () => {
197
+ const snapshots: THREE.Object3D[][] = []
198
+ const meshRef = React.createRef<THREE.Mesh>()
199
+
200
+ const render = (innerEnabled: boolean) =>
201
+ root.render(
202
+ <Selection>
203
+ <Capture onSnapshot={(s) => snapshots.push(s)} />
204
+ <Select enabled>
205
+ <Select enabled={innerEnabled}>
206
+ <mesh ref={meshRef}>
207
+ <boxGeometry />
208
+ <meshBasicMaterial />
209
+ </mesh>
210
+ </Select>
211
+ </Select>
212
+ </Selection>
213
+ )
214
+
215
+ await React.act(async () => render(true))
216
+ await flush()
217
+ await flush()
218
+ await flush()
219
+ expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
220
+
221
+ await React.act(async () => render(false))
222
+ await flush()
223
+ await flush()
224
+ await flush()
225
+
226
+ // The inner Select's own cleanup drops its claim, but the mesh never
227
+ // left the outer Select's subtree - its own traversal still finds it.
228
+ expect(snapshots[snapshots.length - 1]).toContain(meshRef.current)
229
+ })
230
+
231
+ it('does not change the selected array reference on a re-render where children only changes identity', async () => {
232
+ const refs: THREE.Object3D[][] = []
233
+ const meshRef = React.createRef<THREE.Mesh>()
234
+
235
+ function CaptureRef() {
236
+ const api = React.useContext(selectionContext)
237
+ React.useEffect(() => {
238
+ if (api) refs.push(api.selected)
239
+ })
240
+ return null
241
+ }
242
+
243
+ // A fresh JSX tree every call, like a parent re-rendering for an
244
+ // unrelated reason - Select's own `children` prop is a new reference
245
+ // each time even though the underlying mesh never changes.
246
+ const render = () =>
247
+ root.render(
248
+ <Selection>
249
+ <CaptureRef />
250
+ <Select enabled>
251
+ <mesh ref={meshRef}>
252
+ <boxGeometry />
253
+ <meshBasicMaterial />
254
+ </mesh>
255
+ </Select>
256
+ </Selection>
257
+ )
258
+
259
+ await React.act(async () => render())
260
+ await flush()
261
+ await flush()
262
+
263
+ refs.length = 0
264
+ for (let i = 0; i < 5; i++) {
265
+ await React.act(async () => render())
266
+ await flush()
267
+ }
268
+
269
+ expect(new Set(refs).size).toBe(1)
270
+ })
271
+
272
+ it('a cleanup updater returns the same reference when nothing is left for it to remove', async () => {
273
+ const meshRef = React.createRef<THREE.Mesh>()
274
+ const updaters: Array<(prev: THREE.Object3D[]) => THREE.Object3D[]> = []
275
+
276
+ // React batches both nested Selects' cleanup calls into a single
277
+ // re-render regardless of this bail-out (the net change from mount to
278
+ // unmount is real), so the optimization isn't observable through
279
+ // rendering alone. Capture the raw updater functions instead and
280
+ // replay them directly to verify the second one bails.
281
+ function CapturingSelection({ children }: { children: React.ReactNode }) {
282
+ const [selected, setSelected] = React.useState<THREE.Object3D[]>([])
283
+ const select = React.useCallback((updater: React.SetStateAction<THREE.Object3D[]>) => {
284
+ if (typeof updater === 'function') updaters.push(updater as (prev: THREE.Object3D[]) => THREE.Object3D[])
285
+ setSelected(updater)
286
+ }, [])
287
+ const value = React.useMemo(() => ({ selected, select, enabled: true }), [selected, select])
288
+ return <selectionContext.Provider value={value}>{children}</selectionContext.Provider>
289
+ }
290
+
291
+ const render = (mounted: boolean) =>
292
+ root.render(
293
+ <CapturingSelection>
294
+ {mounted && (
295
+ <Select enabled>
296
+ <Select enabled>
297
+ <mesh ref={meshRef}>
298
+ <boxGeometry />
299
+ <meshBasicMaterial />
300
+ </mesh>
301
+ </Select>
302
+ </Select>
303
+ )}
304
+ </CapturingSelection>
305
+ )
306
+
307
+ await React.act(async () => render(true))
308
+ await flush()
309
+ await flush()
310
+ await flush()
311
+
312
+ const mesh = meshRef.current!
313
+ updaters.length = 0
314
+ await React.act(async () => render(false))
315
+ await flush()
316
+ await flush()
317
+ await flush()
318
+
319
+ expect(updaters).toHaveLength(2)
320
+ const afterFirst = updaters[0]([mesh])
321
+ const afterSecond = updaters[1](afterFirst)
322
+ expect(afterSecond).toBe(afterFirst)
323
+ })
324
+ })
@@ -0,0 +1,118 @@
1
+ import { EffectComposer as EffectComposerImpl, SelectiveBloomEffect } from 'postprocessing'
2
+ import * as React from 'react'
3
+ import { Mesh, Object3D, PointLight } from 'three'
4
+ import { afterEach, describe, expect, it } from 'vitest'
5
+ import { EffectComposer } from '../EffectComposer'
6
+ import { SelectiveBloom } from '../effects/SelectiveBloom'
7
+ import { Select, Selection } from '../Selection'
8
+ import { flush, root, waitForComposer } from './test-utils'
9
+
10
+ afterEach(async () => {
11
+ await React.act(async () => {
12
+ root.render(null)
13
+ })
14
+ })
15
+
16
+ describe('SelectiveBloom', () => {
17
+ it('does not reconstruct the effect (with its GPU resources) on unrelated re-renders', async () => {
18
+ const composerRef = React.createRef<EffectComposerImpl>()
19
+ const effectRef = React.createRef<SelectiveBloomEffect>()
20
+ const light = new PointLight()
21
+
22
+ const render = (tick: number) =>
23
+ root.render(
24
+ <EffectComposer ref={composerRef}>
25
+ <SelectiveBloom ref={effectRef} lights={[light]} intensity={2} />
26
+ <group name={`tick-${tick}`} />
27
+ </EffectComposer>
28
+ )
29
+
30
+ await React.act(async () => render(0))
31
+ await waitForComposer(composerRef)
32
+ await flush()
33
+ const first = effectRef.current
34
+ expect(first).toBeTruthy()
35
+
36
+ for (let t = 1; t <= 5; t++) {
37
+ await React.act(async () => render(t))
38
+ await flush()
39
+ }
40
+
41
+ expect(effectRef.current).toBe(first)
42
+ })
43
+
44
+ it('sets its selection from the Selection/Select API and clears it when the object deselects', async () => {
45
+ const effectRef = React.createRef<SelectiveBloomEffect>()
46
+ const meshRef = React.createRef<Mesh>()
47
+ const light = new PointLight()
48
+
49
+ const render = (enabled: boolean) =>
50
+ root.render(
51
+ <EffectComposer>
52
+ <Selection>
53
+ <Select enabled={enabled}>
54
+ <mesh ref={meshRef}>
55
+ <boxGeometry />
56
+ <meshBasicMaterial />
57
+ </mesh>
58
+ </Select>
59
+ <SelectiveBloom ref={effectRef} lights={[light]} />
60
+ </Selection>
61
+ </EffectComposer>
62
+ )
63
+
64
+ await React.act(async () => render(true))
65
+ await flush()
66
+ await flush()
67
+
68
+ expect(Array.from(effectRef.current!.selection)).toContain(meshRef.current)
69
+
70
+ await React.act(async () => render(false))
71
+ await flush()
72
+ await flush()
73
+
74
+ expect(Array.from(effectRef.current!.selection)).not.toContain(meshRef.current)
75
+ })
76
+
77
+ it('moves lights to the new render layer when selectionLayer changes', async () => {
78
+ const composerRef = React.createRef<EffectComposerImpl>()
79
+ const effectRef = React.createRef<SelectiveBloomEffect>()
80
+ const light = new PointLight()
81
+ const onLayer = (n: number) => light.layers.test({ mask: 1 << n } as never)
82
+
83
+ const render = (layer: number) =>
84
+ root.render(
85
+ <EffectComposer ref={composerRef}>
86
+ <SelectiveBloom ref={effectRef} lights={[light]} selectionLayer={layer} />
87
+ </EffectComposer>
88
+ )
89
+
90
+ await React.act(async () => render(10))
91
+ await waitForComposer(composerRef)
92
+ await flush()
93
+ await flush()
94
+ expect(onLayer(10)).toBe(true)
95
+
96
+ await React.act(async () => render(15))
97
+ await flush()
98
+ await flush()
99
+
100
+ expect(onLayer(15)).toBe(true)
101
+ expect(onLayer(10)).toBe(false)
102
+ })
103
+
104
+ it('does not throw when a lights ref has not attached yet', async () => {
105
+ const composerRef = React.createRef<EffectComposerImpl>()
106
+ const unattachedRef = React.createRef<Object3D>()
107
+
108
+ await React.act(async () =>
109
+ root.render(
110
+ <EffectComposer ref={composerRef}>
111
+ <SelectiveBloom lights={[unattachedRef]} />
112
+ </EffectComposer>
113
+ )
114
+ )
115
+ await waitForComposer(composerRef)
116
+ await expect(flush()).resolves.not.toThrow()
117
+ })
118
+ })