Add ESLint config and Node.js test suite (198/198 passing)
This commit is contained in:
parent
dc0705d235
commit
b8f6889cb4
5 changed files with 1538 additions and 2 deletions
23
eslint.config.js
Normal file
23
eslint.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import js from '@eslint/js';
|
||||
import globals from 'globals';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
globals: {
|
||||
...globals.browser,
|
||||
...globals.node,
|
||||
},
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
rules: {
|
||||
'no-var': 'error',
|
||||
'no-unused-vars': ['warn', { args: 'none' }],
|
||||
'semi': ['error', 'always'],
|
||||
'no-console': 'off',
|
||||
'no-constant-condition': 'off',
|
||||
},
|
||||
},
|
||||
];
|
||||
1089
package-lock.json
generated
Normal file
1089
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
16
package.json
Normal file
16
package.json
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "sorting-visualization",
|
||||
"version": "1.0.0",
|
||||
"description": "Sorting algorithm visualization - single-file HTML app",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "eslint sorting_visualization.html",
|
||||
"test": "node test_algorithms.js",
|
||||
"test:verbose": "node test_algorithms.js --verbose"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^9.0.0",
|
||||
"@eslint/js": "^9.0.0",
|
||||
"globals": "^16.0.0"
|
||||
}
|
||||
}
|
||||
|
|
@ -885,8 +885,6 @@ let isPaused = false;
|
|||
let arrayFresh = false; // true after Reset generated a new array, reset to false when sort starts
|
||||
let startTime = 0; // performance.now() when animation started
|
||||
let elapsedMs = 0; // accumulated ms across pauses
|
||||
let startTime = 0; // performance.now() when animation started
|
||||
let elapsedMs = 0; // accumulated ms across pauses
|
||||
|
||||
// ================================================================
|
||||
// UI Helpers
|
||||
|
|
|
|||
410
test_algorithms.js
Normal file
410
test_algorithms.js
Normal file
|
|
@ -0,0 +1,410 @@
|
|||
/**
|
||||
* Test-Skript für Sortieralgorithmen
|
||||
* Extrahiert buildSteps aus sorting_visualization.html und testet alle Algorithmen
|
||||
*
|
||||
* Usage:
|
||||
* node test_algorithms.js # Kurzbericht
|
||||
* node test_algorithms.js --verbose # Detaillierte Ausgabe
|
||||
*/
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import vm from 'node:vm';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const verbose = process.argv.includes('--verbose');
|
||||
|
||||
// ── HTML parsen und JS extrahieren ──
|
||||
const html = fs.readFileSync(path.join(__dirname, 'sorting_visualization.html'), 'utf-8');
|
||||
|
||||
// Finde den letzten <script>-Block (der unseren Code enthält), ignoriere CDN-Scripts
|
||||
const scriptBlocks = [];
|
||||
let idx = 0;
|
||||
while (true) {
|
||||
const openIdx = html.indexOf('<script', idx);
|
||||
if (openIdx === -1) break;
|
||||
const closeIdx = html.indexOf('>', openIdx);
|
||||
if (closeIdx === -1) break;
|
||||
const tag = html.substring(openIdx, closeIdx + 1);
|
||||
const endIdx = html.indexOf('</script>', closeIdx);
|
||||
if (endIdx === -1) break;
|
||||
const content = html.substring(closeIdx + 1, endIdx);
|
||||
scriptBlocks.push({ tag, content, start: closeIdx + 1 });
|
||||
idx = endIdx + 9;
|
||||
}
|
||||
|
||||
// Nimm den Block der 'function buildSteps' enthält
|
||||
const codeBlock = scriptBlocks.find(b => b.content.includes('function buildSteps'));
|
||||
if (!codeBlock) {
|
||||
console.error('Fehler: Kein <script>-Block mit buildSteps gefunden');
|
||||
process.exit(1);
|
||||
}
|
||||
const rawJs = codeBlock.content;
|
||||
|
||||
// ── Mocks ──
|
||||
const mockEl = (overrides = {}) => ({
|
||||
classList: { add: () => {}, remove: () => {}, contains: () => false },
|
||||
style: {},
|
||||
textContent: '',
|
||||
innerHTML: '',
|
||||
value: '50',
|
||||
min: '5',
|
||||
max: '200',
|
||||
clientWidth: 800,
|
||||
getBoundingClientRect: () => ({ left: 0, top: 0, width: 800, height: 400 }),
|
||||
addEventListener: () => {},
|
||||
setPointerCapture: () => {},
|
||||
dispatchEvent: () => {},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const mockCtx = {
|
||||
clearRect: () => {}, fillRect: () => {}, strokeRect: () => {},
|
||||
beginPath: () => {}, closePath: () => {}, moveTo: () => {},
|
||||
lineTo: () => {}, quadraticCurveTo: () => {}, arc: () => {},
|
||||
fill: () => {}, stroke: () => {}, save: () => {}, restore: () => {},
|
||||
setTransform: () => {},
|
||||
};
|
||||
|
||||
const mockCanvasEl = {
|
||||
width: 800,
|
||||
height: 400,
|
||||
style: {},
|
||||
parentElement: { clientWidth: 800 },
|
||||
getContext: () => mockCtx,
|
||||
};
|
||||
|
||||
// ── VM-Kontext erstellen ──
|
||||
const context = vm.createContext({
|
||||
console, Math, Array, Object, String, Number,
|
||||
parseInt, parseFloat, Date, Set, Map, JSON,
|
||||
Infinity, NaN, undefined,
|
||||
|
||||
navigator: { hardwareConcurrency: 4 },
|
||||
|
||||
window: {
|
||||
devicePixelRatio: 1,
|
||||
setInterval: () => 0,
|
||||
clearInterval: () => {},
|
||||
setTimeout: () => 0,
|
||||
clearTimeout: () => {},
|
||||
matchMedia: () => ({ matches: false }),
|
||||
localStorage: { getItem: () => null, setItem: () => {} },
|
||||
history: { replaceState: () => {} },
|
||||
location: { pathname: '/', search: '' },
|
||||
innerWidth: 800,
|
||||
addEventListener: () => {},
|
||||
},
|
||||
|
||||
document: {
|
||||
getElementById: (id) => {
|
||||
if (id === 'sortCanvas' || id === 'memCanvas') return mockCanvasEl;
|
||||
return mockEl();
|
||||
},
|
||||
querySelector: () => mockEl(),
|
||||
querySelectorAll: () => [],
|
||||
createElement: () => mockEl(),
|
||||
documentElement: { classList: { contains: () => true } },
|
||||
},
|
||||
|
||||
canvas: mockCanvasEl,
|
||||
ctx: mockCtx,
|
||||
memCanvas: mockCanvasEl,
|
||||
memCtx: mockCtx,
|
||||
lucide: { createIcons: () => {} },
|
||||
|
||||
$algoSelect: mockEl({ value: 'bubble' }),
|
||||
$sizeSlider: mockEl(),
|
||||
$speedSlider: mockEl(),
|
||||
$threadSlider: mockEl(),
|
||||
$presetSelect: mockEl({ value: 'random' }),
|
||||
$customArray: mockEl({ value: '' }),
|
||||
$btnPlayPause: mockEl(),
|
||||
$btnStep: mockEl(),
|
||||
$btnStepBack: mockEl(),
|
||||
$btnReset: mockEl(),
|
||||
$btnTheme: mockEl(),
|
||||
$btnClearCustom: mockEl(),
|
||||
$statComp: mockEl(),
|
||||
$statSwap: mockEl(),
|
||||
$statMove: mockEl(),
|
||||
$statStep: mockEl(),
|
||||
$statTime: mockEl(),
|
||||
$speedVal: mockEl(),
|
||||
$sizeVal: mockEl(),
|
||||
$threadVal: mockEl(),
|
||||
$memContainer: mockEl(),
|
||||
$memLabel: mockEl(),
|
||||
$progressFill: mockEl(),
|
||||
$stepCounter: mockEl(),
|
||||
$phaseLabel: mockEl(),
|
||||
$stepExplanation: mockEl(),
|
||||
|
||||
// State-Variablen die von 'let' zu 'var'-artigen werden
|
||||
array: [],
|
||||
steps: [],
|
||||
stepIndex: 0,
|
||||
animTimer: null,
|
||||
isRunning: false,
|
||||
isPaused: false,
|
||||
arrayFresh: false,
|
||||
startTime: 0,
|
||||
elapsedMs: 0,
|
||||
});
|
||||
|
||||
// ── JS vorbereiten und ausführen ──
|
||||
// Finde die buildSteps-Funktion
|
||||
const buildStepsStart = rawJs.indexOf('function buildSteps(algoName)');
|
||||
if (buildStepsStart === -1) {
|
||||
console.error('Fehler: buildSteps-Funktion nicht gefunden');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Finde das Ende von buildSteps durch Brace-Counting
|
||||
let braceCount = 0;
|
||||
let funcEnd = rawJs.length;
|
||||
for (let i = buildStepsStart; i < rawJs.length; i++) {
|
||||
if (rawJs[i] === '{') braceCount++;
|
||||
if (rawJs[i] === '}') {
|
||||
braceCount--;
|
||||
if (braceCount === 0) {
|
||||
funcEnd = i + 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Nimm alles vom Anfang bis zum Ende von buildSteps
|
||||
// Finde den Anfang: const COLORS
|
||||
const colorsStart = rawJs.indexOf('const COLORS');
|
||||
if (colorsStart === -1) {
|
||||
console.error('Fehler: COLORS-Konstanten nicht gefunden');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const codeToRun = rawJs.substring(colorsStart, funcEnd);
|
||||
|
||||
// Bereinige: Entferne Code der nicht benötigt wird oder fehlschlägt
|
||||
const cleanedCode = codeToRun
|
||||
.replace(/makeTouchSlider\(\$.*?\);/g, '')
|
||||
.replace(/resizeCanvas\(\);/g, '')
|
||||
.replace(/const prefersReducedMotion[\s\S]*?el\.classList\.add\('anim-stat'\);\s*\}\s*\)\s*;?/g, '')
|
||||
.replace(/lucide\.createIcons\(\);/g, '')
|
||||
.replace(/\(function initTheme\(\)[\s\S]*?\}\)\(\);/g, '')
|
||||
.replace(/refreshColors\(\);/g, '')
|
||||
.replace(/updateSpeedLabel\(\);/g, '')
|
||||
.replace(/updateSizeLabel\(\);/g, '')
|
||||
.replace(/updateThreadSlider\(\);/g, '')
|
||||
.replace(/updateButtonStates\(\);/g, '')
|
||||
.replace(/updateProgress\(0, 0\);/g, '')
|
||||
.replace(/renderCurrent\(\);/g, '')
|
||||
.replace(/generateArray\(\);/g, '')
|
||||
.replace(/arrayFresh = true;/g, '')
|
||||
// Ersetze 'let array = []' durch Zuweisung an context.array
|
||||
.replace(/let array\s*=\s*\[\];/, 'array = [];')
|
||||
// Entferne andere let-Deklarationen die schon im Kontext existieren
|
||||
.replace(/let steps\s*=\s*\[\];/, 'steps = [];')
|
||||
.replace(/let stepIndex\s*=\s*0;/, 'stepIndex = 0;')
|
||||
.replace(/let animTimer\s*=\s*null;/, 'animTimer = null;')
|
||||
.replace(/let isRunning\s*=\s*false;/, 'isRunning = false;')
|
||||
.replace(/let isPaused\s*=\s*false;/, 'isPaused = false;')
|
||||
.replace(/let arrayFresh\s*=\s*false;[\s\S]*?let startTime\s*=\s*0;[\s\S]*?let elapsedMs\s*=\s*0;/,
|
||||
'arrayFresh = false;\nlet startTime = 0;\nlet elapsedMs = 0;');
|
||||
|
||||
try {
|
||||
vm.runInContext(cleanedCode, context, { filename: 'sorting_visualization.html' });
|
||||
} catch (e) {
|
||||
console.error('Fehler beim Ausführen des JavaScript-Codes:', e.message);
|
||||
console.error(e.stack.split('\n').slice(0, 5).join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const buildSteps = context.buildSteps;
|
||||
if (!buildSteps) {
|
||||
console.error('Fehler: buildSteps wurde nicht exportiert');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Test-Framework ──
|
||||
const PRESETS = ['random', 'sorted', 'reversed', 'nearly', 'duplicates'];
|
||||
const ALGORITHMS = [
|
||||
'bubble', 'selection', 'insertion', 'cocktail',
|
||||
'merge', 'heap', 'quick', 'quick3way', 'dualpivot', 'introsort',
|
||||
'shell', 'tree', 'timsort',
|
||||
'bitonic',
|
||||
'counting', 'radix', 'bucket',
|
||||
'pancake', 'cycle', 'bogo',
|
||||
];
|
||||
|
||||
const Bogo_MAX_SIZE = 6;
|
||||
|
||||
let totalTests = 0;
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
const failures = [];
|
||||
|
||||
function isSorted(arr) {
|
||||
for (let i = 0; i < arr.length - 1; i++) {
|
||||
if (arr[i] > arr[i + 1]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function arraysEqual(a, b) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function generatePresetArray(preset, size) {
|
||||
let arr = [];
|
||||
switch (preset) {
|
||||
case 'random':
|
||||
for (let i = 0; i < size; i++) arr.push(Math.floor(Math.random() * 100) + 1);
|
||||
break;
|
||||
case 'sorted':
|
||||
for (let i = 1; i <= size; i++) arr.push(i);
|
||||
break;
|
||||
case 'reversed':
|
||||
for (let i = size; i >= 1; i--) arr.push(i);
|
||||
break;
|
||||
case 'nearly': {
|
||||
for (let i = 1; i <= size; i++) arr.push(i);
|
||||
const swaps = Math.max(1, Math.floor(size * 0.05));
|
||||
for (let s = 0; s < swaps; s++) {
|
||||
const i = Math.floor(Math.random() * size);
|
||||
const j = Math.floor(Math.random() * size);
|
||||
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'duplicates': {
|
||||
const values = [5, 15, 25, 35, 45];
|
||||
for (let i = 0; i < size; i++) arr.push(values[Math.floor(Math.random() * values.length)]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
function testAlgorithm(algo, preset, size) {
|
||||
totalTests++;
|
||||
const arr = generatePresetArray(preset, size);
|
||||
|
||||
context.array = arr.slice();
|
||||
|
||||
try {
|
||||
const steps = buildSteps(algo);
|
||||
const lastStep = steps[steps.length - 1];
|
||||
|
||||
if (lastStep.type !== 'done') {
|
||||
throw new Error(`Letzter Step type '${lastStep.type}', erwartet 'done'`);
|
||||
}
|
||||
|
||||
if (!isSorted(lastStep.array)) {
|
||||
throw new Error(`Array ist nicht sortiert`);
|
||||
}
|
||||
|
||||
// Prüfe Array-Länge (einige Algos wie Counting/Radix runden Werte)
|
||||
if (lastStep.array.length !== arr.length) {
|
||||
throw new Error(`Array-Länge geändert: ${lastStep.array.length} statt ${arr.length}`);
|
||||
}
|
||||
|
||||
passed++;
|
||||
if (verbose) {
|
||||
console.log(` ✅ ${algo} + ${preset} (${size} Elemente, ${steps.length} Steps)`);
|
||||
}
|
||||
} catch (e) {
|
||||
failed++;
|
||||
failures.push({ algo, preset, size, error: e.message });
|
||||
console.log(` ❌ ${algo} + ${preset} (${size} Elemente): ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Tests ausführen ──
|
||||
console.log('\n🧪 Sortieralgorithmen-Tests\n');
|
||||
console.log('═'.repeat(50));
|
||||
|
||||
const NORMAL_SIZES = [10, 50];
|
||||
|
||||
for (const algo of ALGORITHMS) {
|
||||
if (verbose) console.log(`\n📦 ${algo}:`);
|
||||
for (const preset of PRESETS) {
|
||||
for (const size of NORMAL_SIZES) {
|
||||
if (algo === 'bogo' && size > Bogo_MAX_SIZE) continue;
|
||||
testAlgorithm(algo, preset, size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edge Cases
|
||||
if (verbose) console.log('\n📦 Edge Cases:');
|
||||
|
||||
context.array = [];
|
||||
try {
|
||||
const steps = buildSteps('bubble');
|
||||
if (steps.length > 0 && steps[steps.length - 1].type === 'done') {
|
||||
passed++; totalTests++;
|
||||
if (verbose) console.log(' ✅ bubble + empty array');
|
||||
} else {
|
||||
throw new Error('Kein done-Step');
|
||||
}
|
||||
} catch (e) {
|
||||
failed++; totalTests++;
|
||||
console.log(` ❌ bubble + empty array: ${e.message}`);
|
||||
}
|
||||
|
||||
context.array = [42];
|
||||
try {
|
||||
const steps = buildSteps('bubble');
|
||||
const last = steps[steps.length - 1];
|
||||
if (last.type === 'done' && isSorted(last.array)) {
|
||||
passed++; totalTests++;
|
||||
if (verbose) console.log(' ✅ bubble + single element');
|
||||
} else {
|
||||
throw new Error('Nicht sortiert');
|
||||
}
|
||||
} catch (e) {
|
||||
failed++; totalTests++;
|
||||
console.log(` ❌ bubble + single element: ${e.message}`);
|
||||
}
|
||||
|
||||
// Performance-Test (100 Elemente)
|
||||
if (verbose) console.log('\n📦 Performance-Test (100 Elemente):');
|
||||
for (const algo of ['bubble', 'merge', 'quick', 'heap', 'shell', 'timsort']) {
|
||||
const arr = [];
|
||||
for (let i = 0; i < 100; i++) arr.push(Math.floor(Math.random() * 1000) + 1);
|
||||
context.array = arr.slice();
|
||||
|
||||
const start = Date.now();
|
||||
const steps = buildSteps(algo);
|
||||
const elapsed = Date.now() - start;
|
||||
|
||||
totalTests++;
|
||||
if (isSorted(steps[steps.length - 1].array)) {
|
||||
passed++;
|
||||
if (verbose) console.log(` ✅ ${algo}: ${steps.length} Steps in ${elapsed}ms`);
|
||||
} else {
|
||||
failed++;
|
||||
console.log(` ❌ ${algo}: nicht sortiert (${elapsed}ms)`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ergebnis ──
|
||||
console.log('\n' + '═'.repeat(50));
|
||||
console.log(`\nErgebnis: ${passed}/${totalTests} bestanden${failed > 0 ? `, ${failed} fehlgeschlagen` : ''}`);
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.log('\nFehler:');
|
||||
for (const f of failures) {
|
||||
console.log(` - ${f.algo} + ${f.preset} (${f.size}): ${f.error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('\n✅ Alle Tests bestanden!');
|
||||
process.exit(0);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue