1
0
Эх сурвалжийг харах

test(nodeOps): improve test coverage

Peter 1 жил өмнө
parent
commit
6fd8c08b54
1 өөрчлөгдсөн 229 нэмэгдсэн , 14 устгасан
  1. 229 14
      src/core/nodeOps.test.ts

+ 229 - 14
src/core/nodeOps.test.ts

@@ -1,18 +1,39 @@
-import { beforeAll, describe, expect, it } from 'vitest'
+import { beforeAll, describe, expect, it, vi } from 'vitest'
 import * as THREE from 'three'
+import type { Vector3 } from 'three'
 import { Mesh, Scene } from 'three'
 import type { TresObject } from '../types'
 import { nodeOps as getNodeOps } from './nodeOps'
 import { extend } from './catalogue'
 
 let nodeOps = getNodeOps()
+const pool = []
 
 describe('nodeOps', () => {
   beforeAll(() => {
-    // Setup
     extend(THREE)
     nodeOps = getNodeOps()
+    const ce = nodeOps.createElement
+    // NOTE: Overwrite createElement in order to push
+    // all objects into a pool, later to be disposed.
+    nodeOps.createElement = (a, b, c, d) => {
+      const v = ce(a, b, c, d)
+      pool.push(v)
+      return v
+    }
+  },
+  )
+
+  afterAll(() => {
+    // NOTE: Dispose disposable objects.
+    for (const obj of pool) {
+      if (obj && 'dispose' in obj && typeof obj.dispose === 'function') {
+        obj.dispose()
+      }
+    }
+    pool.length = 0
   })
+
   describe('createElement', () => {
     it('creates an instance with given tag', async () => {
     // Setup
@@ -73,6 +94,40 @@ describe('nodeOps', () => {
       expect(consoleWarnSpy).toHaveBeenCalled()
     })
 
+    it('throws an error if passed a "primitive" tag without an "object" prop', () => {
+      expect(() => {
+        nodeOps.createElement('primitive', undefined, undefined, {})
+      }).toThrowError()
+    })
+
+    it('returns null if passed the tag "template"', () => {
+      expect(nodeOps.createElement('template', undefined, undefined, {})).equals(null)
+    })
+
+    it('returns null if passed an HTML tag', () => {
+      for (const htmlTag of ['div', 'h1', 'hr', 'p']) {
+        expect(nodeOps.createElement(htmlTag, undefined, undefined, {})).equals(null)
+      }
+    })
+
+    it('it sets a non-zero position on a camera if no position is provided', () => {
+      const camera = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, {})
+      const position: Vector3 = camera.position
+      assert(['x', 'y', 'z'].some(coord => position[coord] !== 0))
+    })
+
+    it('it calls `camera.lookAt(0, 0, 0)` on a camera if no "look-at" prop is provided', () => {
+      for (const position of [[1, 2, 3], [1, 0, 0], [3, 4, 5], [-1, 2, -10]]) {
+        const cameraA = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, { position })
+        const cameraB = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, { position, lookAt: [0, 0, 0] })
+        assert(cameraA.rotation.equals(cameraB.rotation))
+      }
+    })
+
+    it('throws an error if tag does not exist in catalogue', () => {
+      expect(() => { nodeOps.createElement('THIS_TAG_DOES_NOT_EXIST', undefined, undefined, {}) }).toThrow()
+    })
+
     it('adds material with "attach" property if instance is a material', () => {
     // Setup
       const tag = 'TresMeshStandardMaterial'
@@ -127,25 +182,66 @@ describe('nodeOps', () => {
       // Assert
       expect(parent.children.includes(child)).toBeTruthy()
     })
+
+    it('does not insert a falsy child', () => {
+      const parent = nodeOps.createElement('Object3D', undefined, undefined, {})
+      for (const falsyChild of [undefined, null]) {
+        nodeOps.insert(falsyChild, parent)
+        expect(parent.children.length).toBe(0)
+        expect(() => nodeOps.insert(falsyChild, parent)).not.toThrow()
+      }
+    })
+
+    it('inserts Fog as a property', () => {
+      const parent = nodeOps.createElement('Object3D', undefined, undefined, {})
+      const fog = nodeOps.createElement('Fog', undefined, undefined, {})
+      nodeOps.insert(fog, parent)
+      expect(parent.fog).toBe(fog)
+    })
+
+    it('if "attach" prop is provided, sets `parent[attach]`', () => {
+      const parent = nodeOps.createElement('Object3D', undefined, undefined, {})
+      for (const attach of ['material', 'foo', 'bar', 'baz']) {
+        const child = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, {})
+        child.attach = attach
+        nodeOps.insert(child, parent)
+        expect(parent[attach]).toBe(child)
+        expect(parent.children.length).toBe(0)
+      }
+    })
   })
 
   describe('remove', () => {
-    it.skip('removes child from parent', async () => {
-    // Setup
-      const parent = new Scene() as unknown as TresObject
-      const child = new Mesh() as unknown as TresObject
+    it('removes child from parent', async () => {
+      const parent = mockTresObjectRootInObject(new Scene() as unknown as TresObject)
+      const child = mockTresObjectRootInObject(new Mesh() as unknown as TresObject)
+      nodeOps.insert(child, parent)
+      nodeOps.remove(child)
+      expect(!parent.children.includes(child)).toBeTruthy()
+    })
 
-      // Fake vnodes
-      child.__vnode = {
-        type: 'TresMesh',
+    it('silently does not remove a falsy child', () => {
+      for (const child of [undefined, null]) {
+        expect(() => nodeOps.remove(child)).not.toThrow()
       }
-      nodeOps.insert(child, parent)
+    })
 
-      // Test
-      nodeOps.remove(child)
+    it('calls dispose on materials', () => {
+      const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
+      const material = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, {})
+      const spy = vi.spyOn(material, 'dispose')
+      nodeOps.insert(material, parent)
+      nodeOps.remove(parent)
+      expect(spy).toHaveBeenCalledOnce()
+    })
 
-      // Assert
-      expect(!parent.children.includes(child)).toBeTruthy()
+    it('calls dispose on geometries', () => {
+      const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
+      const geometry = nodeOps.createElement('SphereGeometry', undefined, undefined, {})
+      const spy = vi.spyOn(geometry, 'dispose')
+      nodeOps.insert(geometry, parent)
+      nodeOps.remove(parent)
+      expect(spy).toHaveBeenCalledOnce()
     })
   })
 
@@ -204,6 +300,104 @@ describe('nodeOps', () => {
       expect(node.defines[allCapsKey]).equals(allCapsValue)
       expect(node.defines[allCapsUnderscoresKey]).equals(allCapsUnderscoresValue)
     })
+
+    it('calls object methods', () => {
+      const camera = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, {})
+      const spy = vi.spyOn(camera, 'lookAt')
+      nodeOps.patchProp(camera, 'look-at', undefined, new THREE.Vector3(0, 0, 0))
+      nodeOps.patchProp(camera, 'look-at', undefined, new THREE.Vector3(1, 0, 0))
+      nodeOps.patchProp(camera, 'look-at', undefined, new THREE.Vector3(1, 2, 0))
+      expect(spy).toHaveBeenCalledTimes(3)
+    })
+
+    it('calls `copy` if property and passed value are of same type', () => {
+      const camera = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, {})
+      const spy = vi.spyOn(camera.position, 'copy')
+      nodeOps.patchProp(camera, 'position', undefined, new THREE.Vector3(1))
+      nodeOps.patchProp(camera, 'position', undefined, new THREE.Vector3(2))
+      nodeOps.patchProp(camera, 'position', undefined, new THREE.Vector3(3))
+      expect(spy).toHaveBeenCalledTimes(3)
+    })
+
+    it('calls `setScalar` method', () => {
+      const camera = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, {})
+      const spy = vi.spyOn(camera.position, 'setScalar')
+      nodeOps.patchProp(camera, 'position', undefined, 1)
+      nodeOps.patchProp(camera, 'position', undefined, 2)
+      nodeOps.patchProp(camera, 'position', undefined, 3)
+      expect(spy).toHaveBeenCalledTimes(3)
+    })
+
+    describe('patch `:object` on primitives', () => {
+      it('replaces original object', () => {
+        const material0 = new THREE.MeshNormalMaterial()
+        const material1 = new THREE.MeshNormalMaterial()
+        const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: material0 })
+        nodeOps.patchProp(primitive, 'object', material0, material1)
+        expect(primitive.object).toBe(material1)
+      })
+      it('does not copy UUID', () => {
+        const material0 = new THREE.MeshNormalMaterial()
+        const material1 = new THREE.MeshNormalMaterial()
+        const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: material0 })
+        nodeOps.patchProp(primitive, 'object', material0, material1)
+        expect(material0.uuid).not.toBe(material1.uuid)
+      })
+    })
+
+    describe('patch `:args`', () => {
+      it('updates values appropriately', () => {
+        const args0 = [{ color: new THREE.Color('red') }]
+        const args1 = [{ color: new THREE.Color('blue') }]
+        const material = nodeOps.createElement('MeshBasicMaterial', undefined, undefined, { args: args0 })
+        expect(material.color.getHexString()).toBe('ff0000')
+        nodeOps.patchProp(material, 'args', args0, args1)
+        expect(material.color.getHexString()).toBe('0000ff')
+      })
+      it('creates a new instance', () => {
+        const args0 = [1, 1]
+        const args1 = [2, 3]
+        const geometry = nodeOps.createElement('TresBoxGeometry', undefined, undefined, { args: args0 })
+        const uuid = geometry.uuid
+        nodeOps.patchProp(geometry, 'args', args0, args1)
+        expect(geometry.uuid).not.toBe(uuid)
+      })
+    })
+
+    describe('if property has a `set` method', () => {
+      it('calls `set`', () => {
+        const object3d = nodeOps.createElement('Object3D', undefined, undefined, {})
+        const spy = vi.spyOn(object3d.layers, 'set')
+        const COUNT = 4
+        for (let i = 0; i < COUNT; i++) {
+          const v = Math.floor(Math.random() * 32)
+          nodeOps.patchProp(object3d, 'layers', undefined, v)
+        }
+        expect(spy).toBeCalledTimes(COUNT)
+      })
+
+      it('calls `set` with value if !Array.isArray(value)', () => {
+        const s = v => JSON.stringify(v)
+        const object3d = nodeOps.createElement('Object3D', undefined, undefined, {})
+        let result = -1
+        object3d.layers.set = v => result = v
+        for (let i = 0; i < 3; i++) {
+          const v = Math.floor(Math.random() * 32)
+          nodeOps.patchProp(object3d, 'layers', undefined, v)
+          expect(s(result)).toBe(s(v))
+        }
+      })
+      it('spreads value if it is an array', () => {
+        const s = v => JSON.stringify(v)
+        const camera = nodeOps.createElement('TresPerspectiveCamera', undefined, undefined, {})
+        const result = []
+        camera.position.set = (x, y, z) => result.push({ x, y, z })
+        nodeOps.patchProp(camera, 'position', undefined, [0, 0, 0])
+        nodeOps.patchProp(camera, 'position', undefined, [1, 2, 3])
+        nodeOps.patchProp(camera, 'position', undefined, [4, 5, 6])
+        expect(s(result)).toBe(s([{ x: 0, y: 0, z: 0 }, { x: 1, y: 2, z: 3 }, { x: 4, y: 5, z: 6 }]))
+      })
+    })
   })
 
   describe('parentNode', () => {
@@ -222,3 +416,24 @@ describe('nodeOps', () => {
     })
   })
 })
+
+// NOTE:
+// This is tightly bound to implementation and likely to change.
+//
+// src/core/nodeOps.ts will throw if some implementation details are not
+// present, making tests unpassable.
+//
+// TODO:
+// * Refactor src/core/nodeOps.ts, so that this function can be removed.
+// * Remove this function.
+//
+function mockTresObjectRootInObject(obj) {
+  if (!('__tres' in obj)) {
+    obj.__tres = {}
+  }
+  obj.__tres.root = {
+    deregisterObjectAtPointerEventHandler: () => {},
+    deregisterBlockingObjectAtPointerEventHandler: () => {},
+  }
+  return obj
+}