llm-semantic-visualizer / tests /ThreadLabels.test.js
hbauzan's picture
fix(sae,compare): unstick train UI and restore GROUP_* badges
bfd323f
Raw
History Blame Contribute Delete
6.2 kB
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);
});
});