588 lines
21 KiB
JavaScript
588 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Test Suite for DesktopLogic.js
|
|
* Provides comprehensive coverage of all pure functions
|
|
* Run: node test_DesktopLogic.js
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
// Load and prepare the module
|
|
const srcPath = path.join(__dirname, '../contents/ui/DesktopLogic.js');
|
|
let code = fs.readFileSync(srcPath, 'utf8');
|
|
code = code.replace('.pragma library', '');
|
|
code += `
|
|
module.exports = {
|
|
buildRemoveCommand, buildRenameCommand, buildCreateCommand, buildSwapWindowsCommand,
|
|
escapeShell, calculateGrid, calculatePreviewHeight, calculateScale,
|
|
nextDesktop, canSwap, findDropTarget, distance, isDragStarted,
|
|
isValidIndex, isValidName, clamp
|
|
};`;
|
|
const tmpPath = '/tmp/DesktopLogic_test.js';
|
|
fs.writeFileSync(tmpPath, code);
|
|
const Logic = require(tmpPath);
|
|
|
|
// Test utilities
|
|
let passed = 0, failed = 0, total = 0;
|
|
const results = { passed: [], failed: [] };
|
|
|
|
function test(name, fn) {
|
|
total++;
|
|
try {
|
|
fn();
|
|
passed++;
|
|
results.passed.push(name);
|
|
console.log(` ✓ ${name}`);
|
|
} catch (e) {
|
|
failed++;
|
|
results.failed.push({ name, error: e.message });
|
|
console.log(` ✗ ${name}`);
|
|
console.log(` Error: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
function assertEqual(actual, expected, msg) {
|
|
if (actual !== expected) {
|
|
throw new Error(`${msg || 'Assertion failed'}: expected "${expected}", got "${actual}"`);
|
|
}
|
|
}
|
|
|
|
function assertDeepEqual(actual, expected, msg) {
|
|
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
|
|
throw new Error(`${msg || 'Assertion failed'}: expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
|
|
}
|
|
}
|
|
|
|
function assertTrue(condition, msg) {
|
|
if (!condition) throw new Error(msg || 'Expected true');
|
|
}
|
|
|
|
function assertFalse(condition, msg) {
|
|
if (condition) throw new Error(msg || 'Expected false');
|
|
}
|
|
|
|
function assertNull(value, msg) {
|
|
if (value !== null) throw new Error(`${msg || 'Expected null'}, got ${value}`);
|
|
}
|
|
|
|
function assertNotNull(value, msg) {
|
|
if (value === null) throw new Error(msg || 'Expected non-null');
|
|
}
|
|
|
|
function assertContains(str, substr, msg) {
|
|
if (!str.includes(substr)) {
|
|
throw new Error(`${msg || 'String does not contain'}: "${substr}" not in "${str}"`);
|
|
}
|
|
}
|
|
|
|
function assertApprox(actual, expected, tolerance, msg) {
|
|
if (Math.abs(actual - expected) > tolerance) {
|
|
throw new Error(`${msg || 'Approximation failed'}: expected ~${expected}, got ${actual}`);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// TEST SUITES
|
|
// ============================================================================
|
|
|
|
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
console.log('║ DesktopLogic.js - Comprehensive Test Suite ║');
|
|
console.log('╚════════════════════════════════════════════════════════════════╝\n');
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('┌─ buildRemoveCommand ─────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('returns valid command for valid ID', () => {
|
|
const result = Logic.buildRemoveCommand('abc-123');
|
|
assertEqual(result, "qdbus org.kde.KWin /VirtualDesktopManager removeDesktop 'abc-123'");
|
|
});
|
|
|
|
test('returns null for null ID', () => {
|
|
assertNull(Logic.buildRemoveCommand(null));
|
|
});
|
|
|
|
test('returns null for undefined ID', () => {
|
|
assertNull(Logic.buildRemoveCommand(undefined));
|
|
});
|
|
|
|
test('returns null for empty string ID', () => {
|
|
assertNull(Logic.buildRemoveCommand(''));
|
|
});
|
|
|
|
test('returns null for whitespace-only ID', () => {
|
|
assertNull(Logic.buildRemoveCommand(' '));
|
|
});
|
|
|
|
test('returns null for numeric ID', () => {
|
|
assertNull(Logic.buildRemoveCommand(123));
|
|
});
|
|
|
|
test('handles UUID format', () => {
|
|
const result = Logic.buildRemoveCommand('550e8400-e29b-41d4-a716-446655440000');
|
|
assertContains(result, '550e8400-e29b-41d4-a716-446655440000');
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ buildRenameCommand ─────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('returns valid command for valid inputs', () => {
|
|
const result = Logic.buildRenameCommand('id1', 'Work');
|
|
assertEqual(result, "qdbus org.kde.KWin /VirtualDesktopManager setDesktopName 'id1' 'Work'");
|
|
});
|
|
|
|
test('escapes single quotes in name', () => {
|
|
const result = Logic.buildRenameCommand('id1', "it's mine");
|
|
assertContains(result, "it'\\''s mine");
|
|
});
|
|
|
|
test('trims whitespace from name', () => {
|
|
const result = Logic.buildRenameCommand('id1', ' Work ');
|
|
assertContains(result, "'Work'");
|
|
});
|
|
|
|
test('returns null for null ID', () => {
|
|
assertNull(Logic.buildRenameCommand(null, 'name'));
|
|
});
|
|
|
|
test('returns null for null name', () => {
|
|
assertNull(Logic.buildRenameCommand('id', null));
|
|
});
|
|
|
|
test('returns null for empty name', () => {
|
|
assertNull(Logic.buildRenameCommand('id', ''));
|
|
});
|
|
|
|
test('returns null for whitespace-only name', () => {
|
|
assertNull(Logic.buildRenameCommand('id', ' '));
|
|
});
|
|
|
|
test('handles special characters in name', () => {
|
|
const result = Logic.buildRenameCommand('id', 'Dev & Test');
|
|
assertContains(result, 'Dev & Test');
|
|
});
|
|
|
|
test('handles unicode in name', () => {
|
|
const result = Logic.buildRenameCommand('id', '桌面');
|
|
assertContains(result, '桌面');
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ buildCreateCommand ───────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('returns valid command for position 0', () => {
|
|
const result = Logic.buildCreateCommand(0, 'New Desktop');
|
|
assertEqual(result, "qdbus org.kde.KWin /VirtualDesktopManager createDesktop 0 'New Desktop'");
|
|
});
|
|
|
|
test('returns valid command for position 5', () => {
|
|
const result = Logic.buildCreateCommand(5, 'Desktop 6');
|
|
assertContains(result, 'createDesktop 5');
|
|
});
|
|
|
|
test('handles negative position (clamps to 0)', () => {
|
|
const result = Logic.buildCreateCommand(-1, 'Test');
|
|
assertContains(result, 'createDesktop 0');
|
|
});
|
|
|
|
test('handles float position (floors)', () => {
|
|
const result = Logic.buildCreateCommand(2.7, 'Test');
|
|
assertContains(result, 'createDesktop 2');
|
|
});
|
|
|
|
test('escapes single quotes in name', () => {
|
|
const result = Logic.buildCreateCommand(0, "John's Desktop");
|
|
assertContains(result, "John'\\''s Desktop");
|
|
});
|
|
|
|
test('handles null name (uses default)', () => {
|
|
const result = Logic.buildCreateCommand(0, null);
|
|
assertContains(result, "'Desktop'");
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ buildSwapWindowsCommand ────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('returns bash command with wmctrl', () => {
|
|
const result = Logic.buildSwapWindowsCommand(0, 1);
|
|
assertContains(result, 'bash -c');
|
|
assertContains(result, 'wmctrl');
|
|
});
|
|
|
|
test('includes both desktop indices', () => {
|
|
const result = Logic.buildSwapWindowsCommand(2, 5);
|
|
assertContains(result, '==2');
|
|
assertContains(result, '==5');
|
|
assertContains(result, '-t 5');
|
|
assertContains(result, '-t 2');
|
|
});
|
|
|
|
test('handles desktop 0', () => {
|
|
const result = Logic.buildSwapWindowsCommand(0, 3);
|
|
assertContains(result, '==0');
|
|
});
|
|
|
|
test('floors float indices', () => {
|
|
const result = Logic.buildSwapWindowsCommand(1.9, 3.1);
|
|
assertContains(result, '==1');
|
|
assertContains(result, '==3');
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ escapeShell ────────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('escapes single quote', () => {
|
|
assertEqual(Logic.escapeShell("it's"), "it'\\''s");
|
|
});
|
|
|
|
test('escapes multiple single quotes', () => {
|
|
assertEqual(Logic.escapeShell("it's John's"), "it'\\''s John'\\''s");
|
|
});
|
|
|
|
test('returns empty for null', () => {
|
|
assertEqual(Logic.escapeShell(null), '');
|
|
});
|
|
|
|
test('returns empty for undefined', () => {
|
|
assertEqual(Logic.escapeShell(undefined), '');
|
|
});
|
|
|
|
test('returns empty for number', () => {
|
|
assertEqual(Logic.escapeShell(123), '');
|
|
});
|
|
|
|
test('preserves string without quotes', () => {
|
|
assertEqual(Logic.escapeShell('hello world'), 'hello world');
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ calculateGrid ──────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('1 item = 1x1 grid', () => {
|
|
assertDeepEqual(Logic.calculateGrid(1), { cols: 1, rows: 1 });
|
|
});
|
|
|
|
test('2 items = 2x1 grid', () => {
|
|
assertDeepEqual(Logic.calculateGrid(2), { cols: 2, rows: 1 });
|
|
});
|
|
|
|
test('3 items = 2x2 grid', () => {
|
|
assertDeepEqual(Logic.calculateGrid(3), { cols: 2, rows: 2 });
|
|
});
|
|
|
|
test('4 items = 2x2 grid', () => {
|
|
assertDeepEqual(Logic.calculateGrid(4), { cols: 2, rows: 2 });
|
|
});
|
|
|
|
test('5 items = 3x2 grid', () => {
|
|
assertDeepEqual(Logic.calculateGrid(5), { cols: 3, rows: 2 });
|
|
});
|
|
|
|
test('9 items = 3x3 grid', () => {
|
|
assertDeepEqual(Logic.calculateGrid(9), { cols: 3, rows: 3 });
|
|
});
|
|
|
|
test('16 items = 4x4 grid', () => {
|
|
assertDeepEqual(Logic.calculateGrid(16), { cols: 4, rows: 4 });
|
|
});
|
|
|
|
test('0 items = 1x1 grid (minimum)', () => {
|
|
assertDeepEqual(Logic.calculateGrid(0), { cols: 1, rows: 1 });
|
|
});
|
|
|
|
test('negative items = 1x1 grid (minimum)', () => {
|
|
assertDeepEqual(Logic.calculateGrid(-5), { cols: 1, rows: 1 });
|
|
});
|
|
|
|
test('18 items = 5x4 grid', () => {
|
|
const result = Logic.calculateGrid(18);
|
|
assertTrue(result.cols * result.rows >= 18);
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ calculatePreviewHeight ─────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('maintains 16:9 aspect ratio', () => {
|
|
const height = Logic.calculatePreviewHeight(160, 1920, 1080);
|
|
assertApprox(height, 90, 0.01);
|
|
});
|
|
|
|
test('maintains 4:3 aspect ratio', () => {
|
|
const height = Logic.calculatePreviewHeight(120, 1024, 768);
|
|
assertApprox(height, 90, 0.01);
|
|
});
|
|
|
|
test('uses 16:9 default for zero width', () => {
|
|
const height = Logic.calculatePreviewHeight(160, 0, 0);
|
|
assertApprox(height, 90, 0.01);
|
|
});
|
|
|
|
test('uses 16:9 default for negative dimensions', () => {
|
|
const height = Logic.calculatePreviewHeight(160, -100, -100);
|
|
assertApprox(height, 90, 0.01);
|
|
});
|
|
|
|
test('handles ultrawide (21:9)', () => {
|
|
const height = Logic.calculatePreviewHeight(210, 2560, 1080);
|
|
assertApprox(height, 88.59, 0.1);
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ calculateScale ─────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('calculates correct scale factors', () => {
|
|
const scale = Logic.calculateScale(130, 73, 1920, 1080);
|
|
assertApprox(scale.x, 130/1920, 0.0001);
|
|
assertApprox(scale.y, 73/1080, 0.0001);
|
|
});
|
|
|
|
test('handles zero screen dimensions', () => {
|
|
const scale = Logic.calculateScale(130, 73, 0, 0);
|
|
assertEqual(scale.x, 130);
|
|
assertEqual(scale.y, 73);
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ nextDesktop ────────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('forward from 0 to 1', () => {
|
|
assertEqual(Logic.nextDesktop(0, 5, 1), 1);
|
|
});
|
|
|
|
test('forward from 4 wraps to 0', () => {
|
|
assertEqual(Logic.nextDesktop(4, 5, 1), 0);
|
|
});
|
|
|
|
test('backward from 0 wraps to 4', () => {
|
|
assertEqual(Logic.nextDesktop(0, 5, -1), 4);
|
|
});
|
|
|
|
test('backward from 2 to 1', () => {
|
|
assertEqual(Logic.nextDesktop(2, 5, -1), 1);
|
|
});
|
|
|
|
test('handles count of 1', () => {
|
|
assertEqual(Logic.nextDesktop(0, 1, 1), 0);
|
|
assertEqual(Logic.nextDesktop(0, 1, -1), 0);
|
|
});
|
|
|
|
test('handles count of 0 (returns 0)', () => {
|
|
assertEqual(Logic.nextDesktop(0, 0, 1), 0);
|
|
});
|
|
|
|
test('floors float current', () => {
|
|
assertEqual(Logic.nextDesktop(1.9, 5, 1), 2);
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ canSwap ────────────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('valid swap returns true', () => {
|
|
assertTrue(Logic.canSwap(0, 1, 5));
|
|
});
|
|
|
|
test('same index returns false', () => {
|
|
assertFalse(Logic.canSwap(2, 2, 5));
|
|
});
|
|
|
|
test('negative indexA returns false', () => {
|
|
assertFalse(Logic.canSwap(-1, 1, 5));
|
|
});
|
|
|
|
test('negative indexB returns false', () => {
|
|
assertFalse(Logic.canSwap(0, -1, 5));
|
|
});
|
|
|
|
test('indexA >= count returns false', () => {
|
|
assertFalse(Logic.canSwap(5, 1, 5));
|
|
});
|
|
|
|
test('indexB >= count returns false', () => {
|
|
assertFalse(Logic.canSwap(0, 5, 5));
|
|
});
|
|
|
|
test('non-number indexA returns false', () => {
|
|
assertFalse(Logic.canSwap('0', 1, 5));
|
|
});
|
|
|
|
test('non-number indexB returns false', () => {
|
|
assertFalse(Logic.canSwap(0, '1', 5));
|
|
});
|
|
|
|
test('non-number count returns false', () => {
|
|
assertFalse(Logic.canSwap(0, 1, '5'));
|
|
});
|
|
|
|
test('boundary valid: 0 and count-1', () => {
|
|
assertTrue(Logic.canSwap(0, 4, 5));
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ findDropTarget ─────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('returns -1 for null repeater', () => {
|
|
assertEqual(Logic.findDropTarget(null, {}, 0, 0), -1);
|
|
});
|
|
|
|
test('returns -1 for null grid', () => {
|
|
assertEqual(Logic.findDropTarget({count: 1}, null, 0, 0), -1);
|
|
});
|
|
|
|
test('returns -1 for invalid repeater', () => {
|
|
assertEqual(Logic.findDropTarget({}, {}, 0, 0), -1);
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ distance ─────────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('distance from origin', () => {
|
|
assertEqual(Logic.distance(0, 0, 3, 4), 5);
|
|
});
|
|
|
|
test('distance same point is 0', () => {
|
|
assertEqual(Logic.distance(5, 5, 5, 5), 0);
|
|
});
|
|
|
|
test('distance negative coordinates', () => {
|
|
assertEqual(Logic.distance(-3, 0, 0, 4), 5);
|
|
});
|
|
|
|
test('distance horizontal', () => {
|
|
assertEqual(Logic.distance(0, 0, 10, 0), 10);
|
|
});
|
|
|
|
test('distance vertical', () => {
|
|
assertEqual(Logic.distance(0, 0, 0, 10), 10);
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ isDragStarted ────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('returns false below threshold', () => {
|
|
assertFalse(Logic.isDragStarted(0, 0, 5, 5, 10));
|
|
});
|
|
|
|
test('returns true above threshold', () => {
|
|
assertTrue(Logic.isDragStarted(0, 0, 15, 0, 10));
|
|
});
|
|
|
|
test('returns false at exact threshold', () => {
|
|
assertFalse(Logic.isDragStarted(0, 0, 10, 0, 10));
|
|
});
|
|
|
|
test('uses default threshold of 10', () => {
|
|
assertFalse(Logic.isDragStarted(0, 0, 8, 0));
|
|
assertTrue(Logic.isDragStarted(0, 0, 12, 0));
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ isValidIndex ───────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('valid index returns true', () => {
|
|
assertTrue(Logic.isValidIndex(0, 5));
|
|
assertTrue(Logic.isValidIndex(4, 5));
|
|
});
|
|
|
|
test('negative index returns false', () => {
|
|
assertFalse(Logic.isValidIndex(-1, 5));
|
|
});
|
|
|
|
test('index >= count returns false', () => {
|
|
assertFalse(Logic.isValidIndex(5, 5));
|
|
});
|
|
|
|
test('float index returns false', () => {
|
|
assertFalse(Logic.isValidIndex(1.5, 5));
|
|
});
|
|
|
|
test('string index returns false', () => {
|
|
assertFalse(Logic.isValidIndex('0', 5));
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ isValidName ────────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('valid name returns true', () => {
|
|
assertTrue(Logic.isValidName('Work'));
|
|
});
|
|
|
|
test('empty string returns false', () => {
|
|
assertFalse(Logic.isValidName(''));
|
|
});
|
|
|
|
test('whitespace only returns false', () => {
|
|
assertFalse(Logic.isValidName(' '));
|
|
});
|
|
|
|
test('null returns false', () => {
|
|
assertFalse(Logic.isValidName(null));
|
|
});
|
|
|
|
test('number returns false', () => {
|
|
assertFalse(Logic.isValidName(123));
|
|
});
|
|
|
|
test('name with spaces returns true', () => {
|
|
assertTrue(Logic.isValidName('My Work'));
|
|
});
|
|
|
|
// ----------------------------------------------------------------------------
|
|
console.log('\n┌─ clamp ──────────────────────────────────────────────────────────');
|
|
// ----------------------------------------------------------------------------
|
|
|
|
test('value within range unchanged', () => {
|
|
assertEqual(Logic.clamp(50, 0, 100), 50);
|
|
});
|
|
|
|
test('value below min clamped', () => {
|
|
assertEqual(Logic.clamp(-10, 0, 100), 0);
|
|
});
|
|
|
|
test('value above max clamped', () => {
|
|
assertEqual(Logic.clamp(150, 0, 100), 100);
|
|
});
|
|
|
|
test('value at min unchanged', () => {
|
|
assertEqual(Logic.clamp(0, 0, 100), 0);
|
|
});
|
|
|
|
test('value at max unchanged', () => {
|
|
assertEqual(Logic.clamp(100, 0, 100), 100);
|
|
});
|
|
|
|
// ============================================================================
|
|
// RESULTS
|
|
// ============================================================================
|
|
|
|
console.log('\n╔════════════════════════════════════════════════════════════════╗');
|
|
console.log('║ TEST RESULTS ║');
|
|
console.log('╠════════════════════════════════════════════════════════════════╣');
|
|
console.log(`║ Total: ${String(total).padStart(3)} ║`);
|
|
console.log(`║ Passed: ${String(passed).padStart(3)} ✓ ║`);
|
|
console.log(`║ Failed: ${String(failed).padStart(3)} ${failed > 0 ? '✗' : ' '} ║`);
|
|
console.log(`║ Coverage: ${((passed/total)*100).toFixed(1)}% ║`);
|
|
console.log('╚════════════════════════════════════════════════════════════════╝');
|
|
|
|
if (failed > 0) {
|
|
console.log('\n Failed tests:');
|
|
results.failed.forEach(f => console.log(` - ${f.name}: ${f.error}`));
|
|
}
|
|
|
|
console.log(failed === 0 ? '\n✓ All tests passed!' : '\n✗ Some tests failed');
|
|
process.exit(failed > 0 ? 1 : 0);
|