From 5b4bfd928d0a0f2fd9e8b874d3243e69d4145fd6 Mon Sep 17 00:00:00 2001 From: igor Date: Tue, 20 Oct 2026 14:11:56 +0200 Subject: [PATCH] initial commit of project generated by OpenCode and MiniMax M3.1 Flash Free --- AGENTS.md | 85 ++ README.md | 159 ++- docs/Screenshot-API-Request.png | Bin 0 -> 69444 bytes docs/Screenshot-Formatter.png | Bin 0 -> 93076 bytes docs/Screenshot-Tree.png | Bin 0 -> 121536 bytes src/app.js | 2015 +++++++++++++++++++++++++++++++ src/index.html | 326 +++++ src/style.css | 984 +++++++++++++++ tests/logic.test.js | 396 ++++++ 9 files changed, 3964 insertions(+), 1 deletion(-) create mode 100644 AGENTS.md create mode 100644 docs/Screenshot-API-Request.png create mode 100644 docs/Screenshot-Formatter.png create mode 100644 docs/Screenshot-Tree.png create mode 100644 src/app.js create mode 100644 src/index.html create mode 100644 src/style.css create mode 100644 tests/logic.test.js diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..fa49424 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,85 @@ +# AGENTS.md — JSON DevTools + +Statická jednostránková aplikácia (vanilla ES6+, HTML/CSS/JS). Bez frameworkov, +bez npm, bez build procesu, bez backendu, bez CDN. Bez CI, `package.json` ani +lint konfigurácie — repozitár je hotový, nič sa nekompiluje. + +## ZákAZ KONTROL V PREHLIADAČOCH + +**V prehliadačoch sa nesú overovanie aplikácie – používateľ to robí sám.** +Neotváraj Chrome/Chromium ani iný prehliadač, neautomatizuj ich, nescreenshotuj +cez prehliadač. Trvalé používateľské nariadenie, zapísané aj v globálnej pamäti +`~/.config/opencode/AGENTS.md`. + +Povolené: statická kontrola (`node --check`), testy logiky v Node.js, `curl`/`python`. +Nepridávaj do repozitára testy ani nástroje založené na prehliadači. + +## Štruktúra + +``` +src/index.html src/style.css src/app.js # aplikácia – musia zostať v jednej zložke +tests/logic.test.js # Node testy (bez prehliadača) +docs/ # screenshoty pre README +``` + +* `src/index.html` načítava `style.css` a `app.js` **relatívnou cestou** → všetky + tri súbory musia zostať v `src/`. Presun do rootu rozbije načítanie aj testy. +* Aplikácia sa otvára ako **`src/index.html`** (relatívne cesty), nie cez root. +* Repo hostuje **Gitea**, nie GitHub — nepridávaj GitHub Actions/Pages šablóny. + +## Príkazy + +```bash +node --check src/app.js # rýchla syntax kontrola +node tests/logic.test.js # testy logiky – 93 assertov, exit 1 pri chybe +python -m http.server 8080 --directory src # lokálne spustenie (voliteľné) +``` + +Nie je čo `npm install`ovať ani buildovať – po úprave `src/*.js|css|html` stačí +reload stránky. + +## Ako funguje `tests/logic.test.js` + +* `src/app.js` sa načíta do `vm` sandboxu s **ručným DOM stubom** (žiadne jsdom). + Aplikácia sa musí vedieť načítať bez reálneho DOM. +* Stub vracia **nový generický element pri každom `querySelector()`** → DOM + interakcie (tlačidlá, eventy, fokus) sa v Node testoch **neoverujú**. Overuj + logiku cez `window.JSONDevTools`, nie cez DOM. +* `src/app.js` musí na konci exportovať **`window.JSONDevTools`** (objekt s + modulmi `Utils, Toast, Theme, Splitter, Highlighter, Doc, Formatter, FileIO, + Tree, Diff, Api, History, App`) — bez neho testy skončí chybou. +* Testy čítajú `../src/app.js` a `../src/index.html` → pri presunu súborov treba + upraviť cesty v `tests/logic.test.js`. +* Pomocníci: `ok(name, cond, extra)`, `eq(name, a, b)`, `deepEq(...)`, `section(t)`. +* Testy obsahujú aj statické kontroly konzistencie: každé `$('#id')` v `app.js` + musí existovať v `index.html`, každé ` + + + + + + +
+ + +
+ + +
+ + +
+ + +
+
+
+

Vstup

+ 0 B +
+
+ +
+
+ + + +
+
+

Výstup

+ Idle +
+
+

+        
+ +
+
+
+ + + + + + + + + + + + + +
+ + + + +
+ + + + + + diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..90e2730 --- /dev/null +++ b/src/style.css @@ -0,0 +1,984 @@ +/* ===================================================================== + JSON DevTools - stylesheet + Dark / Light themes via CSS custom properties on [data-theme] + ===================================================================== */ + +:root { + --font-ui: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + --font-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", "Courier New", monospace; + + --radius: 8px; + --radius-sm: 5px; + --header-h: 52px; + --dur: 140ms; +} + +/* ------------------------------ DARK ------------------------------ */ +html[data-theme="dark"] { + --bg: #0e1116; + --bg-2: #141922; + --bg-3: #1a212c; + --bg-elev: #1c2430; + --panel: #121821; + --panel-head: #161d27; + --border: #232c3a; + --border-strong: #33405400; + --text: #dfe6f1; + --text-dim: #9aa8bd; + --text-faint: #6b7a91; + --accent: #4c9aff; + --accent-hover: #6aacff; + --accent-soft: rgba(76, 154, 255, 0.14); + --green: #4ec98a; + --green-soft: rgba(78, 201, 138, 0.13); + --red: #f2777a; + --red-soft: rgba(242, 119, 122, 0.13); + --orange: #e5a54b; + --orange-soft: rgba(229, 165, 75, 0.14); + --purple: #c08cf0; + --purple-soft: rgba(192, 140, 240, 0.14); + --cyan: #56c9d8; + --shadow: 0 8px 28px rgba(0, 0, 0, 0.5); + --sel: rgba(76, 154, 255, 0.28); + color-scheme: dark; +} + +/* ------------------------------ LIGHT ----------------------------- */ +html[data-theme="light"] { + --bg: #f2f4f8; + --bg-2: #ffffff; + --bg-3: #eceff5; + --bg-elev: #ffffff; + --panel: #ffffff; + --panel-head: #f7f9fc; + --border: #dbe1ea; + --text: #1d2735; + --text-dim: #5a6779; + --text-faint: #8a95a6; + --accent: #1a6fe0; + --accent-hover: #0d5cc7; + --accent-soft: rgba(26, 111, 224, 0.1); + --green: #12855a; + --green-soft: rgba(18, 133, 90, 0.1); + --red: #c9373d; + --red-soft: rgba(201, 55, 61, 0.1); + --orange: #a86a12; + --orange-soft: rgba(168, 106, 18, 0.12); + --purple: #7a41bd; + --purple-soft: rgba(122, 65, 189, 0.1); + --cyan: #0f7c8c; + --shadow: 0 6px 22px rgba(24, 39, 62, 0.12); + --sel: rgba(26, 111, 224, 0.2); + color-scheme: light; +} + +/* ------------------------------ BASE ------------------------------ */ +* { box-sizing: border-box; } + +html, body { + height: 100%; + margin: 0; +} + +body { + background: var(--bg); + color: var(--text); + font-family: var(--font-ui); + font-size: 14px; + line-height: 1.5; + display: flex; + flex-direction: column; + overflow: hidden; + -webkit-font-smoothing: antialiased; +} + +::selection { background: var(--sel); } + +button, input, select, textarea { + font-family: inherit; + font-size: inherit; + color: inherit; +} + +:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; + border-radius: 3px; +} + +.skip-link { + position: absolute; + left: -9999px; + top: 0; + z-index: 200; + background: var(--accent); + color: #fff; + padding: 10px 16px; + border-radius: 0 0 var(--radius) 0; + text-decoration: none; +} +.skip-link:focus { left: 0; } + +/* ------------------------------ HEADER ---------------------------- */ +.app-header { + height: var(--header-h); + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 16px; + padding: 0 14px; + background: var(--bg-2); + border-bottom: 1px solid var(--border); + z-index: 20; +} + +.brand { + display: flex; + align-items: center; + gap: 9px; + font-weight: 650; + letter-spacing: -0.01em; + white-space: nowrap; +} +.brand .logo { + font-family: var(--font-mono); + font-weight: 700; + color: var(--accent); + background: var(--accent-soft); + border: 1px solid var(--accent-soft); + padding: 2px 7px; + border-radius: var(--radius-sm); + font-size: 13px; +} +.brand-name { font-size: 15px; } + +.tabs { + display: flex; + gap: 2px; + flex: 1 1 auto; + min-width: 0; + overflow-x: auto; + scrollbar-width: none; +} +.tabs::-webkit-scrollbar { display: none; } + +.tab { + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius-sm); + color: var(--text-dim); + padding: 7px 14px; + cursor: pointer; + white-space: nowrap; + font-weight: 500; + transition: background var(--dur), color var(--dur); +} +.tab:hover { background: var(--bg-3); color: var(--text); } +.tab[aria-selected="true"] { + background: var(--accent-soft); + color: var(--accent); + border-color: var(--accent-soft); + font-weight: 600; +} + +.header-actions { + display: flex; + gap: 6px; + align-items: center; + flex: 0 0 auto; +} + +.icon-btn { + background: transparent; + border: 1px solid transparent; + border-radius: var(--radius-sm); + color: var(--text-dim); + width: 32px; + height: 32px; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background var(--dur), color var(--dur); +} +.icon-btn:hover { background: var(--bg-3); color: var(--text); } +.icon-btn .icon { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.7; } + +html[data-theme="dark"] .icon-sun { display: none; } +html[data-theme="light"] .icon-moon { display: none; } + +/* ------------------------------ MAIN ------------------------------ */ +.main { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.view { + flex: 1 1 auto; + min-height: 0; + display: flex; + flex-direction: column; + padding: 10px; + gap: 10px; +} +.view[hidden] { display: none; } + +/* ------------------------------ TOOLBAR --------------------------- */ +.toolbar { + flex: 0 0 auto; + display: flex; + align-items: center; + gap: 6px; + flex-wrap: wrap; + padding: 6px 8px; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); +} + +.btn { + background: var(--bg-3); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + color: var(--text); + padding: 6px 12px; + cursor: pointer; + font-weight: 500; + white-space: nowrap; + transition: background var(--dur), border-color var(--dur), transform var(--dur); +} +.btn:hover { background: var(--bg-elev); border-color: var(--accent); } +.btn:active { transform: translateY(1px); } +.btn:disabled { opacity: 0.5; cursor: not-allowed; } + +.btn-primary { + background: var(--accent); + border-color: var(--accent); + color: #fff; +} +.btn-primary:hover { background: var(--accent-hover); border-color: var(--accent-hover); } + +.btn-ghost { background: transparent; } +.btn-ghost:hover { background: var(--bg-3); } + +.btn-danger { background: transparent; border-color: var(--red); color: var(--red); } +.btn-danger:hover { background: var(--red-soft); border-color: var(--red); } + +.btn-sm { padding: 4px 9px; font-size: 12.5px; } + +.sep { + width: 1px; + height: 20px; + background: var(--border); + margin: 0 4px; + flex: 0 0 auto; +} + +.panel-meta { + font-size: 12px; + color: var(--text-faint); + font-family: var(--font-mono); + white-space: nowrap; +} +.muted { color: var(--text-dim); } + +/* ------------------------------ INPUTS ---------------------------- */ +.input { + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 6px 9px; + color: var(--text); + min-width: 0; + transition: border-color var(--dur); +} +.input:focus { border-color: var(--accent); outline: none; box-shadow: 0 0 0 3px var(--accent-soft); } +.input.mono { font-family: var(--font-mono); } + +select.input { cursor: pointer; } + +.search-wrap { + display: flex; + align-items: center; + gap: 6px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 2px 8px; + min-width: 240px; + flex: 0 1 340px; +} +.search-wrap:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px var(--accent-soft); } +.search-input { + background: transparent; + border: none; + outline: none; + padding: 5px 0; + flex: 1 1 auto; + min-width: 0; +} +.search-input:focus { box-shadow: none; } +.search-icon { width: 15px; height: 15px; fill: none; stroke: var(--text-faint); stroke-width: 1.8; flex: 0 0 auto; } + +/* ------------------------------ PANELS ---------------------------- */ +.split-pane { + flex: 1 1 auto; + min-height: 0; + display: flex; + gap: 0; + align-items: stretch; + --split: 50%; +} + +.split-pane > .panel:first-child { flex: 0 0 var(--split); } +.split-pane > .panel:last-child { flex: 1 1 0; } + +#req-split { --split: 38%; } + +.panel { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + display: flex; + flex-direction: column; + min-width: 0; + min-height: 0; + flex: 1 1 0; + overflow: hidden; +} + +.panel-head { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + padding: 7px 11px; + background: var(--panel-head); + border-bottom: 1px solid var(--border); + min-height: 38px; +} + +.panel-title { + margin: 0; + font-size: 12.5px; + font-weight: 650; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--text-dim); +} + +.panel-body { + flex: 1 1 auto; + min-height: 0; + overflow: auto; + padding: 10px 12px; +} +.panel-body.no-pad { padding: 0; overflow: hidden; } +.panel-body.scroll { overflow: auto; } + +/* ------------------------------ CODE AREA -------------------------- */ +.code-area { + width: 100%; + height: 100%; + resize: none; + border: none; + outline: none; + background: transparent; + color: var(--text); + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.65; + padding: 12px 14px; + tab-size: 2; + white-space: pre; + overflow: auto; +} +.code-area::placeholder { color: var(--text-faint); } +.code-area:focus { background: color-mix(in srgb, var(--accent) 3%, transparent); } + +.code-pre { + margin: 0; + display: block; + overflow: auto; + white-space: pre; +} + +.code-body { + height: 170px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg); + display: block; +} + +/* ------------------------------ SPLITTER -------------------------- */ +.splitter { + flex: 0 0 6px; + cursor: col-resize; + background: transparent; + position: relative; + border-radius: 3px; + transition: background var(--dur); +} +.splitter::after { + content: ""; + position: absolute; + inset: 0 2px; + background: var(--border); + transition: background var(--dur); +} +.splitter:hover::after, .splitter:focus-visible::after, .splitter.dragging::after { + background: var(--accent); +} +body.resizing { cursor: col-resize; user-select: none; } +body.resizing * { cursor: col-resize !important; } + +/* ------------------------------ ERROR BOX ------------------------- */ +.error-box { + flex: 0 0 auto; + max-height: 40%; + overflow: auto; + margin: 0; + padding: 9px 12px; + background: var(--red-soft); + border-top: 1px solid var(--red); + color: var(--red); + font-family: var(--font-mono); + font-size: 12.5px; + white-space: pre-wrap; + word-break: break-word; +} +.error-box[hidden] { display: none; } +.error-box .err-title { font-weight: 700; font-family: var(--font-ui); display: block; margin-bottom: 3px; } +.error-box .err-pos { color: var(--text-dim); } + +/* ------------------------------ SYNTAX HIGHLIGHT ------------------ */ +.tok-key { color: var(--accent); } +.tok-str { color: var(--green); } +.tok-num { color: var(--purple); } +.tok-bool { color: var(--orange); } +.tok-null { color: var(--orange); font-style: italic; } +.tok-punc { color: var(--text-faint); } +html[data-theme="light"] .tok-bool { color: var(--orange); } + +mark.hl { + background: var(--orange-soft); + color: var(--orange); + border-radius: 2px; + padding: 0 1px; +} + +/* ------------------------------ TREE ------------------------------ */ +.tree { + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.6; +} + +.tnode-row { + display: flex; + align-items: flex-start; + gap: 3px; + padding: 1px 4px 1px 0; + border-radius: 3px; + white-space: nowrap; +} +.tnode-row:hover { background: var(--bg-3); } +.tnode-row.focused { outline: 1px solid var(--accent); outline-offset: -1px; } + +.twisty { + background: transparent; + border: none; + color: var(--text-faint); + cursor: pointer; + width: 16px; + min-width: 16px; + height: 20px; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 10px; + transition: transform var(--dur); +} +.twisty.open { transform: rotate(90deg); } +.twisty:hover { color: var(--accent); } +.twisty.leaf { visibility: hidden; } + +.tkey { color: var(--accent); } +.tsep { color: var(--text-faint); } +.tval.str { color: var(--green); } +.tval.num { color: var(--purple); } +.tval.bool { color: var(--orange); } +.tval.null { color: var(--orange); font-style: italic; } +.tcount { + color: var(--text-faint); + font-size: 11.5px; + background: var(--bg-3); + border-radius: 20px; + padding: 0 6px; + margin-left: 2px; +} +.ttype { + color: var(--text-faint); + font-size: 11px; + font-style: italic; +} +.tchildren { margin-left: 15px; border-left: 1px dashed var(--border); padding-left: 7px; } +.tchildren[hidden] { display: none; } + +.tact { + background: transparent; + border: 1px solid var(--border); + border-radius: 3px; + color: var(--text-faint); + font-size: 10.5px; + padding: 0 4px; + cursor: pointer; + margin-left: 4px; + opacity: 0; + transition: opacity var(--dur), color var(--dur), border-color var(--dur); +} +.tnode-row:hover .tact, .tact:focus-visible { opacity: 1; } +.tact:hover { color: var(--accent); border-color: var(--accent); } +.tact.copied { opacity: 1; color: var(--green); border-color: var(--green); } + +.tree-empty { color: var(--text-faint); font-style: italic; padding: 10px; } + +/* ------------------------------ DIFF ------------------------------ */ +.results { + flex: 0 0 auto; + max-height: 45%; + min-height: 0; + display: flex; + flex-direction: column; + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; +} + +.legend { display: flex; gap: 6px; } + +.diff-row { + display: grid; + grid-template-columns: 86px minmax(0, 1fr); + gap: 10px; + padding: 7px 9px; + border-bottom: 1px solid var(--border); + font-family: var(--font-mono); + font-size: 12.5px; + align-items: start; +} +.diff-row:last-child { border-bottom: none; } +.diff-row:hover { background: var(--bg-3); } + +.diff-badge { + font-family: var(--font-ui); + font-size: 10.5px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.03em; + border-radius: 4px; + padding: 2px 6px; + text-align: center; +} +.badge-added { background: var(--green-soft); color: var(--green); } +.badge-removed { background: var(--red-soft); color: var(--red); } +.badge-changed { background: var(--orange-soft); color: var(--orange); } + +.diff-path { + color: var(--accent); + cursor: pointer; + word-break: break-all; + margin-bottom: 3px; +} +.diff-path:hover { text-decoration: underline; } + +.diff-val { + display: block; + white-space: pre-wrap; + word-break: break-word; + border-radius: 3px; + padding: 1px 5px; + margin-top: 2px; +} +.diff-old { background: var(--red-soft); color: var(--red); } +.diff-new { background: var(--green-soft); color: var(--green); } +.diff-typechange { color: var(--orange); font-style: italic; } + +.diff-arrow { color: var(--text-faint); margin: 0 4px; } + +/* ------------------------------ REQUEST --------------------------- */ +.horizontal-request { flex-direction: row; } +.req-form { max-width: 760px; } +.req-response { flex: 1 1 0; } + +.field { margin-bottom: 12px; } +.field > label { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text-dim); + margin-bottom: 4px; + text-transform: uppercase; + letter-spacing: 0.03em; +} +.row { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; } +.row > .input { flex: 1 1 auto; } +.hint { font-size: 11.5px; color: var(--text-faint); margin: 4px 0 0; } + +.group { + border: 1px solid var(--border); + border-radius: var(--radius-sm); + margin-bottom: 12px; + background: var(--bg-2); +} +.group > summary { + cursor: pointer; + padding: 7px 10px; + font-weight: 600; + font-size: 12.5px; + color: var(--text-dim); + text-transform: uppercase; + letter-spacing: 0.03em; + list-style: none; + user-select: none; +} +.group > summary::-webkit-details-marker { display: none; } +.group > summary::before { content: "\25B8"; margin-right: 7px; color: var(--accent); display: inline-block; } +.group[open] > summary::before { content: "\25BE"; } +.group > summary:hover { color: var(--text); } +.group > *:not(summary) { margin: 0 10px 10px; } +.group > .row:last-child { margin-bottom: 10px; } + +.kv-list { display: flex; flex-direction: column; gap: 5px; margin-top: 6px; } +.kv-row { display: flex; gap: 5px; align-items: center; } +.kv-row .input { flex: 1 1 0; padding: 4px 7px; font-size: 12.5px; } +.kv-row .input.k { flex: 1 1 35%; } +.kv-row .input.v { flex: 1 1 60%; font-family: var(--font-mono); } +.kv-del { + background: transparent; + border: 1px solid var(--border); + border-radius: 4px; + color: var(--text-faint); + cursor: pointer; + width: 26px; + height: 28px; + flex: 0 0 auto; + font-size: 15px; + line-height: 1; +} +.kv-del:hover { color: var(--red); border-color: var(--red); background: var(--red-soft); } + +.plain-list { margin: 0; padding-left: 18px; color: var(--text-dim); font-size: 12.5px; } +.plain-list li { margin-bottom: 4px; } +code { + font-family: var(--font-mono); + background: var(--bg-3); + padding: 1px 4px; + border-radius: 3px; + font-size: 12px; +} + +/* response */ +.resp-status-bar { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + padding: 9px 11px; + border-radius: var(--radius-sm); + background: var(--bg-3); + margin-bottom: 10px; +} +.resp-code { + font-family: var(--font-mono); + font-weight: 700; + padding: 3px 8px; + border-radius: 4px; +} +.resp-code.ok { background: var(--green-soft); color: var(--green); } +.resp-code.redirect { background: var(--accent-soft); color: var(--accent); } +.resp-code.client { background: var(--orange-soft); color: var(--orange); } +.resp-code.server { background: var(--red-soft); color: var(--red); } +.resp-code.err { background: var(--red-soft); color: var(--red); } +.resp-stat { color: var(--text-dim); font-size: 12.5px; font-family: var(--font-mono); } + +.resp-tabs { display: flex; gap: 4px; margin-bottom: 8px; border-bottom: 1px solid var(--border); } +.resp-tab { + background: transparent; + border: none; + border-bottom: 2px solid transparent; + color: var(--text-dim); + padding: 5px 10px; + cursor: pointer; + font-size: 12.5px; +} +.resp-tab[aria-selected="true"] { color: var(--accent); border-bottom-color: var(--accent); font-weight: 600; } + +.resp-headers { font-family: var(--font-mono); font-size: 12.5px; } +.resp-headers tr:nth-child(odd) { background: var(--bg-3); } +.resp-headers td { padding: 4px 8px; border-bottom: 1px solid var(--border); vertical-align: top; } +.resp-headers td:first-child { color: var(--accent); white-space: nowrap; width: 1%; } + +.cors-box { + border: 1px solid var(--orange); + background: var(--orange-soft); + border-radius: var(--radius-sm); + padding: 12px 14px; + margin-bottom: 12px; +} +.cors-box h3 { margin: 0 0 6px; font-size: 13.5px; color: var(--orange); } +.cors-box ul { margin: 6px 0 0; padding-left: 20px; color: var(--text-dim); font-size: 12.5px; } +.cors-box li { margin-bottom: 4px; } +.cors-box code { background: rgba(0,0,0,0.15); } + +.err-box-generic { + border: 1px solid var(--red); + background: var(--red-soft); + border-radius: var(--radius-sm); + padding: 11px 13px; + color: var(--red); +} +.err-box-generic strong { display: block; margin-bottom: 4px; } + +/* ------------------------------ HISTORY --------------------------- */ +.seg { + display: inline-flex; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + overflow: hidden; +} +.seg-btn { + background: transparent; + border: none; + color: var(--text-dim); + padding: 6px 14px; + cursor: pointer; + font-weight: 500; +} +.seg-btn.is-active { background: var(--accent); color: #fff; font-weight: 600; } + +.hist-list { display: flex; flex-direction: column; gap: 8px; } +.hist-item { + border: 1px solid var(--border); + border-radius: var(--radius-sm); + background: var(--bg-2); + padding: 9px 11px; +} +.hist-item-head { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 4px; +} +.hist-title { + font-family: var(--font-mono); + font-weight: 600; + font-size: 12.5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + flex: 1 1 auto; +} +.hist-time { font-size: 11.5px; color: var(--text-faint); white-space: nowrap; } +.hist-preview { + font-family: var(--font-mono); + font-size: 11.5px; + color: var(--text-dim); + background: var(--bg); + border-radius: 4px; + padding: 5px 7px; + max-height: 92px; + overflow: auto; + white-space: pre; + margin: 4px 0 6px; +} +.hist-actions { display: flex; gap: 5px; flex-wrap: wrap; } +.hist-method { + font-family: var(--font-mono); + font-weight: 700; + font-size: 11px; + padding: 2px 6px; + border-radius: 4px; + background: var(--accent-soft); + color: var(--accent); +} +.hist-method.GET { background: var(--green-soft); color: var(--green); } +.hist-method.POST { background: var(--accent-soft); color: var(--accent); } +.hist-method.PUT, .hist-method.PATCH { background: var(--orange-soft); color: var(--orange); } +.hist-method.DELETE { background: var(--red-soft); color: var(--red); } + +/* ------------------------------ TOASTS ---------------------------- */ +.toasts { + position: fixed; + right: 16px; + bottom: 16px; + z-index: 120; + display: flex; + flex-direction: column; + gap: 8px; + max-width: min(380px, calc(100vw - 32px)); +} +.toast { + background: var(--bg-elev); + border: 1px solid var(--border); + border-left: 3px solid var(--accent); + border-radius: var(--radius-sm); + box-shadow: var(--shadow); + padding: 9px 12px; + font-size: 13px; + display: flex; + align-items: flex-start; + gap: 8px; + animation: toast-in 180ms ease-out; +} +.toast.success { border-left-color: var(--green); } +.toast.error { border-left-color: var(--red); } +.toast.warn { border-left-color: var(--orange); } +.toast.leaving { animation: toast-out 160ms ease-in forwards; } +.toast-msg { flex: 1 1 auto; word-break: break-word; } +.toast-msg b { display: block; font-weight: 650; } +.toast-msg span { color: var(--text-dim); font-size: 12px; } +.toast-close { + background: transparent; + border: none; + color: var(--text-faint); + cursor: pointer; + font-size: 16px; + line-height: 1; + padding: 0 2px; +} +.toast-close:hover { color: var(--text); } + +@keyframes toast-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } } +@keyframes toast-out { to { opacity: 0; transform: translateX(20px); } } + +/* ------------------------------ MODAL ----------------------------- */ +.modal-backdrop { + position: fixed; + inset: 0; + background: rgba(4, 8, 14, 0.6); + backdrop-filter: blur(2px); + z-index: 150; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} +.modal-backdrop[hidden] { display: none; } +.modal { + background: var(--panel); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + width: min(560px, 100%); + max-height: 85vh; + display: flex; + flex-direction: column; + overflow: hidden; +} +.modal-body { padding: 14px 16px; overflow: auto; } +.shortcuts { + display: grid; + grid-template-columns: auto 1fr; + gap: 7px 16px; + margin: 0; + align-items: center; +} +.shortcuts dt { text-align: right; } +.shortcuts dd { margin: 0; color: var(--text-dim); } +kbd { + font-family: var(--font-mono); + font-size: 11.5px; + background: var(--bg-3); + border: 1px solid var(--border); + border-bottom-width: 2px; + border-radius: 4px; + padding: 1px 5px; + color: var(--text); +} + +/* ------------------------------ DRAG OVERLAY ---------------------- */ +.drag-overlay { + position: fixed; + inset: 0; + background: rgba(4, 8, 14, 0.72); + z-index: 140; + display: flex; + align-items: center; + justify-content: center; +} +.drag-overlay[hidden] { display: none; } +.drag-card { + border: 2px dashed var(--accent); + color: var(--accent); + border-radius: var(--radius); + padding: 28px 40px; + font-size: 16px; + font-weight: 600; + background: var(--panel); +} + +/* ------------------------------ SCROLLBARS ------------------------ */ +*::-webkit-scrollbar { width: 11px; height: 11px; } +*::-webkit-scrollbar-track { background: transparent; } +*::-webkit-scrollbar-thumb { + background: var(--border); + border-radius: 6px; + border: 3px solid transparent; + background-clip: content-box; +} +*::-webkit-scrollbar-thumb:hover { background: var(--text-faint); background-clip: content-box; } +* { scrollbar-width: thin; scrollbar-color: var(--border) transparent; } + +/* ------------------------------ RESPONSIVE ------------------------ */ +@media (max-width: 1000px) { + .req-form { flex: 0 0 380px; } +} + +@media (max-width: 820px) { + body { overflow: auto; } + .app-header { + height: auto; + flex-wrap: wrap; + padding: 8px 10px; + gap: 8px; + } + .tabs { order: 3; flex-basis: 100%; } + .brand-name { display: none; } + .split-pane { flex-direction: column; } + .split-pane > .panel:first-child, + .split-pane > .panel:last-child { + flex: 0 0 auto; + width: auto; + min-height: 240px; + height: 38vh; + max-height: none; + } + .splitter { flex: 0 0 8px; cursor: row-resize; } + .splitter::after { inset: 2px 0; } + .results { max-height: none; height: 45vh; } + .req-form { flex: 0 0 auto; max-width: none; } + .horizontal-request { flex-direction: column; } + .view { padding: 8px; gap: 8px; } + .search-wrap { flex: 1 1 100%; } +} + +@media (max-width: 520px) { + .diff-row { grid-template-columns: 1fr; gap: 4px; } + .diff-badge { justify-self: start; } + .toolbar { gap: 4px; } + .btn { padding: 6px 9px; font-size: 12.5px; } +} + +@media (prefers-reduced-motion: reduce) { + * { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } +} diff --git a/tests/logic.test.js b/tests/logic.test.js new file mode 100644 index 0000000..52e366b --- /dev/null +++ b/tests/logic.test.js @@ -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>&"''); +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('').includes('<script>')); +ok('no raw script tag', !D.Highlighter.highlight('').includes('')); +ok('highlightMatch wraps', D.Highlighter.highlightMatch('abcdef', 'cd').includes('cd')); +ok('highlightMatch case-insensitive', D.Highlighter.highlightMatch('ABCDEF', 'cd').includes('CD')); +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(/]*\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);