/**
* GodotBridge Unit Tests
* Tests TCP client functionality without requiring Godot runtime
* ISO/IEC 25010 compliant test coverage
*/
import { jest } from '@jest/globals';
import { GodotBridge, getGodotBridge } from './GodotBridge.js';
describe('GodotBridge', () => {
describe('constructor', () => {
it('should create instance with default host and port', () => {
const bridge = new GodotBridge();
expect(bridge).toBeInstanceOf(GodotBridge);
expect(bridge.isConnected()).toBe(false);
});
it('should create instance with custom host and port', () => {
const bridge = new GodotBridge('192.168.1.100', 7000);
expect(bridge).toBeInstanceOf(GodotBridge);
});
});
describe('singleton', () => {
it('should return same instance from getGodotBridge', () => {
const bridge1 = getGodotBridge();
const bridge2 = getGodotBridge();
expect(bridge1).toBe(bridge2);
});
});
describe('isConnected', () => {
it('should return false when not connected', () => {
const bridge = new GodotBridge();
expect(bridge.isConnected()).toBe(false);
});
});
describe('disconnect', () => {
it('should handle disconnect when not connected', () => {
const bridge = new GodotBridge();
expect(() => bridge.disconnect()).not.toThrow();
expect(bridge.isConnected()).toBe(false);
});
});
describe('event emitter', () => {
it('should emit events', () => {
const bridge = new GodotBridge();
const errorHandler = jest.fn();
bridge.on('error', errorHandler);
bridge.emit('error', new Error('test'));
expect(errorHandler).toHaveBeenCalled();
});
});
});
describe('GodotBridge message parsing', () => {
// Test the response parsing logic indirectly
describe('JSON response format', () => {
it('should handle valid JSON response structure', () => {
const response = {
id: 'req_1',
success: true,
result: { message: 'pong', version: '1.0.0' },
};
expect(response.success).toBe(true);
expect(response.result.message).toBe('pong');
});
it('should handle error response structure', () => {
const response = {
id: 'req_1',
success: false,
error: { code: 'not_found', message: 'Script not found' },
};
expect(response.success).toBe(false);
expect(response.error.code).toBe('not_found');
});
});
describe('request format', () => {
it('should format ping command correctly', () => {
const request = {
id: 'req_1',
command: 'ping',
params: {},
};
const json = JSON.stringify(request);
expect(json).toContain('"command":"ping"');
});
it('should format validate_script command correctly', () => {
const request = {
id: 'req_2',
command: 'validate_script',
params: { path: 'res://scripts/player.gd' },
};
const json = JSON.stringify(request);
expect(json).toContain('"command":"validate_script"');
expect(json).toContain('res://scripts/player.gd');
});
});
});
describe('GodotBridge types', () => {
describe('EditorInfo structure', () => {
it('should match expected interface', () => {
const editorInfo = {
godot_version: { major: 4, minor: 5, patch: 1, status: 'stable' },
editor_scale: 1.0,
current_path: 'res://',
playing: false,
};
expect(editorInfo.godot_version.major).toBe(4);
expect(typeof editorInfo.playing).toBe('boolean');
});
});
describe('ScriptValidationResult structure', () => {
it('should handle valid script', () => {
const result = {
valid: true,
errors: [],
};
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it('should handle script with errors', () => {
const result = {
valid: false,
errors: [
{ message: 'Unexpected token', line: 10, column: 5 },
{ message: 'Undefined variable', line: 15 },
],
};
expect(result.valid).toBe(false);
expect(result.errors).toHaveLength(2);
expect(result.errors[0].line).toBe(10);
});
});
describe('OpenScene structure', () => {
it('should match expected interface', () => {
const scene = {
name: 'Main',
path: 'res://scenes/main.tscn',
type: 'Node2D',
};
expect(scene.path).toContain('res://');
});
});
describe('SelectedNode structure', () => {
it('should match expected interface', () => {
const node = {
name: 'Player',
type: 'CharacterBody2D',
path: '/root/Main/Player',
};
expect(node.path).toContain('/root/');
});
});
describe('Autoload structure', () => {
it('should match expected interface', () => {
const autoload = {
name: 'GameManager',
path: '*res://scripts/game_manager.gd',
};
expect(autoload.name).toBe('GameManager');
});
});
});
describe('Connection timeout behavior', () => {
it('should have reasonable default timeout', () => {
// Connection timeout should be reasonable (5-10 seconds)
const CONNECTION_TIMEOUT = 5000;
expect(CONNECTION_TIMEOUT).toBeGreaterThanOrEqual(1000);
expect(CONNECTION_TIMEOUT).toBeLessThanOrEqual(30000);
});
it('should have reasonable request timeout', () => {
// Request timeout should allow for complex operations
const REQUEST_TIMEOUT = 10000;
expect(REQUEST_TIMEOUT).toBeGreaterThanOrEqual(5000);
expect(REQUEST_TIMEOUT).toBeLessThanOrEqual(60000);
});
});
describe('Buffer handling', () => {
it('should handle newline-delimited JSON', () => {
const messages = [
'{"id":"1","success":true}',
'{"id":"2","success":false}',
];
const buffer = messages.join('\n') + '\n';
const parts = buffer.split('\n').filter((p) => p.trim());
expect(parts).toHaveLength(2);
expect(JSON.parse(parts[0]).id).toBe('1');
expect(JSON.parse(parts[1]).id).toBe('2');
});
it('should handle partial messages', () => {
const partial1 = '{"id":"1","suc';
const partial2 = 'cess":true}\n';
const combined = partial1 + partial2;
const newlineIndex = combined.indexOf('\n');
expect(newlineIndex).toBeGreaterThan(0);
const complete = combined.slice(0, newlineIndex);
expect(() => JSON.parse(complete)).not.toThrow();
});
});