Spaces:
Sleeping
Sleeping
| import { describe, it, expect, beforeEach } from 'vitest'; | |
| import * as THREE from 'three'; | |
| import { ThreadLabels } from '../src/ui/ThreadLabels.js'; | |
| // Lightweight mock for document & DOM elements in Node environment | |
| function createMockElement(tagName = 'div') { | |
| const children = []; | |
| const classList = new Set(); | |
| let _className = ''; | |
| const element = { | |
| tagName: tagName.toUpperCase(), | |
| children, | |
| classList: { | |
| add: (cls) => { classList.add(cls); _className = Array.from(classList).join(' '); }, | |
| remove: (cls) => { classList.delete(cls); _className = Array.from(classList).join(' '); }, | |
| contains: (cls) => classList.has(cls), | |
| toggle: (cls, force) => { | |
| const shouldHave = force === undefined ? !classList.has(cls) : !!force; | |
| if (shouldHave) classList.add(cls); else classList.delete(cls); | |
| _className = Array.from(classList).join(' '); | |
| return shouldHave; | |
| }, | |
| }, | |
| style: {}, | |
| appendChild: (child) => children.push(child), | |
| setAttribute: () => {}, | |
| removeAttribute: () => {}, | |
| querySelector: (selector) => { | |
| const cleanSel = selector.replace('#', '').replace('.', ''); | |
| for (const child of children) { | |
| if (child.id === cleanSel || child.classList.contains(cleanSel)) return child; | |
| const found = child.querySelector ? child.querySelector(selector) : null; | |
| if (found) return found; | |
| } | |
| return null; | |
| }, | |
| querySelectorAll: (selector) => { | |
| const cleanSel = selector.replace('#', '').replace('.', ''); | |
| let results = []; | |
| for (const child of children) { | |
| if (child.classList.contains(cleanSel)) results.push(child); | |
| if (child.querySelectorAll) results = results.concat(child.querySelectorAll(selector)); | |
| } | |
| return results; | |
| }, | |
| set innerHTML(val) { | |
| if (val === '') children.length = 0; | |
| }, | |
| set className(val) { | |
| _className = val; | |
| val.split(' ').forEach(c => { if (c) classList.add(c); }); | |
| }, | |
| get className() { | |
| return _className; | |
| }, | |
| id: '', | |
| textContent: '' | |
| }; | |
| return element; | |
| } | |
| if (typeof global.document === 'undefined') { | |
| global.document = { | |
| createElement: (tagName) => createMockElement(tagName), | |
| body: createMockElement('body') | |
| }; | |
| } | |
| describe('ThreadLabels Component Tests', () => { | |
| let container; | |
| let threadLabels; | |
| beforeEach(() => { | |
| container = document.createElement('div'); | |
| threadLabels = new ThreadLabels(container); | |
| }); | |
| it('initializes the labels overlay container', () => { | |
| const overlay = container.querySelector('#thread-labels-container'); | |
| expect(overlay).not.toBeNull(); | |
| }); | |
| it('registers label cards for each thread', () => { | |
| const mockItems = [ | |
| { id: 'vec_a', text: 'KING', type: 'word_a', origin3D: new THREE.Vector3(0, 50, 0) }, | |
| { id: 'res', text: 'RESULT', type: 'res', origin3D: new THREE.Vector3(0, -50, 0) } | |
| ]; | |
| threadLabels.setLabels(mockItems); | |
| expect(threadLabels.labels.length).toBe(2); | |
| const cards = container.querySelectorAll('.thread-label-card'); | |
| expect(cards.length).toBe(2); | |
| expect(cards[1].classList.contains('res-label')).toBe(true); | |
| }); | |
| it('clears registered labels', () => { | |
| const mockItems = [ | |
| { id: 'vec_a', text: 'KING', type: 'word_a', origin3D: new THREE.Vector3(0, 50, 0) } | |
| ]; | |
| threadLabels.setLabels(mockItems); | |
| expect(threadLabels.labels.length).toBe(1); | |
| threadLabels.clear(); | |
| expect(threadLabels.labels.length).toBe(0); | |
| const cards = container.querySelectorAll('.thread-label-card'); | |
| expect(cards.length).toBe(0); | |
| }); | |
| it('updates origins in-place during reorder tween', () => { | |
| threadLabels.setLabels([ | |
| { id: 'tok_0', text: 'king', type: 'compare', origin3D: new THREE.Vector3(0, 10, 0) }, | |
| { id: 'tok_1', text: 'queen', type: 'compare', origin3D: new THREE.Vector3(0, 0, 0) }, | |
| ]); | |
| threadLabels.updateOrigins([ | |
| { id: 'tok_0', origin3D: new THREE.Vector3(0, 0, 0) }, | |
| { id: 'tok_1', origin3D: new THREE.Vector3(0, 10, 0) }, | |
| ]); | |
| expect(threadLabels.labels[0].origin3D.y).toBe(0); | |
| expect(threadLabels.labels[1].origin3D.y).toBe(10); | |
| expect(threadLabels.labels.length).toBe(2); | |
| }); | |
| it('registers group-label cards with distinct class', () => { | |
| threadLabels.setLabels([ | |
| { id: 'group:G1', text: 'GROUP_1', type: 'group', origin3D: new THREE.Vector3(0, 5, 0) }, | |
| { id: 'tok_0', text: 'car', type: 'compare', origin3D: new THREE.Vector3(0, 10, 0) }, | |
| ]); | |
| expect(threadLabels.labels[0].screenOffsetX).toBeGreaterThan(0); | |
| expect(threadLabels.labels[1].screenOffsetX).toBe(0); | |
| const cards = container.querySelectorAll('.thread-label-card'); | |
| expect(cards[0].classList.contains('group-label')).toBe(true); | |
| }); | |
| it('rebuilds DOM when updateOrigins switches tokens → group badges', () => { | |
| threadLabels.setLabels([ | |
| { id: 'tok_0', text: 'car', type: 'compare', origin3D: new THREE.Vector3(0, 10, 0) }, | |
| { id: 'tok_1', text: 'grace', type: 'compare', origin3D: new THREE.Vector3(0, -10, 0) }, | |
| ]); | |
| expect(threadLabels.labels).toHaveLength(2); | |
| threadLabels.updateOrigins([ | |
| { id: 'group:GROUP_1', text: 'GROUP_1', type: 'group', origin3D: new THREE.Vector3(0, 5, 0) }, | |
| { id: 'group:GROUP_2', text: 'GROUP_2', type: 'group', origin3D: new THREE.Vector3(0, -5, 0) }, | |
| ]); | |
| expect(threadLabels.labels).toHaveLength(2); | |
| expect(threadLabels.labels[0].id).toBe('group:GROUP_1'); | |
| expect(threadLabels.labels[0].type).toBe('group'); | |
| const cards = container.querySelectorAll('.thread-label-card'); | |
| expect(cards).toHaveLength(2); | |
| expect(cards[0].classList.contains('group-label')).toBe(true); | |
| }); | |
| it('hides and shows the overlay via setVisible', () => { | |
| threadLabels.setVisible(false); | |
| expect(threadLabels.isVisible()).toBe(false); | |
| const overlay = container.querySelector('#thread-labels-container'); | |
| expect(overlay.classList.contains('is-hidden')).toBe(true); | |
| threadLabels.setVisible(true); | |
| expect(threadLabels.isVisible()).toBe(true); | |
| expect(overlay.classList.contains('is-hidden')).toBe(false); | |
| }); | |
| }); | |