Files
JSONdevTools/tests/logic.test.js
T
IgorMino 5b4bfd928d initial commit of project
generated by OpenCode and MiniMax M3.1 Flash Free
2026-10-20 14:11:56 +02:00

397 lines
16 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =====================================================================
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);