- Move top-level engine and e2e prefixed test files under domain folders - Update fixture path helpers after moving render and pen tests - Add lint coverage to prevent new top-level prefixed test files - Refresh testing docs for the new fig and layout paths
219 lines
5.3 KiB
TypeScript
219 lines
5.3 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
|
|
import { SceneGraph } from '@open-pencil/core'
|
|
|
|
function pageId(graph: SceneGraph) {
|
|
return graph.getPages()[0].id
|
|
}
|
|
|
|
describe('hitTest — hidden nodes', () => {
|
|
test('visible node at point is found', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const rect = graph.createNode('RECTANGLE', page, {
|
|
name: 'R',
|
|
x: 100,
|
|
y: 100,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
|
|
const hit = graph.hitTest(125, 125)
|
|
expect(hit).not.toBeNull()
|
|
expect(hit?.id).toBe(rect.id)
|
|
})
|
|
|
|
test('hidden node (visible: false) is skipped', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
graph.createNode('RECTANGLE', page, {
|
|
name: 'R',
|
|
x: 100,
|
|
y: 100,
|
|
width: 50,
|
|
height: 50,
|
|
visible: false
|
|
})
|
|
|
|
const hit = graph.hitTest(125, 125)
|
|
expect(hit).toBeNull()
|
|
})
|
|
|
|
test('hidden child inside visible frame is skipped', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const frame = graph.createNode('FRAME', page, {
|
|
name: 'F',
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 200
|
|
})
|
|
graph.createNode('RECTANGLE', frame.id, {
|
|
name: 'R',
|
|
x: 50,
|
|
y: 50,
|
|
width: 50,
|
|
height: 50,
|
|
visible: false
|
|
})
|
|
|
|
const hit = graph.hitTest(75, 75)
|
|
// Frame itself has no visible fill/stroke, so it's not hit either
|
|
expect(hit).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('hitTest — locked nodes', () => {
|
|
test('locked container returns itself, does not recurse deeper', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
// A locked GROUP (transparent container) with a child at the same point
|
|
const group = graph.createNode('GROUP', page, {
|
|
name: 'G',
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 200,
|
|
locked: true
|
|
})
|
|
graph.createNode('RECTANGLE', group.id, {
|
|
name: 'R',
|
|
x: 50,
|
|
y: 50,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
|
|
// The point (75, 75) is inside the child rectangle inside the locked group.
|
|
// Since the group is locked, hitTest should return the GROUP (not the child).
|
|
const hit = graph.hitTest(75, 75)
|
|
expect(hit).not.toBeNull()
|
|
expect(hit?.id).toBe(group.id)
|
|
})
|
|
|
|
test('unlocked container recurses to child', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const frame = graph.createNode('FRAME', page, {
|
|
name: 'F',
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 200
|
|
})
|
|
const rect = graph.createNode('RECTANGLE', frame.id, {
|
|
name: 'R',
|
|
x: 50,
|
|
y: 50,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
|
|
const hit = graph.hitTest(75, 75)
|
|
expect(hit).not.toBeNull()
|
|
expect(hit?.id).toBe(rect.id)
|
|
})
|
|
})
|
|
|
|
describe('hitTest — opacity', () => {
|
|
test('zero-opacity node is still hit-testable', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const rect = graph.createNode('RECTANGLE', page, {
|
|
name: 'R',
|
|
x: 100,
|
|
y: 100,
|
|
width: 50,
|
|
height: 50,
|
|
opacity: 0
|
|
})
|
|
|
|
// Opacity is visual, not structural. Zero-opacity nodes should still be hit.
|
|
const hit = graph.hitTest(125, 125)
|
|
expect(hit).not.toBeNull()
|
|
expect(hit?.id).toBe(rect.id)
|
|
})
|
|
})
|
|
|
|
describe('hitTest — point outside all nodes', () => {
|
|
test('returns null for empty area', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
|
|
const hit = graph.hitTest(500, 500)
|
|
expect(hit).toBeNull()
|
|
})
|
|
|
|
test('returns null for point outside node bounds', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
graph.createNode('RECTANGLE', page, {
|
|
name: 'R',
|
|
x: 100,
|
|
y: 100,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
|
|
const hit = graph.hitTest(200, 200)
|
|
expect(hit).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('hitTest — deep mode', () => {
|
|
test('hitTestDeep returns deep container for opaque containers', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const component = graph.createNode('COMPONENT', page, {
|
|
name: 'C',
|
|
x: 0,
|
|
y: 0,
|
|
width: 200,
|
|
height: 200
|
|
})
|
|
graph.createNode('RECTANGLE', component.id, {
|
|
name: 'R',
|
|
x: 50,
|
|
y: 50,
|
|
width: 50,
|
|
height: 50
|
|
})
|
|
|
|
// Normal hitTest should return the component (opaque)
|
|
const normalHit = graph.hitTest(75, 75)
|
|
expect(normalHit?.id).toBe(component.id)
|
|
|
|
// Deep hitTest should return the rectangle inside the component
|
|
const deepHit = graph.hitTestDeep(75, 75)
|
|
expect(deepHit).not.toBeNull()
|
|
expect(deepHit?.type).toBe('RECTANGLE')
|
|
})
|
|
})
|
|
|
|
describe('hitTest — z-order (last child wins)', () => {
|
|
test('overlapping children: last in childIds array is hit', () => {
|
|
const graph = new SceneGraph()
|
|
const page = pageId(graph)
|
|
const rect1 = graph.createNode('RECTANGLE', page, {
|
|
name: 'R1',
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100
|
|
})
|
|
const rect2 = graph.createNode('RECTANGLE', page, {
|
|
name: 'R2',
|
|
x: 0,
|
|
y: 0,
|
|
width: 100,
|
|
height: 100
|
|
})
|
|
|
|
// Both overlap at (50, 50). Last child (rect2) should be hit.
|
|
const hit = graph.hitTest(50, 50)
|
|
expect(hit?.id).toBe(rect2.id)
|
|
expect(hit?.id).not.toBe(rect1.id)
|
|
})
|
|
})
|