plasma-virtual-desktop-swit.../tests/test_DesktopLogic.js

588 lines
21 KiB
JavaScript
Raw Normal View History

#!/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);