initial commit of project

generated by OpenCode and MiniMax M3.1 Flash Free
This commit is contained in:
2026-10-20 14:11:56 +02:00
parent 354a83d651
commit 5b4bfd928d
9 changed files with 3964 additions and 1 deletions
+396
View File
@@ -0,0 +1,396 @@
/* =====================================================================
JSON DevTools - logicke testy (bez prehliadaca, bez zavislosti)
Spustenie: node tests/logic.test.js
===================================================================== */
'use strict';
const fs = require('fs');
const path = require('path');
const vm = require('vm');
/* ---------------------- minimalny DOM stub ---------------------- */
function makeElement(tag) {
const el = {
tagName: (tag || 'div').toUpperCase(),
children: [],
attributes: {},
dataset: {},
style: {
_props: {},
setProperty(k, v) { this._props[k] = v; },
getPropertyValue(k) { return this._props[k] || ''; }
},
classList: {
_set: new Set(),
add(...c) { c.forEach((x) => this._set.add(x)); },
remove(...c) { c.forEach((x) => this._set.delete(x)); },
contains(c) { return this._set.has(c); },
toggle(c, f) { if (f === undefined) { this._set.has(c) ? this._set.delete(c) : this._set.add(c); } else if (f) { this._set.add(c); } else { this._set.delete(c); } }
},
value: '',
textContent: '',
innerHTML: '',
hidden: false,
title: '',
type: '',
tabIndex: 0,
files: [],
checked: false,
addEventListener() {}, removeEventListener() {},
setAttribute(k, v) { this.attributes[k] = v; },
getAttribute(k) { return this.attributes[k]; },
removeAttribute(k) { delete this.attributes[k]; },
appendChild(c) { this.children.push(c); return c; },
removeChild(c) { const i = this.children.indexOf(c); if (i >= 0) this.children.splice(i, 1); return c; },
insertBefore(c) { this.children.unshift(c); return c; },
querySelector() { return makeElement('div'); },
querySelectorAll() { return []; },
closest() { return null; },
focus() {}, blur() {}, click() {},
getBoundingClientRect() { return { left: 0, top: 0, width: 800, height: 600, right: 800, bottom: 600 }; },
dispatchEvent() { return true; }
};
return el;
}
function createSandbox() {
const store = Object.create(null);
const localStorage = {
getItem(k) { return Object.prototype.hasOwnProperty.call(store, k) ? store[k] : null; },
setItem(k, v) { store[k] = String(v); },
removeItem(k) { delete store[k]; },
clear() { for (const k in store) delete store[k]; }
};
const documentStub = {
readyState: 'complete',
documentElement: makeElement('html'),
body: makeElement('body'),
createElement: (t) => makeElement(t),
querySelector: () => makeElement('div'),
querySelectorAll: () => [],
addEventListener() {},
removeEventListener() {},
execCommand() { return true; },
createTextNode: (t) => ({ text: t })
};
const windowStub = {
document: documentStub,
localStorage,
matchMedia: () => ({ matches: false, addEventListener() {} }),
addEventListener() {},
removeEventListener() {},
isSecureContext: false,
location: { hash: '', search: '', href: 'file:///index.html' },
history: { replaceState() {} },
getComputedStyle: () => ({ getPropertyValue: () => '' }),
navigator: { clipboard: null },
performance: { now: () => Date.now() },
setTimeout, clearTimeout, setInterval, clearInterval,
Blob: globalThis.Blob,
URL: { createObjectURL: () => 'blob:stub', revokeObjectURL() {} },
console
};
windowStub.window = windowStub;
windowStub.globalThis = windowStub;
const sandbox = {
window: windowStub,
document: documentStub,
localStorage,
navigator: windowStub.navigator,
location: windowStub.location,
history: windowStub.history,
performance: windowStub.performance,
matchMedia: windowStub.matchMedia,
console,
setTimeout, clearTimeout, setInterval, clearInterval,
Blob: globalThis.Blob,
URL: windowStub.URL,
Math, JSON, Date, Array, Object, String, Number, Boolean, Error, TypeError,
RegExp, Promise, Map, Set, isNaN, parseInt, parseFloat, encodeURIComponent,
decodeURIComponent, btoa: (s) => Buffer.from(s, 'binary').toString('base64'),
atob: (s) => Buffer.from(s, 'base64').toString('binary')
};
sandbox.globalThis = sandbox;
return sandbox;
}
/* ---------------------- nacitanie aplikacie ---------------------- */
const appPath = path.join(__dirname, '..', 'src', 'app.js');
const source = fs.readFileSync(appPath, 'utf8');
const sandbox = createSandbox();
vm.createContext(sandbox);
vm.runInContext(source, sandbox, { filename: 'app.js' });
const D = sandbox.window.JSONDevTools;
if (!D) {
console.error('FAIL: aplikacia sa nenačítala (window.JSONDevTools chýba)');
process.exit(1);
}
/* ---------------------- testovacie makra ---------------------- */
let passed = 0, failed = 0;
const failures = [];
function ok(name, cond, extra) {
if (cond) { passed++; }
else { failed++; failures.push(name + (extra !== undefined ? ' >> ' + extra : '')); }
}
function eq(name, a, b) { ok(name, a === b, 'got ' + JSON.stringify(a) + ' expected ' + JSON.stringify(b)); }
function deepEq(name, a, b) { ok(name, JSON.stringify(a) === JSON.stringify(b), 'got ' + JSON.stringify(a) + ' expected ' + JSON.stringify(b)); }
function section(t) { console.log('\n--- ' + t + ' ---'); }
/* ======================= TESTY ======================= */
section('Utils');
eq('typeOf null', D.Utils.typeOf(null), 'null');
eq('typeOf array', D.Utils.typeOf([]), 'array');
eq('typeOf object', D.Utils.typeOf({}), 'object');
eq('typeOf number', D.Utils.typeOf(1), 'number');
eq('typeOf string', D.Utils.typeOf('a'), 'string');
eq('typeOf undefined', D.Utils.typeOf(undefined), 'undefined');
eq('isPlainObject', D.Utils.isPlainObject({}), true);
eq('isPlainObject array', D.Utils.isPlainObject([]), false);
eq('pathSegment simple', D.Utils.pathSegment('abc'), '.abc');
eq('pathSegment underscore', D.Utils.pathSegment('_a1'), '._a1');
eq('pathSegment digits start', D.Utils.pathSegment('1a'), "['1a']");
eq('pathSegment space', D.Utils.pathSegment('a b'), "['a b']");
eq('pathSegment quote', D.Utils.pathSegment("it's"), "['it\\'s']");
eq('formatBytes B', D.Utils.formatBytes(12), '12 B');
eq('formatBytes KB', D.Utils.formatBytes(2048), '2.0 KB');
eq('formatBytes MB', D.Utils.formatBytes(3 * 1024 * 1024), '3.00 MB');
eq('formatDuration ms', D.Utils.formatDuration(12.4), '12 ms');
eq('formatDuration sub-ms', D.Utils.formatDuration(0.5), '0.50 ms');
eq('escapeHtml', D.Utils.escapeHtml('<b>&"\''), '&lt;b&gt;&amp;&quot;&#39;');
ok('uid uniq', D.Utils.uid() !== D.Utils.uid());
ok('formatTime format', /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(D.Utils.formatTime(Date.now())));
section('localStorage helper');
sandbox.localStorage.setItem('t.probe', '"v"');
eq('store.get', D.Utils.store.get('t.probe', null), 'v');
eq('store.get fallback', D.Utils.store.get('t.missing', 'fb'), 'fb');
D.Utils.store.set('t.probe2', { a: 1 });
deepEq('store.set/get object', D.Utils.store.get('t.probe2', null), { a: 1 });
D.Utils.store.remove('t.probe2');
eq('store.remove', D.Utils.store.get('t.probe2', null), null);
section('Highlighter');
const hl = D.Highlighter.highlight('{"a": 1, "b": "x", "c": true, "d": false, "e": null, "f": -1.5e3}');
ok('key token', hl.includes('tok-key'));
ok('string token', hl.includes('tok-str'));
ok('number token', hl.includes('tok-num'));
ok('bool token', hl.includes('tok-bool'));
ok('null token', hl.includes('tok-null'));
ok('punc token', hl.includes('tok-punc'));
ok('html escaped', D.Highlighter.highlight('<script>x</script>').includes('&lt;script&gt;'));
ok('no raw script tag', !D.Highlighter.highlight('</script>').includes('</script>'));
ok('highlightMatch wraps', D.Highlighter.highlightMatch('abcdef', 'cd').includes('<mark class="hl">cd</mark>'));
ok('highlightMatch case-insensitive', D.Highlighter.highlightMatch('ABCDEF', 'cd').includes('<mark class="hl">CD</mark>'));
ok('highlightMatch no match', !D.Highlighter.highlightMatch('abc', 'zz').includes('mark'));
section('Diff algoritmus');
{
const out = [];
D.Diff.walk({ a: 1 }, { a: 1 }, '$', out);
eq('zhodne -> ziadny rozdiel', out.length, 0);
const out2 = [];
D.Diff.walk({ a: 1 }, { a: 2 }, '$', out2);
eq('zmenena hodnota', out2.length, 1);
eq('typ zmeny', out2[0].type, 'changed');
eq('cesta zmeny', out2[0].path, '$.a');
eq('stara hodnota', out2[0].old, 1);
eq('nova hodnota', out2[0].new, 2);
const out3 = [];
D.Diff.walk({ a: 1 }, { a: 1, b: 2 }, '$', out3);
eq('pridany kluc', out3.length, 1);
eq('typ pridany', out3[0].type, 'added');
eq('cesta pridaneho', out3[0].path, '$.b');
const out4 = [];
D.Diff.walk({ a: 1, b: 2 }, { a: 1 }, '$', out4);
eq('odstraneny kluc', out4[0].type, 'removed');
eq('cesta odstraneneho', out4[0].path, '$.b');
const out5 = [];
D.Diff.walk({ a: [1, 2, 3] }, { a: [1, 2] }, '$', out5);
eq('kratke pole -> odstranene', out5.length, 1);
eq('cesta prvku pola', out5[0].path, '$.a[2]');
const out6 = [];
D.Diff.walk({ a: [1] }, { a: [1, 9] }, '$', out6);
eq('dlhsie pole -> pridane', out6[0].type, 'added');
eq('cesta pridaneho prvku', out6[0].path, '$.a[1]');
const out7 = [];
D.Diff.walk({ a: 1 }, { a: '1' }, '$', out7);
eq('zmena typu', out7[0].type, 'changed');
eq('stary typ', out7[0].oldType, 'number');
eq('novy typ', out7[0].newType, 'string');
const out8 = [];
D.Diff.walk({ a: { b: { c: 1 } } }, { a: { b: { c: 2 } } }, '$', out8);
eq('vnorene porovnanie', out8[0].path, '$.a.b.c');
const out9 = [];
D.Diff.walk({ 'weird key': 1 }, { 'weird key': 2 }, '$', out9);
eq('kluc so specialnymi znakmi', out9[0].path, "$['weird key']");
const out10 = [];
D.Diff.walk([1, 2], [1, 2, 3], '$', out10);
eq('pole na urovni korena', out10[0].path, '$[2]');
}
section('Historia – sanitizácia');
{
const saved = [];
const realSet = sandbox.localStorage.setItem.bind(sandbox.localStorage);
sandbox.localStorage.setItem = (k, v) => { saved.push([k, v]); return realSet(k, v); };
D.History.saveRequest({
method: 'POST',
url: 'https://api.example.com/v1/users',
status: 201,
statusText: 'Created',
duration: 42,
requestHeaders: { 'Content-Type': 'application/json', 'Authorization': 'Bearer SECRET123', 'Cookie': 'a=b', 'X-Api-Key': 'K' },
requestBody: '{"a":1}',
responsePreview: '{"ok":true}',
isJson: true
});
const raw = saved.filter(([k]) => k.indexOf('requests') !== -1).map(([, v]) => v).join('');
ok('Authorization odstranena', raw.toLowerCase().indexOf('authorization') === -1, raw.slice(0, 200));
ok('secret token nie je ulozeny', raw.indexOf('SECRET123') === -1);
ok('cookie odstranena', raw.toLowerCase().indexOf('cookie') === -1);
ok('x-api-key odstraneny', raw.toLowerCase().indexOf('x-api-key') === -1);
ok('content-type zachovany', raw.indexOf('Content-Type') !== -1);
ok('url zaznamany', raw.indexOf('api.example.com') !== -1);
ok('status zaznamany', raw.indexOf('201') !== -1);
sandbox.localStorage.setItem = realSet;
}
section('Historia – dokumenty');
{
sandbox.localStorage.removeItem('jsondevtools.history.docs');
D.History.saveDocument('{"x":1}');
D.History.saveDocument('{"x":1}');
D.History.saveDocument('{"y":2}');
const docs = JSON.parse(sandbox.localStorage.getItem('jsondevtools.history.docs'));
eq('duplicitne sa nededuplikuju', docs.length, 2);
eq('najnovsi je prvy', docs[0].text, '{"y":2}');
for (let i = 0; i < 30; i++) D.History.saveDocument('{"i":' + i + '}');
const many = JSON.parse(sandbox.localStorage.getItem('jsondevtools.history.docs'));
ok('historia obmedzena na 20', many.length <= 20, 'n=' + many.length);
sandbox.localStorage.removeItem('jsondevtools.history.docs');
}
section('Theme');
{
const before = D.Theme.current;
D.Theme.toggle();
ok('tema sa prepne', D.Theme.current !== before);
eq('tema persistovana', JSON.parse(sandbox.localStorage.getItem('jsondevtools.theme')), D.Theme.current);
D.Theme.toggle();
eq('tema spat vratena', D.Theme.current, before);
}
section('App – navigacia');
{
eq('readHash default', D.App.readHash(), 'formatter');
D.App.go('diff');
eq('go nastavi current', D.App.current, 'diff');
D.App.go('neexistujuci');
eq('neplatny view -> formatter', D.App.current, 'formatter');
D.App.go('tree');
eq('go tree', D.App.current, 'tree');
}
section('Parser chyb');
{
// parseJSON je vnútorná funkcia – overujeme cez verejné API (Formatter.validate)
const D2 = D;
const before = D2.Formatter.input.value;
D2.Formatter.input.value = '{"a":1,,}';
const res = D2.Formatter.validate();
eq('neplatny JSON -> chyba', res.ok, false);
ok('chyba obsahuje poziciu', /position|position|Unexpected/i.test(res.error), res.error);
D2.Formatter.input.value = before;
D2.Formatter.validate();
}
section('API – URL a hlavicky (bez siete)');
{
const el = (id, val) => { const e = makeElement('input'); e.value = val; return e; };
const api = D.Api;
// buildUrl cez parametre
api.params = [{ key: 'q', value: 'a b' }, { key: 'page', value: '2' }, { key: '', value: 'x' }];
const savedUrl = api.buildUrl ? null : null;
// buildUrl cita #req-url, ktory v stub-e nie je nastaveny -> vratime prazdny string
ok('buildUrl existuje', typeof api.buildUrl === 'function');
api.headers = [{ key: 'Accept', value: 'application/json' }];
api.params = [];
ok('buildHeaders existuje', typeof api.buildHeaders === 'function');
}
section('Cesta uzla – parent path');
{
eq('parent $', D.parentPathOf('$.a'), '$');
eq('parent hnedom', D.parentPathOf('$.a.b'), '$.a');
eq('parent prvku pola', D.parentPathOf('$.a[0]'), '$.a');
eq('parent prvku pola 2', D.parentPathOf('$.a.b[12]'), '$.a.b');
eq('parent kluc so specialmi znakmi', D.parentPathOf("$['a b'].c"), "$['a b']");
eq('parent root je null', D.parentPathOf('$'), null);
}
section('Kontrola konzistencie DOM id');
{
const html = fs.readFileSync(path.join(__dirname, '..', 'src', 'index.html'), 'utf8');
const ids = Array.from(html.matchAll(/\bid="([^"]+)"/g)).map((m) => m[1]);
const dupes = ids.filter((v, i) => ids.indexOf(v) !== i);
eq('ziadne duplicitne id v HTML', dupes.length, 0);
const js = fs.readFileSync(path.join(__dirname, '..', 'src', 'app.js'), 'utf8');
const used = Array.from(new Set(Array.from(js.matchAll(/\$\('#([a-zA-Z0-9_-]+)'\)/g)).map((m) => m[1])));
const missing = used.filter((u) => ids.indexOf(u) === -1);
eq('vsetky pouzite id existuju v HTML', missing.length, 0);
if (missing.length) console.log(' chybne id: ' + missing.join(', '));
// duplicitne aria-controls
const dupAria = (html.match(/aria-controls="view-formatter"/g) || []).length;
eq('aria-controls bez duplicitov', dupAria, 1);
// kazde tlacidlo s id musi mat handlov v app.js
// (tab-y sa obsluhuju delegovane cez triedu '.tab')
const btnIds = Array.from(html.matchAll(/<button[^>]*\bid="([^"]+)"/g)).map((m) => m[1]);
const btnMissing = btnIds.filter((b) => {
if (b.indexOf('tab-') === 0) return js.indexOf("'.tab'") === -1;
return js.indexOf("'#" + b + "'") === -1 && js.indexOf('"#' + b + '"') === -1;
});
eq('vsetky tlacidla s id su napojene v app.js', btnMissing.length, 0);
if (btnMissing.length) console.log(' chybne handlery pre: ' + btnMissing.join(', '));
// kazdy view ma tab aj panel
['formatter', 'tree', 'diff', 'request', 'history'].forEach((v) => {
ok('view ' + v + ' ma tab aj panel',
html.indexOf('id="tab-' + v + '"') !== -1 && html.indexOf('id="view-' + v + '"') !== -1);
});
}
/* ---------------------- vysledok ---------------------- */
console.log('\n========================================');
console.log('Vysledok: ' + passed + ' ok, ' + failed + ' neuspechov');
if (failures.length) {
console.log('\nNeuspechy:');
failures.forEach((f) => console.log(' FAIL ' + f));
}
console.log('========================================');
process.exit(failed ? 1 : 0);