Skip to content

Commit

Permalink
test: add insert/remove tests (#738)
Browse files Browse the repository at this point in the history
  • Loading branch information
andretchen0 authored Jun 21, 2024
1 parent 15e3f07 commit e1df8a9
Showing 1 changed file with 312 additions and 1 deletion.
313 changes: 312 additions & 1 deletion src/core/nodeOps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,50 @@ describe('nodeOps', () => {
expect(parent.children.includes(child)).toBeTruthy()
})

describe('primitive :object', () => {
describe('into mesh', () => {
it.skip('inserts a mesh :object', () => {
const parent = nodeOps.createElement('Mesh', undefined, undefined, {})
const object = new THREE.Mesh()
const primitive = nodeOps.createElement('primitive', undefined, undefined, { object })

expect(parent.material.uuid).not.toBe(object.uuid)
nodeOps.insert(primitive, parent)
expect(parent.material.uuid).toBe(object.uuid)
})

it.skip('inserts a material :object', () => {
const parent = nodeOps.createElement('Mesh', undefined, undefined, {})
const object = new THREE.MeshNormalMaterial()
const primitive = nodeOps.createElement('primitive', undefined, undefined, { object })

expect(parent.material.uuid).not.toBe(object.uuid)
nodeOps.insert(primitive, parent)
expect(parent.material.uuid).toBe(object.uuid)
})

it.skip('inserts a geometry :object', () => {
const parent = nodeOps.createElement('Mesh', undefined, undefined, {})
const object = new THREE.BoxGeometry()
const primitive = nodeOps.createElement('primitive', undefined, undefined, { object })

expect(parent.material.uuid).not.toBe(object.uuid)
nodeOps.insert(primitive, parent)
expect(parent.material.uuid).toBe(object.uuid)
})

it.skip('inserts a group :object', () => {
const parent = nodeOps.createElement('Mesh', undefined, undefined, {})
const object = new THREE.Group()
const primitive = nodeOps.createElement('primitive', undefined, undefined, { object })

expect(parent.material.uuid).not.toBe(object.uuid)
nodeOps.insert(primitive, parent)
expect(parent.material.uuid).toBe(object.uuid)
})
})
})

it('does not insert a falsy child', () => {
const parent = nodeOps.createElement('Object3D', undefined, undefined, {})
for (const falsyChild of [undefined, null]) {
Expand Down Expand Up @@ -228,7 +272,7 @@ describe('nodeOps', () => {
}
})

it('calls dispose on materials', () => {
it('calls dispose on a material', () => {
const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
const material = nodeOps.createElement('MeshNormalMaterial', undefined, undefined, {})
const spy = vi.spyOn(material, 'dispose')
Expand All @@ -237,6 +281,18 @@ describe('nodeOps', () => {
expect(spy).toHaveBeenCalledOnce()
})

it('calls dispose on a material array', () => {
const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
const material0 = new THREE.MeshNormalMaterial()
const material1 = new THREE.MeshNormalMaterial()
const spy0 = vi.spyOn(material0, 'dispose')
const spy1 = vi.spyOn(material1, 'dispose')
parent.material = [material0, material1]
nodeOps.remove(parent)
expect(spy0).toHaveBeenCalledOnce()
expect(spy1).toHaveBeenCalledOnce()
})

it('calls dispose on geometries', () => {
const parent = mockTresObjectRootInObject(nodeOps.createElement('Mesh', undefined, undefined, {}))
const geometry = nodeOps.createElement('SphereGeometry', undefined, undefined, {})
Expand All @@ -245,6 +301,220 @@ describe('nodeOps', () => {
nodeOps.remove(parent)
expect(spy).toHaveBeenCalledOnce()
})

it('calls dispose on every material/geometry in a TresMesh tree', () => {
const NUM_LEVEL = 5
const NUM_CHILD_PER_NODE = 3
const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Mesh'))
const disposalSpies = []

createTreeIn(rootNode, (parent, childI, levelI) => {
if (levelI > NUM_LEVEL || childI >= NUM_CHILD_PER_NODE) {
return false
}
const { mesh, material, geometry } = createElementMesh(nodeOps)
nodeOps.insert(mesh, parent)
disposalSpies.push(vi.spyOn(geometry, 'dispose'))
disposalSpies.push(vi.spyOn(material, 'dispose'))
return mesh
})

nodeOps.remove(rootNode)
for (const spy of disposalSpies) {
expect(spy).toHaveBeenCalledOnce()
}
})

it('calls dispose on every material/geometry in a TresMesh/TresGroup tree', () => {
const NUM_LEVEL = 5
const NUM_CHILD_PER_NODE = 3
const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Group'))
const disposalSpies = []

createTreeIn(rootNode, (parent, childI, levelI) => {
if (childI > NUM_CHILD_PER_NODE || levelI > NUM_LEVEL) {
return false
}
if (Math.random() > 0.3) {
const { mesh, material, geometry } = createElementMesh(nodeOps)
nodeOps.insert(mesh, parent)
disposalSpies.push(vi.spyOn(geometry, 'dispose'))
disposalSpies.push(vi.spyOn(material, 'dispose'))
return mesh
}
else {
const group = nodeOps.createElement('Group')
nodeOps.insert(group, parent)
return group
}
})

nodeOps.remove(rootNode)
for (const spy of disposalSpies) {
expect(spy).toHaveBeenCalledOnce()
}
})

it('does not dispose primitive material/geometries on remove(primitive)', () => {
const { primitive, material, geometry } = createElementPrimitiveMesh(nodeOps)
const spy0 = vi.spyOn(material, 'dispose')
const spy1 = vi.spyOn(geometry, 'dispose')

const group = nodeOps.createElement('Group')
nodeOps.insert(primitive, group)
nodeOps.remove(primitive)

expect(spy0).not.toBeCalled()
expect(spy1).not.toBeCalled()
})

it.skip('does not dispose primitive material/geometries on remove(ascestorOfPrimitive)', () => {
const { primitive, material, geometry } = createElementPrimitiveMesh(nodeOps)
const spy0 = vi.spyOn(material, 'dispose')
const spy1 = vi.spyOn(geometry, 'dispose')

const group = nodeOps.createElement('Group')
nodeOps.insert(primitive, group)
nodeOps.remove(group)

expect(spy0).not.toBeCalled()
expect(spy1).not.toBeCalled()
})

it.skip('does not call dispose on primitive materials/geometries in a tree of Mesh/Groups/Primitives created by nodeOps', () => {
const NUM_LEVEL = 5
const NUM_CHILD_PER_NODE = 3
const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Group'))
const disposalSpies = []

createTreeIn(rootNode, (parent, childI, levelI) => {
if (childI > NUM_CHILD_PER_NODE || levelI > NUM_LEVEL) {
return false
}
if (Math.random() > 0.5) {
const { mesh } = createElementMesh(nodeOps)
nodeOps.insert(mesh, parent)
return mesh
}
else if (Math.random() > 0.5) {
const group = nodeOps.createElement('Group')
nodeOps.insert(group, parent)
return group
}
else {
const { primitive, material, geometry } = createElementPrimitiveMesh(nodeOps)
disposalSpies.push(vi.spyOn(geometry, 'dispose'))
disposalSpies.push(vi.spyOn(material, 'dispose'))
nodeOps.insert(primitive, parent)
return primitive
}
})

nodeOps.remove(rootNode)
for (const spy of disposalSpies) {
expect(spy).not.toHaveBeenCalled()
}
})

describe(':dispose="null"', () => {
it.skip('does not call dispose on any element in a subtree where the root :dispose==="null"', () => {
const NUM_LEVEL = 5
const NUM_CHILD_PER_NODE = 3
const rootNode = mockTresObjectRootInObject(nodeOps.createElement('Group'))
const disposalSpies = []
const nullDisposeObjects = new Set()

createTreeIn(rootNode, (parent, childI, levelI) => {
if (childI > NUM_CHILD_PER_NODE || levelI > NUM_LEVEL) {
return false
}
const { mesh, material, geometry } = createElementMesh(nodeOps)
if (nullDisposeObjects.has(parent)) {
nullDisposeObjects.add(mesh)
disposalSpies.push(vi.spyOn(geometry, 'dispose'))
disposalSpies.push(vi.spyOn(material, 'dispose'))
}
else if (levelI > 2 && Math.random() > 0.8) {
nodeOps.patchProp(mesh, 'dispose', undefined, null)
nullDisposeObjects.add(mesh)
disposalSpies.push(vi.spyOn(geometry, 'dispose'))
disposalSpies.push(vi.spyOn(material, 'dispose'))
}
nodeOps.insert(mesh, parent)
return mesh
})

nodeOps.remove(rootNode)
for (const spy of disposalSpies) {
expect(spy).not.toHaveBeenCalled()
}
})
})

describe('in the THREE parent-child graph', () => {
it('detaches mesh from mesh', () => {
const { mesh: parent } = createElementMesh(nodeOps)
const { mesh: child } = createElementMesh(nodeOps)
nodeOps.insert(child, parent)
expect(child.parent.uuid).toBe(parent.uuid)

nodeOps.remove(child)
expect(child.parent?.uuid).toBeFalsy()
})
it('detaches group from mesh', () => {
const { mesh: parent } = createElementMesh(nodeOps)
const child = nodeOps.createElement('Group')
nodeOps.insert(child, parent)
expect(child.parent.uuid).toBe(parent.uuid)

nodeOps.remove(child)
expect(child.parent?.uuid).toBeFalsy()
})
it('detaches mesh from group', () => {
const parent = nodeOps.createElement('Group')
const { mesh: child } = createElementMesh(nodeOps)
nodeOps.insert(child, parent)
expect(child.parent.uuid).toBe(parent.uuid)

nodeOps.remove(child)
expect(child.parent?.uuid).toBeFalsy()
})
it.skip('detaches mesh (in primitive :object) from mesh', () => {
const { mesh: parent } = createElementMesh(nodeOps)
const { primitive, mesh } = createElementPrimitiveMesh(nodeOps)
nodeOps.insert(primitive, parent)
expect(primitive.parent?.uuid).toBe(mesh.uuid)

nodeOps.remove(primitive)
expect(mesh.parent?.uuid).toBeFalsy()
})
it.skip('detaches mesh (in primitive :object) when mesh ancestor is removed', () => {
const { mesh: grandparent } = createElementMesh(nodeOps)
const { mesh: parent } = createElementMesh(nodeOps)
const { primitive, mesh: primitiveMesh } = createElementPrimitiveMesh(nodeOps)
nodeOps.insert(parent, grandparent)
nodeOps.insert(primitive, parent)
expect(primitiveMesh.parent?.uuid).toBe(parent.uuid)

nodeOps.remove(parent)
expect(primitiveMesh.parent?.uuid).toBeFalsy()
})
it('does not detach primitive :object descendants', () => {
const { mesh: parent } = createElementMesh(nodeOps)
const { primitive, mesh: primitiveMesh } = createElementPrimitiveMesh(nodeOps)
const grandChild0 = new THREE.Mesh()
const grandChild1 = new THREE.Group()
primitiveMesh.add(grandChild0, grandChild1)

nodeOps.insert(primitive, parent)
expect(grandChild0.parent.uuid).toBe(primitiveMesh.uuid)
expect(grandChild1.parent.uuid).toBe(primitiveMesh.uuid)

nodeOps.remove(primitive)
expect(grandChild0.parent.uuid).toBe(primitiveMesh.uuid)
expect(grandChild1.parent.uuid).toBe(primitiveMesh.uuid)
})
})
})

describe('patchProp', () => {
Expand Down Expand Up @@ -468,3 +738,44 @@ function mockTresContext() {
deregisterCamera: () => {},
} as unknown as TresContext
}

function createElementMesh(nodeOps: ReturnType<typeof getNodeOps>) {
const geometry = nodeOps.createElement('BoxGeometry')
const material = nodeOps.createElement('MeshNormalMaterial')
const mesh = nodeOps.createElement('Mesh')
nodeOps.insert(geometry, mesh)
nodeOps.insert(material, mesh)
return { mesh, geometry, material }
}

function createElementPrimitiveMesh(nodeOps: ReturnType<typeof getNodeOps>) {
const geometry = new THREE.BoxGeometry()
const material = new THREE.MeshNormalMaterial()
const mesh = new THREE.Mesh(geometry, material)
const primitive = nodeOps.createElement('primitive', undefined, undefined, { object: mesh })
return { primitive, mesh, geometry, material }
}

function createTreeIn<T>(root: T, insertCallback: (parent: T, childI: number, levelI: number) => T) {
let levelII = 0
const nextLevel = [root] as T[]
while (nextLevel.length) {
const currLevel = Array.from(nextLevel)
nextLevel.length = 0

while (currLevel.length) {
const parent = currLevel.shift()
let childI = 0
while (true) {
const child = insertCallback(parent, childI++, levelII)
if (child) {
nextLevel.push(child)
}
else {
break
}
}
}
levelII++
}
}

0 comments on commit e1df8a9

Please sign in to comment.