Files
hermes-webui/tests/test_issue484_json_tree_viewer.py
T
allenliang2022 fbb21964a9 test(chat): update 6 post-render tests orphaned by the _postProcessWithAnchorSuppression refactor (#5338)
Commit 7536f6f1 routed the deferred post-render dispatches through
_postProcessWithAnchorSuppression() (holds overflow-anchor suppression across
the async media/layout settle frame, then calls postProcessRenderedMessages).
Six pre-existing tests string-matched the old
'requestAnimationFrame(()=>postProcessRenderedMessages(inner))' literal and
failed on the rename — behavior is preserved (the wrapper still invokes
postProcessRenderedMessages), so this is a test-fix not a code-fix.

Per the gate-cert recommendation, the tests now assert the BEHAVIOR chain
(post-render is scheduled via _postProcessWithAnchorSuppression, and that
wrapper calls postProcessRenderedMessages) rather than the exact rAF literal, so
a future wrapper rename can't re-orphan them.

Files: test_csv_table_rendering, test_excalidraw_inline_embed,
test_issue483_inline_diff_viewer, test_issue484_json_tree_viewer, test_issue347,
test_pdf_html_preview. Verified: the 6 updated assertions pass locally (the only
local failures are the pre-existing Windows-only WinError 206 command-line-too-long
in Node-harness tests, unrelated, green on Linux CI).
2026-07-01 17:46:03 +00:00

298 lines
13 KiB
Python

"""Tests for issue #484 — collapsible JSON/YAML tree viewer.
The original feature hardcoded the default view: valid JSON/YAML fenced blocks
opened in Tree view at 10+ lines (`const showTree=lineCount>=10;`). That default
is now user-configurable (#484 follow-up) via two settings:
- structured_code_default_view: "auto" | "on" | "off"
on => always default to Tree
off => always default to Raw
auto => Tree only when the block line count >= the configured threshold
- structured_code_auto_tree_lines: integer 1..1000 (default 10)
`auto` + threshold 10 reproduces the original behavior, so the default is
preserved. These tests pin the new configurable shape while keeping the existing
Tree/Raw renderer invariants (wrapper class, helpers, value types, toggle, YAML
support) intact.
"""
import shutil
import subprocess
from pathlib import Path
import pytest
REPO_ROOT = Path(__file__).parent.parent.resolve()
UI_JS_PATH = REPO_ROOT / "static" / "ui.js"
NODE = shutil.which("node")
class TestTreeRenderer:
"""Fenced JSON/YAML blocks should get a tree view toggle."""
def test_json_blocks_get_tree_wrapper(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "code-tree-wrap" in content
assert "data-raw" in content
assert "data-lang" in content
def test_json_yaml_lang_detection(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "lang==='json'||lang==='yaml'" in content
def test_initTreeViews_function_exists(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "function initTreeViews" in content
def test_buildTreeDOM_function_exists(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "function _buildTreeDOM(val, depth)" in content
def test_initTreeViews_called_in_post_render(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
# Behavior assertion (#5338): post-render scheduled via
# _postProcessWithAnchorSuppression, which still calls postProcessRenderedMessages.
assert "requestAnimationFrame(()=>_postProcessWithAnchorSuppression(" in content
_wrap = content.find('function _postProcessWithAnchorSuppression')
assert _wrap != -1, "post-render should be wrapped by _postProcessWithAnchorSuppression"
assert 'postProcessRenderedMessages(container)' in content[_wrap:_wrap + 500], \
"the wrapper must still invoke postProcessRenderedMessages"
start = content.find("function postProcessRenderedMessages")
body = content[start:start + 500]
assert "initTreeViews(container)" in body
def test_tree_handles_all_value_types(self):
"""_buildTreeDOM should handle null, boolean, number, string, array, object."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
for cls in ("tree-null", "tree-bool", "tree-num", "tree-str", "tree-array", "tree-object"):
assert cls in content, f"Missing type class: {cls}"
def test_tree_collapse_support(self):
"""Tree nodes should be collapsible with collapsed/expanded states."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "tree-collapsed" in content
assert "tree-collapsible" in content
assert "classList.toggle" in content
def test_tree_depth_auto_collapse(self):
"""Nested levels beyond depth 2 should be collapsed by default."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "depth>=2" in content
def test_toggle_button_uses_i18n(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "t('raw_view')" in content
assert "t('tree_view')" in content
def test_yaml_support_via_jsyaml(self):
"""YAML should be parsed via jsyaml if available."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "jsyaml" in content
class TestConfigurableDefaultView:
"""The default Tree/Raw decision is configurable, not hardcoded (#484 follow-up)."""
def test_hardcoded_threshold_is_gone(self):
"""The original `lineCount>=10` hardcode must no longer drive the default."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "const showTree=lineCount>=10;" not in content, (
"The hardcoded `const showTree=lineCount>=10;` must be replaced by the "
"configurable decision helper."
)
assert "lineCount>=10" not in content, (
"No hardcoded `lineCount>=10` comparison should remain; the threshold "
"is configurable and the default is read from a sanitized helper."
)
def test_decision_helper_exists(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "function _structuredCodeShowTree(mode,threshold,lineCount)" in content
assert "function _structuredCodeMode(" in content
assert "function _structuredCodeThreshold(" in content
def test_all_three_modes_represented(self):
"""auto / on / off must all be handled in the renderer."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
start = content.find("function _structuredCodeShowTree")
body = content[start:start + 400]
assert "'on'" in body, "mode 'on' (always Tree) must be handled"
assert "'off'" in body, "mode 'off' (always Raw) must be handled"
# 'auto' is the fallthrough; the mode reader sanitizes to it.
mode_start = content.find("function _structuredCodeMode")
mode_body = content[mode_start:mode_start + 300]
assert "'auto'" in mode_body, "mode 'auto' must be the sanitized fallback"
def test_default_threshold_fallback_is_10(self):
"""An invalid/missing threshold must fall back to 10 (original behavior)."""
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
thr_start = content.find("function _structuredCodeThreshold")
thr_body = content[thr_start:thr_start + 300]
assert ":10" in thr_body or "?10" in thr_body or " 10" in thr_body, (
"_structuredCodeThreshold must fall back to 10 for invalid/missing values"
)
# Sanitization bounds (1..1000) keep the value safe.
assert "1000" in thr_body, "threshold should be clamped to an upper bound"
def test_renderer_uses_helper_for_decision(self):
with open("static/ui.js", "r", encoding="utf-8") as f:
content = f.read()
assert "_structuredCodeShowTree(_structuredCodeMode(),_structuredCodeThreshold(),lineCount)" in content
@pytest.mark.skipif(NODE is None, reason="node not on PATH")
def test_decision_helper_behaviour_via_node(self, tmp_path):
"""Evaluate the real _structuredCodeShowTree() against the prompt's cases.
- off, lineCount 999 => False
- on, lineCount 1 => True
- auto, threshold 10, count 9 => False
- auto, threshold 10, count 10 => True
- auto, invalid threshold => fallback 10 (count 10 => True, 9 => False)
"""
src = UI_JS_PATH.read_text(encoding="utf-8")
# Extract the brace-balanced body of _structuredCodeShowTree.
marker = "function _structuredCodeShowTree("
start = src.find(marker)
assert start >= 0, "_structuredCodeShowTree not found"
i = src.index("{", start)
depth = 1
i += 1
while depth > 0 and i < len(src):
if src[i] == "{":
depth += 1
elif src[i] == "}":
depth -= 1
i += 1
fn_src = src[start:i]
driver = tmp_path / "driver.js"
driver.write_text(
fn_src
+ "\n"
+ r"""
const cases = [
['off', 5, 999, false],
['on', 5, 1, true],
['auto', 10, 9, false],
['auto', 10, 10, true],
['auto', 11, 10, false],
// invalid threshold falls back to 10
['auto', NaN, 10, true],
['auto', NaN, 9, false],
// out-of-range thresholds fall back to 10 inside the pure helper
['auto', 0, 10, true],
['auto', 0, 9, false],
['auto', 99999, 10, true],
['auto', 99999, 9, false],
// unknown mode is treated as auto by the pure helper's fallthrough
['bogus', 10, 10, true],
['bogus', 10, 9, false],
];
let failures = [];
for (const [mode, thr, count, want] of cases) {
const got = _structuredCodeShowTree(mode, thr, count);
if (got !== want) failures.push(`${mode},${thr},${count} => ${got} (want ${want})`);
}
if (failures.length) { console.error('FAIL: ' + failures.join(' | ')); process.exit(1); }
console.log('OK');
""",
encoding="utf-8",
)
result = subprocess.run(
[NODE, str(driver)], capture_output=True, text=True, timeout=30
)
assert result.returncode == 0, (
f"_structuredCodeShowTree behaviour mismatch: {result.stdout}{result.stderr}"
)
class TestTreeCSS:
"""CSS classes for tree viewer."""
def test_tree_css_classes_exist(self):
with open("static/style.css", "r", encoding="utf-8") as f:
content = f.read()
for cls in (".code-tree-wrap", ".tree-view", ".tree-hidden", ".tree-toggle-btn",
".tree-node", ".tree-collapsible", ".tree-children", ".tree-collapsed",
".tree-key", ".tree-str", ".tree-num", ".tree-bool", ".tree-null",
".tree-comma", ".tree-item"):
assert cls in content, f"Missing CSS: {cls}"
def test_tree_colors_match_types(self):
with open("static/style.css", "r", encoding="utf-8") as f:
content = f.read()
# Green strings, blue numbers, amber booleans
assert "#4ade80" in content # tree-str green
assert "#60a5fa" in content # tree-key/tree-num blue
assert "#fbbf24" in content # tree-bool amber
class TestTreeI18n:
def test_i18n_keys_present(self):
with open("static/i18n.js", "r", encoding="utf-8") as f:
content = f.read()
for key in ("tree_view", "raw_view"):
count = content.count(key)
assert count >= 7, f"{key} found {count} times, expected >= 7"
def test_structured_code_setting_i18n_keys_present(self):
"""The new settings labels/options/help text must exist in i18n."""
with open("static/i18n.js", "r", encoding="utf-8") as f:
content = f.read()
for key in (
"settings_label_structured_code",
"settings_option_structured_code_auto",
"settings_option_structured_code_on",
"settings_option_structured_code_off",
"settings_label_structured_code_auto_lines",
"settings_desc_structured_code",
):
assert f"{key}:" in content, f"Missing i18n key: {key}"
class TestStructuredCodeSettingsWiring:
"""The setting must be a real, persisted WebUI setting — not a localStorage hack."""
def test_server_defaults_and_validation(self):
with open("api/config.py", "r", encoding="utf-8") as f:
content = f.read()
# Defaults preserve current behavior: auto + threshold 10.
assert '"structured_code_default_view": "auto"' in content
assert '"structured_code_auto_tree_lines": 10' in content
# Enum + range validation are registered.
assert '"structured_code_default_view": {"auto", "on", "off"}' in content
assert '"structured_code_auto_tree_lines": (1, 1000)' in content
def test_settings_ui_controls_present(self):
with open("static/index.html", "r", encoding="utf-8") as f:
content = f.read()
assert 'id="settingsStructuredCodeMode"' in content
assert 'id="settingsStructuredCodeAutoLines"' in content
def test_boot_initializes_runtime_globals(self):
with open("static/boot.js", "r", encoding="utf-8") as f:
content = f.read()
assert "window._structuredCodeDefaultView" in content
assert "window._structuredCodeAutoTreeLines" in content
def test_panel_persists_setting(self):
with open("static/panels.js", "r", encoding="utf-8") as f:
content = f.read()
assert "_structuredCodeViewFromUi" in content
assert "structured_code_default_view" in content
assert "structured_code_auto_tree_lines" in content