Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 137 additions & 0 deletions src/__tests__/events-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { test, expect, describe, vi } from 'vitest';
import { EventsDispatcher, CallbackFunction } from '../events-dispatcher';

describe('EventsDispatcher', () => {
describe('addEventListener', () => {
test('registers a callback for a single event', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener('testEvent', callback);
dispatcher.emit('testEvent', 'arg1');

expect(callback).toHaveBeenCalledTimes(1);
expect(callback).toHaveBeenCalledWith('arg1');
});

test('registers a callback for multiple events', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener(['event1', 'event2'], callback);
dispatcher.emit('event1', 'arg1');
dispatcher.emit('event2', 'arg2');

expect(callback).toHaveBeenCalledTimes(2);
expect(callback).toHaveBeenNthCalledWith(1, 'arg1');
expect(callback).toHaveBeenNthCalledWith(2, 'arg2');
});

test('allows multiple callbacks for the same event', () => {
const dispatcher = new EventsDispatcher();
const callback1 = vi.fn();
const callback2 = vi.fn();

dispatcher.addEventListener('testEvent', callback1);
dispatcher.addEventListener('testEvent', callback2);
dispatcher.emit('testEvent', 'arg');

expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).toHaveBeenCalledTimes(1);
});
});

describe('emit', () => {
test('calls all registered callbacks for an event', () => {
const dispatcher = new EventsDispatcher();
const callback1 = vi.fn();
const callback2 = vi.fn();

dispatcher.addEventListener('testEvent', callback1);
dispatcher.addEventListener('testEvent', callback2);
dispatcher.emit('testEvent', 'data');

expect(callback1).toHaveBeenCalledWith('data');
expect(callback2).toHaveBeenCalledWith('data');
});

test('does nothing when emitting an unregistered event', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener('registeredEvent', callback);
dispatcher.emit('unregisteredEvent', 'data');

expect(callback).not.toHaveBeenCalled();
});

test('passes array arguments as spread parameters', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener('testEvent', callback);
dispatcher.emit('testEvent', ['arg1', 'arg2', 'arg3']);

expect(callback).toHaveBeenCalledWith('arg1', 'arg2', 'arg3');
});

test('wraps non-array arguments in array', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener('testEvent', callback);
dispatcher.emit('testEvent', { key: 'value' });

expect(callback).toHaveBeenCalledWith({ key: 'value' });
});

test('handles undefined arguments', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener('testEvent', callback);
dispatcher.emit('testEvent', undefined);

expect(callback).toHaveBeenCalledWith(undefined);
});

test('handles null arguments', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener('testEvent', callback);
dispatcher.emit('testEvent', null);

expect(callback).toHaveBeenCalledWith(null);
});
});

describe('integration', () => {
test('callbacks are independent per event type', () => {
const dispatcher = new EventsDispatcher();
const callback1 = vi.fn();
const callback2 = vi.fn();

dispatcher.addEventListener('event1', callback1);
dispatcher.addEventListener('event2', callback2);

dispatcher.emit('event1', 'data1');

expect(callback1).toHaveBeenCalledTimes(1);
expect(callback2).not.toHaveBeenCalled();
});

test('same callback can be registered for different events', () => {
const dispatcher = new EventsDispatcher();
const callback = vi.fn();

dispatcher.addEventListener('event1', callback);
dispatcher.addEventListener('event2', callback);

dispatcher.emit('event1', 'data1');
dispatcher.emit('event2', 'data2');

expect(callback).toHaveBeenCalledTimes(2);
});
});
});
235 changes: 235 additions & 0 deletions src/__tests__/objects-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
import { test, expect, describe, vi, beforeEach } from 'vitest';
import { ObjectsManager } from '../objects-manager';
import { Path, PathType } from '../path';
import { Scene, Color, Group } from 'three';

describe('ObjectsManager', () => {
let scene: Scene;
let objectsManager: ObjectsManager;

beforeEach(() => {
scene = new Scene();
objectsManager = new ObjectsManager(scene, 0.4, 0.2, 0.6);
});

describe('constructor', () => {
test('creates extrusions and travel moves groups', () => {
expect(objectsManager.extrusionsGroup).toBeInstanceOf(Group);
expect(objectsManager.travelMovesGroup).toBeInstanceOf(Group);
});

test('adds groups to scene', () => {
expect(scene.children).toContain(objectsManager.extrusionsGroup);
expect(scene.children).toContain(objectsManager.travelMovesGroup);
});

test('names the groups correctly', () => {
expect(objectsManager.extrusionsGroup.name).toBe('Extrusions');
expect(objectsManager.travelMovesGroup.name).toBe('Travel Moves');
});

test('sets line width and height', () => {
expect(objectsManager.lineWidth).toBe(0.4);
expect(objectsManager.lineHeight).toBe(0.2);
});

test('sets extrusion width', () => {
expect(objectsManager.extrusionWidth).toBe(0.6);
});

test('creates clipping planes', () => {
expect(objectsManager.clippingPlanes.length).toBe(2);
});
});

describe('visibility controls', () => {
describe('hideTravels', () => {
test('sets travelMovesGroup visibility to false', () => {
objectsManager.showTravels();
objectsManager.hideTravels();

expect(objectsManager.travelMovesGroup.visible).toBe(false);
});
});

describe('showTravels', () => {
test('sets travelMovesGroup visibility to true', () => {
objectsManager.hideTravels();
objectsManager.showTravels();

expect(objectsManager.travelMovesGroup.visible).toBe(true);
});
});

describe('hideExtrusions', () => {
test('sets extrusionsGroup visibility to false', () => {
objectsManager.showExtrusions();
objectsManager.hideExtrusions();

expect(objectsManager.extrusionsGroup.visible).toBe(false);
});
});

describe('showExtrusions', () => {
test('sets extrusionsGroup visibility to true', () => {
objectsManager.hideExtrusions();
objectsManager.showExtrusions();

expect(objectsManager.extrusionsGroup.visible).toBe(true);
});
});
});

describe('renderTravelLines', () => {
test('adds rendered paths to travelMovesGroup', () => {
const path = createTestPath();
const color = new Color(0xff0000);

objectsManager.renderTravelLines([path], color);

expect(objectsManager.travelMovesGroup.children.length).toBe(1);
});

test('does not re-render already rendered paths', () => {
const path = createTestPath();
const color = new Color(0xff0000);

objectsManager.renderTravelLines([path], color);
objectsManager.renderTravelLines([path], color);

expect(objectsManager.travelMovesGroup.children.length).toBe(2);
});

test('renders only unrendered paths from a mixed array', () => {
const path1 = createTestPath();
const path2 = createTestPath();
const color = new Color(0xff0000);

objectsManager.renderTravelLines([path1], color);
const initialChildren = objectsManager.travelMovesGroup.children.length;

objectsManager.renderTravelLines([path1, path2], color);

expect(objectsManager.travelMovesGroup.children.length).toBe(initialChildren + 1);
});
});

describe('renderExtrusionLines', () => {
test('adds rendered paths to extrusionsGroup', () => {
const path = createTestPath();
const color = new Color(0x00ff00);

objectsManager.renderExtrusionLines([path], color);

expect(objectsManager.extrusionsGroup.children.length).toBe(1);
});

test('does not re-render already rendered paths', () => {
const path = createTestPath();
const color = new Color(0x00ff00);

objectsManager.renderExtrusionLines([path], color);
objectsManager.renderExtrusionLines([path], color);

expect(objectsManager.extrusionsGroup.children.length).toBe(2);
});
});

describe('renderExtrusionTubes', () => {
test('adds rendered paths to extrusionsGroup', () => {
const path = createTestPath();
const color = new Color(0x0000ff);

objectsManager.renderExtrusionTubes([path], color);

expect(objectsManager.extrusionsGroup.children.length).toBe(1);
});

test('does not re-render already rendered paths', () => {
const path = createTestPath();
const color = new Color(0x0000ff);

objectsManager.renderExtrusionTubes([path], color);
objectsManager.renderExtrusionTubes([path], color);

expect(objectsManager.extrusionsGroup.children.length).toBe(2);
});

test('adds material to materials array', () => {
const path = createTestPath();
const color = new Color(0x0000ff);

const initialMaterialCount = objectsManager.materials.length;
objectsManager.renderExtrusionTubes([path], color);

expect(objectsManager.materials.length).toBe(initialMaterialCount + 1);
});
});

describe('dispose', () => {
test('removes extrusionsGroup from parent', () => {
objectsManager.dispose();

expect(scene.children).not.toContain(objectsManager.extrusionsGroup);
});

test('removes travelMovesGroup from parent', () => {
objectsManager.dispose();

expect(scene.children).not.toContain(objectsManager.travelMovesGroup);
});

test('disposes all disposables', () => {
const mockDisposable = { dispose: vi.fn() };
objectsManager.disposables.push(mockDisposable);

objectsManager.dispose();

expect(mockDisposable.dispose).toHaveBeenCalledTimes(1);
});

test('clears disposables array after dispose', () => {
const mockDisposable = { dispose: vi.fn() };
objectsManager.disposables.push(mockDisposable);

objectsManager.dispose();

expect(objectsManager.disposables.length).toBe(0);
});
});

describe('updateClippingPlanes', () => {
test('updates shader materials with min and max Z values', () => {
const path = createTestPath();
const color = new Color(0x0000ff);
objectsManager.renderExtrusionTubes([path], color);

objectsManager.updateClippingPlanes(1.0, 10.0);

objectsManager.materials.forEach((material) => {
expect(material.uniforms.clipMinY.value).toBe(1.0);
expect(material.uniforms.clipMaxY.value).toBe(10.0);
});
});
});

describe('group orientation', () => {
test('groups are rotated to match coordinate system', () => {
const expectedRotation = -Math.PI / 2;

const extrusionEuler = objectsManager.extrusionsGroup.rotation;
const travelEuler = objectsManager.travelMovesGroup.rotation;

expect(extrusionEuler.x).toBeCloseTo(expectedRotation, 5);
expect(travelEuler.x).toBeCloseTo(expectedRotation, 5);
});
});
});

function createTestPath(): Path {
const path = new Path(PathType.Extrusion, 0.6, 0.2, 0);
path.addPoint(0, 0, 0);
path.addPoint(10, 0, 0);
path.addPoint(10, 10, 0);
return path;
}
Loading