Răsfoiți Sursa

Merge pull request #637 from Tresjs/test/nodeOps-organize

test(nodeOps): rename file, fix type errors, add tests
andretchen0 1 an în urmă
părinte
comite
1121eb1225
4 a modificat fișierele cu 486 adăugiri și 226 ștergeri
  1. 2 1
      package.json
  2. 45 0
      pnpm-lock.yaml
  3. 439 0
      src/core/nodeOps.test.ts
  4. 0 225
      src/core/nodeOpts.test.ts

+ 2 - 1
package.json

@@ -50,7 +50,7 @@
     "playground": "cd playground && npm run dev",
     "test": "vitest",
     "test:ci": "vitest run",
-    "test:ui": "vitest --ui",
+    "test:ui": "vitest --ui --coverage.enabled=true",
     "release": "release-it",
     "coverage": "vitest run --coverage",
     "lint": "eslint .",
@@ -81,6 +81,7 @@
     "@typescript-eslint/parser": "^7.7.1",
     "@vitejs/plugin-vue": "^5.0.4",
     "@vitest/coverage-c8": "^0.33.0",
+    "@vitest/coverage-v8": "^1.5.0",
     "@vitest/ui": "^1.5.0",
     "@vue/test-utils": "^2.4.5",
     "eslint": "^9.1.1",

+ 45 - 0
pnpm-lock.yaml

@@ -45,6 +45,9 @@ importers:
       '@vitest/coverage-c8':
         specifier: ^0.33.0
         version: 0.33.0(vitest@1.5.0)
+      '@vitest/coverage-v8':
+        specifier: ^1.5.0
+        version: 1.5.0(vitest@1.5.0)
       '@vitest/ui':
         specifier: ^1.5.0
         version: 1.5.0(vitest@1.5.0)
@@ -2722,6 +2725,29 @@ packages:
       vitest: 1.5.0(@vitest/ui@1.5.0)(jsdom@24.0.0)
     dev: true
 
+  /@vitest/coverage-v8@1.5.0(vitest@1.5.0):
+    resolution: {integrity: sha512-1igVwlcqw1QUMdfcMlzzY4coikSIBN944pkueGi0pawrX5I5Z+9hxdTR+w3Sg6Q3eZhvdMAs8ZaF9JuTG1uYOQ==}
+    peerDependencies:
+      vitest: 1.5.0
+    dependencies:
+      '@ampproject/remapping': 2.3.0
+      '@bcoe/v8-coverage': 0.2.3
+      debug: 4.3.4
+      istanbul-lib-coverage: 3.2.2
+      istanbul-lib-report: 3.0.1
+      istanbul-lib-source-maps: 5.0.4
+      istanbul-reports: 3.1.7
+      magic-string: 0.30.10
+      magicast: 0.3.4
+      picocolors: 1.0.0
+      std-env: 3.7.0
+      strip-literal: 2.1.0
+      test-exclude: 6.0.0
+      vitest: 1.5.0(@vitest/ui@1.5.0)(jsdom@24.0.0)
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /@vitest/expect@1.5.0:
     resolution: {integrity: sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==}
     dependencies:
@@ -6107,6 +6133,17 @@ packages:
       supports-color: 7.2.0
     dev: true
 
+  /istanbul-lib-source-maps@5.0.4:
+    resolution: {integrity: sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==}
+    engines: {node: '>=10'}
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.25
+      debug: 4.3.4
+      istanbul-lib-coverage: 3.2.2
+    transitivePeerDependencies:
+      - supports-color
+    dev: true
+
   /istanbul-reports@3.1.7:
     resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==}
     engines: {node: '>=8'}
@@ -6480,6 +6517,14 @@ packages:
       '@jridgewell/sourcemap-codec': 1.4.15
     dev: true
 
+  /magicast@0.3.4:
+    resolution: {integrity: sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==}
+    dependencies:
+      '@babel/parser': 7.24.4
+      '@babel/types': 7.24.0
+      source-map-js: 1.2.0
+    dev: true
+
   /make-dir@4.0.0:
     resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==}
     engines: {node: '>=10'}

+ 439 - 0
src/core/nodeOps.test.ts

@@ -0,0 +1,439 @@
+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(() => {
+    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
+      const tag = 'TresMesh'
+      const props = { args: [] }
+
+      // Test
+      const instance = nodeOps.createElement(tag, undefined, undefined, props)
+
+      // Assert
+      expect(instance?.isObject3D).toBeTruthy()
+      expect(instance).toBeInstanceOf(Mesh)
+    })
+
+    it('creates an instance with given tag and props', async () => {
+    // Setup
+      const tag = 'TresTorusGeometry'
+      const props = { args: [10, 3, 16, 100] }
+
+      // Test
+      const instance = nodeOps.createElement(tag, undefined, undefined, props)
+
+      // Assert
+      expect(instance?.parameters.radius).toBe(10)
+      expect(instance?.parameters.tube).toBe(3)
+      expect(instance?.parameters.radialSegments).toBe(16)
+      expect(instance?.parameters.tubularSegments).toBe(100)
+    })
+
+    it.skip('creates an camera instance', async () => {
+    // Setup
+      const tag = 'TresPerspectiveCamera'
+      const props = { args: [75, 2, 0.1, 5] }
+
+      // Test
+      const instance = nodeOps.createElement(tag, undefined, undefined, props)
+
+      // Assert
+      expect(instance?.isCamera).toBeTruthy()
+      expect(instance).toBeInstanceOf(THREE.PerspectiveCamera)
+    })
+
+    it.skip('logs a warning if the camera doesnt have a position', async () => {
+    // Setup
+      const tag = 'TresPerspectiveCamera'
+      const props = { args: [75, 2, 0.1, 5] }
+
+      // Spy
+      const consoleWarnSpy = vi.spyOn(console, 'warn')
+      consoleWarnSpy.mockImplementation(() => { })
+
+      // Test
+      const instance = nodeOps.createElement(tag, undefined, undefined, props)
+
+      // Assert
+      expect(instance?.isCamera).toBeTruthy()
+      expect(instance).toBeInstanceOf(THREE.PerspectiveCamera)
+      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'
+      const props = { args: [] }
+
+      // Test
+      const instance = nodeOps.createElement(tag, undefined, undefined, props)
+
+      // Assert
+      expect(instance?.isMaterial).toBeTruthy()
+      expect(instance?.attach).toBe('material')
+    })
+
+    it('adds attach geometry property if instance is a geometry', () => {
+    // Setup
+      const tag = 'TresTorusGeometry'
+      const props = { args: [] }
+
+      // Test
+      const instance = nodeOps.createElement(tag, undefined, undefined, props)
+
+      // Assert
+      expect(instance?.isBufferGeometry).toBeTruthy()
+      expect(instance?.attach).toBe('geometry')
+    })
+  })
+
+  describe('insert', () => {
+    it('inserts child into parent', async () => {
+    // Setup
+      const parent = new Scene()
+      parent.__tres = {
+        root: {
+          registerCamera: () => { },
+          registerObjectAtPointerEventHandler: () => { },
+        },
+      }
+      const child = new Mesh()
+
+      child.__tres = {
+        root: null,
+      }
+
+      // Fake vnodes
+      child.__vnode = {
+        type: 'TresMesh',
+      }
+
+      // Test
+      nodeOps.insert(child, parent, null)
+
+      // 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('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()
+    })
+
+    it('silently does not remove a falsy child', () => {
+      for (const child of [undefined, null]) {
+        expect(() => nodeOps.remove(child)).not.toThrow()
+      }
+    })
+
+    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()
+    })
+
+    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()
+    })
+  })
+
+  describe('patchProp', () => {
+    it('patches property of node', async () => {
+    // Setup
+      const node = nodeOps.createElement('Mesh')!
+      const prop = 'visible'
+      const nextValue = false
+
+      // Test
+      nodeOps.patchProp(node, prop, null, nextValue)
+
+      // Assert
+      expect(node.visible === nextValue)
+    })
+
+    it('patches/traverses pierced props', async () => {
+    // Setup
+      const node = nodeOps.createElement('Mesh')!
+      const prop = 'position-x'
+      const nextValue = 5
+
+      // Test
+      nodeOps.patchProp(node, prop, null, nextValue)
+
+      // Assert
+      expect(node.position.x === nextValue)
+    })
+
+    it('does not patch/traverse pierced props of existing dashed properties', async () => {
+    // Setup
+      const node = nodeOps.createElement('Mesh')!
+      const prop = 'cast-shadow'
+      const nextValue = true
+
+      // Test
+      nodeOps.patchProp(node, prop, null, nextValue)
+
+      // Assert
+      expect(node.castShadow === nextValue)
+    })
+
+    it('preserves ALL_CAPS_CASE in pierced props', () => {
+    // Issue: https://github.com/Tresjs/tres/issues/605
+      const { createElement, patchProp } = nodeOps
+      const node = createElement('TresMeshStandardMaterial', undefined, undefined, {})!
+      const allCapsKey = 'STANDARD'
+      const allCapsUnderscoresKey = 'USE_UVS'
+      const allCapsValue = 'hello'
+      const allCapsUnderscoresValue = 'goodbye'
+
+      patchProp(node, `defines-${allCapsKey}`, null, allCapsValue)
+      patchProp(node, `defines-${allCapsUnderscoresKey}`, null, allCapsUnderscoresValue)
+
+      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', () => {
+    it('returns parent of a node', async () => {
+    // Setup
+      const parent: TresObject = new Scene()
+      const child: TresObject = nodeOps.createElement('Mesh')!
+      parent.children.push(child)
+      child.parent = parent
+
+      // Test
+      const parentNode = nodeOps.parentNode(child)
+
+      // Assert
+      expect(parentNode === parent)
+    })
+  })
+})
+
+// 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
+}

+ 0 - 225
src/core/nodeOpts.test.ts

@@ -1,225 +0,0 @@
-import * as THREE from 'three'
-import { Mesh, Scene } from 'three'
-import type { TresObject } from '../types'
-import { nodeOps } from './nodeOps'
-import { extend } from './catalogue'
-
-describe('nodeOps', () => {
-  beforeAll(() => {
-    // Setup
-    extend(THREE)
-  })
-  it('createElement should create an instance with given tag', async () => {
-    // Setup
-    const tag = 'TresMesh'
-    const props = { args: [] }
-
-    // Test
-    const instance = nodeOps().createElement(tag, false, null, props)
-
-    // Assert
-    expect(instance.isObject3D).toBeTruthy()
-    expect(instance).toBeInstanceOf(Mesh)
-  })
-
-  it('createElement should create an instance with given tag and props', async () => {
-    // Setup
-    const tag = 'TresTorusGeometry'
-    const props = { args: [10, 3, 16, 100] }
-
-    // Test
-    const instance = nodeOps().createElement(tag, false, null, props)
-
-    // Assert
-    expect(instance.parameters.radius).toBe(10)
-    expect(instance.parameters.tube).toBe(3)
-    expect(instance.parameters.radialSegments).toBe(16)
-    expect(instance.parameters.tubularSegments).toBe(100)
-  })
-
-  it.skip('createElement should create an camera instance', async () => {
-    // Setup
-    const tag = 'TresPerspectiveCamera'
-    const props = { args: [75, 2, 0.1, 5] }
-
-    // Test
-    const instance = nodeOps().createElement(tag, false, null, props)
-
-    // Assert
-    expect(instance.isCamera).toBeTruthy()
-    expect(instance).toBeInstanceOf(THREE.PerspectiveCamera)
-  })
-
-  it.skip('createElement should log a warning if the camera doesnt have a position', async () => {
-    // Setup
-    const tag = 'TresPerspectiveCamera'
-    const props = { args: [75, 2, 0.1, 5] }
-
-    // Spy
-    const consoleWarnSpy = vi.spyOn(console, 'warn')
-    consoleWarnSpy.mockImplementation(() => { })
-
-    // Test
-    const instance = nodeOps().createElement(tag, false, null, props)
-
-    // Assert
-    expect(instance.isCamera).toBeTruthy()
-    expect(instance).toBeInstanceOf(THREE.PerspectiveCamera)
-    expect(consoleWarnSpy).toHaveBeenCalled()
-  })
-
-  it('createElement should add attach material property if instance is a material', () => {
-    // Setup
-    const tag = 'TresMeshStandardMaterial'
-    const props = { args: [] }
-
-    // Test
-    const instance = nodeOps().createElement(tag, false, null, props)
-
-    // Assert
-    expect(instance.isMaterial).toBeTruthy()
-    expect(instance.attach).toBe('material')
-  })
-
-  it('createElement should add attach geometry property if instance is a geometry', () => {
-    // Setup
-    const tag = 'TresTorusGeometry'
-    const props = { args: [] }
-
-    // Test
-    const instance = nodeOps().createElement(tag, false, null, props)
-
-    // Assert
-    expect(instance.isBufferGeometry).toBeTruthy()
-    expect(instance.attach).toBe('geometry')
-  })
-
-  it('insert should insert child into parent', async () => {
-    // Setup
-    const parent = new Scene()
-    parent.__tres = {
-      root: {
-        registerCamera: () => { },
-        registerObjectAtPointerEventHandler: () => { },
-      },
-    }
-    const child = new Mesh()
-
-    child.__tres = {
-      root: null,
-    }
-
-    // Fake vnodes
-    child.__vnode = {
-      type: 'TresMesh',
-    }
-
-    // Test
-    nodeOps().insert(child, parent, null)
-
-    // Assert
-    expect(parent.children.includes(child)).toBeTruthy()
-  })
-
-  it.skip('remove: removes child from parent', async () => {
-    // Setup
-    const parent = new Scene() as unknown as TresObject
-    const child = new Mesh() as unknown as TresObject
-
-    // Fake vnodes
-    child.__vnode = {
-      type: 'TresMesh',
-    }
-    nodeOps().insert(child, parent)
-
-    // Test
-    nodeOps().remove(child)
-
-    // Assert
-    expect(!parent.children.includes(child)).toBeTruthy()
-  })
-
-  it('patchProp should patch property of node', async () => {
-    // Setup
-    const node: TresObject = new Mesh()
-    node.__tres = {
-      root: {
-        invalidate: () => { },
-      },
-    }
-    const prop = 'visible'
-    const nextValue = false
-
-    // Test
-    nodeOps().patchProp(node, prop, null, nextValue)
-
-    // Assert
-    expect(node.visible === nextValue)
-  })
-
-  it('patchProp should patch traverse pierced props', async () => {
-    // Setup
-    const node: TresObject = new Mesh()
-    node.__tres = {
-      root: {
-        invalidate: () => { },
-      },
-    }
-    const prop = 'position-x'
-    const nextValue = 5
-
-    // Test
-    nodeOps().patchProp(node, prop, null, nextValue)
-
-    // Assert
-    expect(node.position.x === nextValue)
-  })
-
-  it('patchProp it should not patch traverse pierced props of existing dashed properties', async () => {
-    // Setup
-    const node: TresObject = new Mesh()
-    node.__tres = {
-      root: {
-        invalidate: () => { },
-      },
-    }
-    const prop = 'cast-shadow'
-    const nextValue = true
-
-    // Test
-    nodeOps().patchProp(node, prop, null, nextValue)
-
-    // Assert
-    expect(node.castShadow === nextValue)
-  })
-
-  it('patchProp should preserve ALL_CAPS_CASE in pierced props', () => {
-    // Issue: https://github.com/Tresjs/tres/issues/605
-    const { createElement, patchProp } = nodeOps()
-    const node = createElement('TresMeshStandardMaterial', null, null, {})
-    const allCapsKey = 'STANDARD'
-    const allCapsUnderscoresKey = 'USE_UVS'
-    const allCapsValue = 'hello'
-    const allCapsUnderscoresValue = 'goodbye'
-
-    patchProp(node, `defines-${allCapsKey}`, null, allCapsValue)
-    patchProp(node, `defines-${allCapsUnderscoresKey}`, null, allCapsUnderscoresValue)
-
-    expect(node.defines[allCapsKey]).equals(allCapsValue)
-    expect(node.defines[allCapsUnderscoresKey]).equals(allCapsUnderscoresValue)
-  })
-
-  it('parentNode: returns parent of a node', async () => {
-    // Setup
-    const parent: TresObject = new Scene()
-    const child: TresObject = new Mesh()
-    parent.children.push(child)
-    child.parent = parent
-
-    // Test
-    const parentNode = nodeOps().parentNode(child)
-
-    // Assert
-    expect(parentNode === parent)
-  })
-})