mirror of
https://github.com/MervinPraison/PraisonAIUI.git
synced 2026-11-07 05:56:17 -10:00
fix: specify UTF-8 encoding for frontend source reads in tests (fixes #232)
Standardize all Path.read_text() calls in the test suite to pass encoding="utf-8" so committed UTF-8 frontend JS/HTML bundles decode correctly on Windows (cp1252 default locale) instead of raising UnicodeDecodeError. Co-authored-by: Mervin Praison <[email protected]>
This commit is contained in:
1 parent
01a31f7c8b
commit
78a8a499c1
20 files changed
+95
-95
No files matched your search
@@ -94,7 +94,7 @@ class TestBuildCommand:
|
||||
["build", "--config", str(project_dir / "aiui.template.yaml"), "--output", str(output)],
|
||||
)
|
||||
|
||||
ui_config = json.loads((output / "ui-config.json").read_text())
|
||||
ui_config = json.loads((output / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["site"]["title"] == "Integration Test Site"
|
||||
assert "header_main" in ui_config["components"]
|
||||
assert "footer_main" in ui_config["components"]
|
||||
@@ -111,7 +111,7 @@ class TestBuildCommand:
|
||||
],
|
||||
)
|
||||
|
||||
content = (output / "ui-config.json").read_text()
|
||||
content = (output / "ui-config.json").read_text(encoding="utf-8")
|
||||
assert "\n" not in content # Minified = no newlines
|
||||
|
||||
def test_build_invalid_config(self, tmp_path: Path):
|
||||
@@ -215,8 +215,8 @@ class TestFullPipeline:
|
||||
assert (output / "index.html").exists()
|
||||
|
||||
# Verify content
|
||||
ui_config = json.loads((output / "ui-config.json").read_text())
|
||||
ui_config = json.loads((output / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["site"]["title"] == "Integration Test Site"
|
||||
|
||||
routes = json.loads((output / "route-manifest.json").read_text())
|
||||
routes = json.loads((output / "route-manifest.json").read_text(encoding="utf-8"))
|
||||
assert len(routes["routes"]) >= 1
|
||||
@@ -22,7 +22,7 @@ EXPLORER_VIEW = _FRONTEND / "views" / "explorer.js"
|
||||
|
||||
|
||||
def _view() -> str:
|
||||
return CHANNELS_VIEW.read_text()
|
||||
return CHANNELS_VIEW.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestAddChannelControls:
|
||||
@@ -91,6 +91,6 @@ class TestEmptyState:
|
||||
|
||||
class TestExplorerPreset:
|
||||
def test_explorer_has_add_channel_post(self):
|
||||
src = EXPLORER_VIEW.read_text()
|
||||
src = EXPLORER_VIEW.read_text(encoding="utf-8")
|
||||
assert "Add Channel" in src
|
||||
assert "env:TELEGRAM_BOT_TOKEN" in src
|
||||
@@ -25,13 +25,13 @@ CHAT_JS = FRONTEND / "views" / "chat.js"
|
||||
|
||||
class TestSelectPageShellError:
|
||||
def test_shell_error_helper_defined(self):
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
assert "function showDashboardInitError(" in src
|
||||
assert "'alert'" in src
|
||||
assert "db-error" in src
|
||||
|
||||
def test_select_page_calls_shell_error_on_missing_main(self):
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("const main = document.getElementById('db-main-content')")
|
||||
assert idx != -1
|
||||
region = src[idx:idx + 400]
|
||||
@@ -41,16 +41,16 @@ class TestSelectPageShellError:
|
||||
|
||||
class TestChatOfflineMessaging:
|
||||
def test_toast_imported(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "import { showToast } from '../toast.js';" in src
|
||||
|
||||
def test_offline_hint_in_fallback(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "navigator.onLine === false" in src
|
||||
assert "You are offline. " in src
|
||||
|
||||
def test_fallback_catch_shows_toast(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("fetch('/api/chat/send'")
|
||||
assert idx != -1
|
||||
region = src[idx:idx + 900]
|
||||
@@ -62,7 +62,7 @@ class TestChatOfflineMessaging:
|
||||
|
||||
class TestSystemErrorRoleAlert:
|
||||
def test_append_message_sets_role_alert(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("function appendMessage(")
|
||||
assert idx != -1
|
||||
region = src[idx:idx + 700]
|
||||
@@ -72,7 +72,7 @@ class TestSystemErrorRoleAlert:
|
||||
|
||||
class TestWebSocketSendGuard:
|
||||
def test_ws_send_wrapped_in_try_catch(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
idx = src.find("ws.send(JSON.stringify(payload))")
|
||||
assert idx != -1
|
||||
before = src[max(0, idx - 120):idx]
|
||||
|
||||
@@ -50,28 +50,28 @@ class TestCodeStudioFrontend:
|
||||
assert (VIEWS / "code-studio.js").is_file()
|
||||
|
||||
def test_view_exports_render(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "export async function render" in src
|
||||
assert "export function cleanup" in src
|
||||
|
||||
def test_view_calls_execute_api(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "/api/code/execute" in src
|
||||
assert "/api/code/languages" in src
|
||||
|
||||
def test_no_browser_eval(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "eval(" not in src
|
||||
|
||||
def test_registered_in_dashboard(self):
|
||||
src = (PLUGINS / "dashboard.js").read_text()
|
||||
src = (PLUGINS / "dashboard.js").read_text(encoding="utf-8")
|
||||
assert "'code-studio'" in src
|
||||
assert "code-studio.js" in src
|
||||
|
||||
def test_send_to_agent_event(self):
|
||||
src = (VIEWS / "code-studio.js").read_text()
|
||||
src = (VIEWS / "code-studio.js").read_text(encoding="utf-8")
|
||||
assert "aiui:prefill-composer" in src
|
||||
|
||||
def test_chat_has_prefill_listener(self):
|
||||
src = (VIEWS / "chat.js").read_text()
|
||||
src = (VIEWS / "chat.js").read_text(encoding="utf-8")
|
||||
assert "aiui:prefill-composer" in src
|
||||
@@ -22,7 +22,7 @@ DASHBOARD_JS = FRONTEND / "dashboard.js"
|
||||
|
||||
class TestCommandPalette:
|
||||
def _src(self) -> str:
|
||||
return DASHBOARD_JS.read_text()
|
||||
return DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
|
||||
def test_open_command_palette_exposed_globally(self):
|
||||
src = self._src()
|
||||
|
||||
+19
-19
@@ -86,7 +86,7 @@ class TestCompilerUiConfig:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["site"]["title"] == "Test Site"
|
||||
assert "templates" in ui_config
|
||||
assert "docs" in ui_config["templates"]
|
||||
@@ -96,7 +96,7 @@ class TestCompilerUiConfig:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert "header_main" in ui_config["components"]
|
||||
assert ui_config["components"]["header_main"]["props"]["logoText"] == "MySite"
|
||||
assert "footer_main" in ui_config["components"]
|
||||
@@ -107,7 +107,7 @@ class TestCompilerUiConfig:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8"))
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
assert docs_template["slots"]["header"]["ref"] == "header_main"
|
||||
assert docs_template["slots"]["footer"]["ref"] == "footer_main"
|
||||
@@ -123,7 +123,7 @@ class TestCompilerRouteManifest:
|
||||
assert "route-manifest.json" in result.files
|
||||
|
||||
manifest = json.loads(
|
||||
(tmp_path / "output" / "route-manifest.json").read_text()
|
||||
(tmp_path / "output" / "route-manifest.json").read_text(encoding="utf-8")
|
||||
)
|
||||
assert len(manifest["routes"]) == 1
|
||||
assert manifest["routes"][0]["pattern"] == "/docs/**"
|
||||
@@ -147,7 +147,7 @@ class TestCompilerRouteManifest:
|
||||
assert result.success is True
|
||||
|
||||
manifest = json.loads(
|
||||
(tmp_path / "output" / "route-manifest.json").read_text()
|
||||
(tmp_path / "output" / "route-manifest.json").read_text(encoding="utf-8")
|
||||
)
|
||||
# First route in list gets highest priority
|
||||
assert manifest["routes"][0]["priority"] > manifest["routes"][1]["priority"]
|
||||
@@ -200,7 +200,7 @@ class TestCompilerOutputFiles:
|
||||
compiler = Compiler(minimal_config, base_path=tmp_path)
|
||||
compiler.compile(tmp_path / "output", minify=True)
|
||||
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text()
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8")
|
||||
# Minified JSON should not have newlines
|
||||
assert "\n" not in content
|
||||
|
||||
@@ -208,7 +208,7 @@ class TestCompilerOutputFiles:
|
||||
compiler = Compiler(minimal_config, base_path=tmp_path)
|
||||
compiler.compile(tmp_path / "output", minify=False)
|
||||
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text()
|
||||
content = (tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8")
|
||||
# Non-minified JSON should have newlines
|
||||
assert "\n" in content
|
||||
|
||||
@@ -228,7 +228,7 @@ class TestCompilerDocsContent:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert "docs-nav.json" in result.files
|
||||
|
||||
nav = json.loads((tmp_path / "output" / "docs-nav.json").read_text())
|
||||
nav = json.loads((tmp_path / "output" / "docs-nav.json").read_text(encoding="utf-8"))
|
||||
assert "items" in nav
|
||||
|
||||
def test_no_docs_nav_without_content(self, minimal_config: Config, tmp_path: Path):
|
||||
@@ -291,7 +291,7 @@ class TestCompilerThemeCss:
|
||||
assert result.success is True
|
||||
theme_css = tmp_path / "output" / "assets" / "theme.css"
|
||||
assert theme_css.exists(), "assets/theme.css must be generated"
|
||||
content = theme_css.read_text()
|
||||
content = theme_css.read_text(encoding="utf-8")
|
||||
assert ":root {" in content
|
||||
assert "--radius:" in content
|
||||
|
||||
@@ -340,7 +340,7 @@ class TestInjectThemeCss:
|
||||
inject_theme_css(tmp_path, preset="zinc", dark_mode=True, radius="0.5rem")
|
||||
theme_file = tmp_path / "assets" / "theme.css"
|
||||
assert theme_file.exists()
|
||||
content = theme_file.read_text()
|
||||
content = theme_file.read_text(encoding="utf-8")
|
||||
assert ":root {" in content
|
||||
|
||||
def test_creates_assets_dir(self, tmp_path: Path):
|
||||
@@ -367,7 +367,7 @@ class TestThemePipelineIntegration:
|
||||
assert result.success is True
|
||||
assert "assets/theme.css" in result.files
|
||||
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text()
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text(encoding="utf-8")
|
||||
assert ":root {" in theme_css
|
||||
assert ".dark {" in theme_css
|
||||
assert "--radius: 1rem" in theme_css # xl = 1rem
|
||||
@@ -386,7 +386,7 @@ class TestThemePipelineIntegration:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text()
|
||||
theme_css = (tmp_path / "output" / "assets" / "theme.css").read_text(encoding="utf-8")
|
||||
assert ":root {" in theme_css
|
||||
assert ".dark" not in theme_css
|
||||
|
||||
@@ -405,7 +405,7 @@ class TestThemePipelineIntegration:
|
||||
assert result.success is True
|
||||
|
||||
ui_config = json.loads(
|
||||
(tmp_path / "output" / "ui-config.json").read_text()
|
||||
(tmp_path / "output" / "ui-config.json").read_text(encoding="utf-8")
|
||||
)
|
||||
theme = ui_config["site"]["theme"]
|
||||
assert theme["preset"] == "blue"
|
||||
@@ -426,7 +426,7 @@ class TestThemePipelineIntegration:
|
||||
|
||||
theme_css_path = tmp_path / "output" / "assets" / "theme.css"
|
||||
assert theme_css_path.exists(), "theme.css must always be generated"
|
||||
content = theme_css_path.read_text()
|
||||
content = theme_css_path.read_text(encoding="utf-8")
|
||||
assert ":root {" in content
|
||||
assert "--radius:" in content
|
||||
|
||||
@@ -444,7 +444,7 @@ class TestThemePipelineIntegration:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
index_html = (tmp_path / "output" / "index.html").read_text()
|
||||
index_html = (tmp_path / "output" / "index.html").read_text(encoding="utf-8")
|
||||
assert "classList.add('dark')" not in index_html
|
||||
|
||||
def test_dark_mode_antiflicker_has_dark_class(self, tmp_path: Path):
|
||||
@@ -461,7 +461,7 @@ class TestThemePipelineIntegration:
|
||||
result = compiler.compile(tmp_path / "output")
|
||||
assert result.success is True
|
||||
|
||||
index_html = (tmp_path / "output" / "index.html").read_text()
|
||||
index_html = (tmp_path / "output" / "index.html").read_text(encoding="utf-8")
|
||||
assert "classList.add('dark')" in index_html
|
||||
|
||||
|
||||
@@ -514,7 +514,7 @@ class TestCompositionResolver:
|
||||
|
||||
# Load ui-config.json and verify auto-bridging
|
||||
ui_config_path = tmp_path / "output" / "ui-config.json"
|
||||
ui_config = json.loads(ui_config_path.read_text())
|
||||
ui_config = json.loads(ui_config_path.read_text(encoding="utf-8"))
|
||||
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
assert "zones" in docs_template
|
||||
@@ -556,7 +556,7 @@ class TestCompositionResolver:
|
||||
|
||||
# Load ui-config.json and verify no auto-bridging occurred
|
||||
ui_config_path = tmp_path / "output" / "ui-config.json"
|
||||
ui_config = json.loads(ui_config_path.read_text())
|
||||
ui_config = json.loads(ui_config_path.read_text(encoding="utf-8"))
|
||||
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
# Should not have zones for non-FlexibleLayout
|
||||
@@ -589,7 +589,7 @@ class TestCompositionResolver:
|
||||
|
||||
# Load ui-config.json and verify no duplicate components
|
||||
ui_config_path = tmp_path / "output" / "ui-config.json"
|
||||
ui_config = json.loads(ui_config_path.read_text())
|
||||
ui_config = json.loads(ui_config_path.read_text(encoding="utf-8"))
|
||||
|
||||
docs_template = ui_config["templates"]["docs"]
|
||||
zones = docs_template["zones"]
|
||||
|
||||
@@ -30,7 +30,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["style"] == "chat"
|
||||
|
||||
def test_layout_in_ui_config(self, tmp_path: Path):
|
||||
@@ -43,7 +43,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["layout"]["mode"] == "bottom-right"
|
||||
assert ui_config["layout"]["width"] == "400px"
|
||||
assert ui_config["layout"]["height"] == "600px"
|
||||
@@ -73,7 +73,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["chat"]["enabled"] is True
|
||||
assert ui_config["chat"]["name"] == "AI Assistant"
|
||||
assert len(ui_config["chat"]["starters"]) == 1
|
||||
@@ -96,7 +96,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert ui_config["auth"]["enabled"] is True
|
||||
assert "password" in ui_config["auth"]["providers"]
|
||||
assert "google" in ui_config["auth"]["providers"]
|
||||
@@ -128,7 +128,7 @@ class TestCompilerChatOutput:
|
||||
result = compiler.compile(tmp_path)
|
||||
|
||||
assert result.success
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text())
|
||||
ui_config = json.loads((tmp_path / "ui-config.json").read_text(encoding="utf-8"))
|
||||
assert len(ui_config["widgets"]) == 2
|
||||
assert ui_config["widgets"][0]["type"] == "slider"
|
||||
assert ui_config["widgets"][0]["name"] == "temperature"
|
||||
|
||||
@@ -24,7 +24,7 @@ DASHBOARD_JS = (
|
||||
@pytest.fixture
|
||||
def dashboard_source():
|
||||
"""Read dashboard.js source."""
|
||||
return DASHBOARD_JS.read_text()
|
||||
return DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestComponentRegistry:
|
||||
@@ -68,7 +68,7 @@ class TestPythonJSComponentParity:
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src" / "praisonaiui" / "ui.py"
|
||||
)
|
||||
source = ui_py.read_text()
|
||||
source = ui_py.read_text(encoding="utf-8")
|
||||
|
||||
# Extract all "type": "xxx" patterns from ui.py
|
||||
py_types = set(re.findall(r'"type":\s*"(\w+)"', source))
|
||||
|
||||
@@ -111,7 +111,7 @@ class TestBuiltinSchemaCoverage:
|
||||
ui_src = (
|
||||
Path(__file__).resolve().parents[2]
|
||||
/ "src" / "praisonaiui" / "ui.py"
|
||||
).read_text()
|
||||
).read_text(encoding="utf-8")
|
||||
py_types = set(re.findall(r'"type":\s*"(\w+)"', ui_src))
|
||||
schemas = srv.get_component_schemas()
|
||||
missing = py_types - set(schemas)
|
||||
|
||||
@@ -136,47 +136,47 @@ class TestRingThresholds:
|
||||
|
||||
class TestChatJsComponents:
|
||||
def test_ring_dom_present(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert 'id="chat-ring-wrap"' in src
|
||||
assert 'id="chat-ring-arc"' in src
|
||||
assert 'id="chat-ring-tooltip"' in src
|
||||
|
||||
def test_send_button_inside_ring_preserved_id(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert 'class="chat-ring-send" id="chat-send-btn"' in src
|
||||
assert 'aria-valuenow' in src
|
||||
assert 'aria-valuemax' in src
|
||||
|
||||
def test_compaction_banner_present(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert 'id="chat-compaction-banner"' in src
|
||||
assert "function updateCompactionBanner(" in src
|
||||
assert "function dismissCompactionBanner(" in src
|
||||
|
||||
def test_memory_chip_present(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert 'id="chat-memory-chip"' in src
|
||||
assert "function refreshMemoryChip(" in src
|
||||
|
||||
def test_ring_state_machine_thresholds(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "function ringStateFor(" in src
|
||||
assert "pct >= 95" in src
|
||||
assert "pct >= 85" in src
|
||||
assert "pct >= 60" in src
|
||||
|
||||
def test_ws_context_events_handled(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "case 'context_update':" in src
|
||||
assert "case 'context_compacted':" in src
|
||||
|
||||
def test_poll_fallback_and_debounce(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "function startContextPoll(" in src
|
||||
assert "function scheduleContextRefresh(" in src
|
||||
|
||||
def test_existing_memory_panel_and_metrics_preserved(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
# Regression: do not remove existing shipped elements
|
||||
assert 'id="chat-memory-panel"' in src
|
||||
assert 'id="chat-memory-btn"' in src
|
||||
|
||||
@@ -27,7 +27,7 @@ DASHBOARD_JS = (
|
||||
|
||||
|
||||
def _style() -> str:
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
m = re.search(r"const DASHBOARD_STYLE = `(.*?)`;", src, re.DOTALL)
|
||||
assert m, "DASHBOARD_STYLE template literal not found"
|
||||
return m.group(1)
|
||||
|
||||
@@ -21,7 +21,7 @@ DASHBOARD_JS = FRONTEND / "dashboard.js"
|
||||
|
||||
class TestSelectPageGuard:
|
||||
def _select_page_body(self) -> str:
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
m = re.search(
|
||||
r"async function selectPage\(pageId\)\s*\{(.*?)^}",
|
||||
src, re.DOTALL | re.MULTILINE,
|
||||
@@ -56,7 +56,7 @@ class TestSelectPageGuard:
|
||||
assert "console.error" in m.group(1)
|
||||
|
||||
def test_show_dashboard_init_error_helper_present(self):
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
m = re.search(
|
||||
r"function showDashboardInitError\(message\)\s*\{(.*?)^}",
|
||||
src, re.DOTALL | re.MULTILINE,
|
||||
|
||||
@@ -23,7 +23,7 @@ GENERATOR = ROOT / "scripts" / "patch_dashboard_modular.py"
|
||||
|
||||
class TestDashboardShellOrder:
|
||||
def _build_body(self) -> str:
|
||||
src = DASHBOARD_JS.read_text()
|
||||
src = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
m = re.search(
|
||||
r"function buildDashboard\([^)]*\)\s*\{(.*?)^}",
|
||||
src, re.DOTALL | re.MULTILINE,
|
||||
@@ -51,7 +51,7 @@ class TestDashboardShellOrder:
|
||||
assert m, "shellHeader insert must be guarded by children.length"
|
||||
|
||||
def test_generator_emits_main_before_insert_before(self):
|
||||
src = GENERATOR.read_text()
|
||||
src = GENERATOR.read_text(encoding="utf-8")
|
||||
append_idx = src.find("root.appendChild(main);\n const shellHeader")
|
||||
insert_idx = src.find("root.insertBefore(shellHeader, main)")
|
||||
assert append_idx != -1, (
|
||||
|
||||
@@ -154,13 +154,13 @@ class TestFinOpsValidation:
|
||||
|
||||
class TestHelpersJs:
|
||||
def test_donut_and_strip_helpers_present(self):
|
||||
src = HELPERS_JS.read_text()
|
||||
src = HELPERS_JS.read_text(encoding="utf-8")
|
||||
assert "export function MiniDonutSVG(" in src
|
||||
assert "export function costStripHTML(" in src
|
||||
assert "export function budgetBannerHTML(" in src
|
||||
|
||||
def test_math_helpers_present(self):
|
||||
src = HELPERS_JS.read_text()
|
||||
src = HELPERS_JS.read_text(encoding="utf-8")
|
||||
assert "export function computeBudgetPct(" in src
|
||||
assert "export function budgetLevelFor(" in src
|
||||
assert "export function sumTodayTokens(" in src
|
||||
@@ -168,53 +168,53 @@ class TestHelpersJs:
|
||||
assert "export function formatCost(" in src
|
||||
|
||||
def test_donut_empty_state_accessible(self):
|
||||
src = HELPERS_JS.read_text()
|
||||
src = HELPERS_JS.read_text(encoding="utf-8")
|
||||
assert 'aria-label="No model data"' in src
|
||||
|
||||
def test_strip_navigates_and_is_region(self):
|
||||
src = HELPERS_JS.read_text()
|
||||
src = HELPERS_JS.read_text(encoding="utf-8")
|
||||
assert 'data-nav="usage"' in src
|
||||
assert 'role="region"' in src
|
||||
|
||||
|
||||
class TestOverviewJs:
|
||||
def test_finops_imports_and_render(self):
|
||||
src = OVERVIEW_JS.read_text()
|
||||
src = OVERVIEW_JS.read_text(encoding="utf-8")
|
||||
assert "costStripHTML" in src
|
||||
assert "budgetBannerHTML" in src
|
||||
assert "function renderFinOps(" in src
|
||||
assert 'id="ov-finops"' in src
|
||||
|
||||
def test_fetches_models_and_config(self):
|
||||
src = OVERVIEW_JS.read_text()
|
||||
src = OVERVIEW_JS.read_text(encoding="utf-8")
|
||||
assert "/api/usage/models" in src
|
||||
assert "/api/config/runtime" in src
|
||||
|
||||
def test_graceful_degrade_state_machine(self):
|
||||
src = OVERVIEW_JS.read_text()
|
||||
src = OVERVIEW_JS.read_text(encoding="utf-8")
|
||||
assert "function finopsState(" in src
|
||||
assert "finops.enabled === false" in src
|
||||
|
||||
|
||||
class TestChatJs:
|
||||
def test_session_cost_chip_present(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert 'id="chat-session-cost-chip"' in src
|
||||
assert "function updateSessionCostChip(" in src
|
||||
|
||||
def test_finops_banner_present(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert 'id="chat-finops-banner"' in src
|
||||
assert "function updateFinopsBanner(" in src
|
||||
assert "function loadFinopsConfig(" in src
|
||||
|
||||
def test_chip_updates_on_run_completed(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert "updateFinopsBanner()" in src
|
||||
assert "updateSessionCostChip(stats)" in src
|
||||
|
||||
def test_context_ring_regression_preserved(self):
|
||||
src = CHAT_JS.read_text()
|
||||
src = CHAT_JS.read_text(encoding="utf-8")
|
||||
assert 'id="chat-ring-wrap"' in src
|
||||
assert "function ringStateFor(" in src
|
||||
assert 'class="chat-ring-send" id="chat-send-btn"' in src
|
||||
|
||||
@@ -108,7 +108,7 @@ class TestFormInputDataLabel:
|
||||
/ "src" / "praisonaiui" / "templates" / "frontend"
|
||||
/ "plugins" / "dashboard.js"
|
||||
)
|
||||
return p.read_text()
|
||||
return p.read_text(encoding="utf-8")
|
||||
|
||||
@pytest.mark.parametrize("renderer", [
|
||||
"renderTextInput",
|
||||
|
||||
@@ -84,7 +84,7 @@ class TestRenderersEmitName:
|
||||
via `el.name` (which has priority over `el.dataset.label`)."""
|
||||
|
||||
def _src(self):
|
||||
return DASHBOARD_JS.read_text()
|
||||
return DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
|
||||
@pytest.mark.parametrize("renderer", [
|
||||
"renderTextInput",
|
||||
@@ -117,7 +117,7 @@ class TestSubmitCollectorPriority:
|
||||
"""renderFormAction submit handler must prefer `name` over `data-label`."""
|
||||
|
||||
def test_name_has_priority_over_label(self):
|
||||
source = DASHBOARD_JS.read_text()
|
||||
source = DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
m = re.search(
|
||||
r"function renderFormAction\(comp\)\s*\{(.*?)^}",
|
||||
source, re.DOTALL | re.MULTILINE,
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestFrontendNavigation:
|
||||
index_path = Path("src/praisonaiui/templates/frontend/index.html")
|
||||
assert index_path.exists(), "index.html should exist"
|
||||
|
||||
content = index_path.read_text()
|
||||
content = index_path.read_text(encoding="utf-8")
|
||||
assert "/assets/index.js" in content, "Should reference index.js"
|
||||
assert "/assets/index.css" in content, "Should reference index.css"
|
||||
|
||||
@@ -26,7 +26,7 @@ class TestFrontendNavigation:
|
||||
if bundle_path.exists():
|
||||
# Check if the bundle contains popstate handling code
|
||||
# Note: The bundle is minified, so we check for the event name
|
||||
content = bundle_path.read_text()
|
||||
content = bundle_path.read_text(encoding="utf-8")
|
||||
assert "popstate" in content, "Bundle should contain popstate event handler"
|
||||
|
||||
def test_title_template_handling(self):
|
||||
|
||||
@@ -23,7 +23,7 @@ SERVER = _ROOT / "src" / "praisonaiui" / "server.py"
|
||||
|
||||
|
||||
def _inbox() -> str:
|
||||
return INBOX_VIEW.read_text()
|
||||
return INBOX_VIEW.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestThreePaneShell:
|
||||
@@ -109,21 +109,21 @@ class TestEmptyState:
|
||||
|
||||
class TestRegistration:
|
||||
def test_route_registered_in_dashboard(self):
|
||||
src = DASHBOARD.read_text()
|
||||
src = DASHBOARD.read_text(encoding="utf-8")
|
||||
assert "inbox:" in src
|
||||
assert "/plugins/views/inbox.js" in src
|
||||
|
||||
def test_page_registered_in_server(self):
|
||||
src = SERVER.read_text()
|
||||
src = SERVER.read_text(encoding="utf-8")
|
||||
assert '"id": "inbox"' in src
|
||||
assert "Channel Inbox" in src
|
||||
|
||||
def test_channels_links_to_inbox(self):
|
||||
assert 'href="/inbox"' in CHANNELS_VIEW.read_text()
|
||||
assert 'href="/inbox"' in CHANNELS_VIEW.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
class TestSessionsPlatformField:
|
||||
def test_sessions_list_surfaces_platform(self):
|
||||
src = SESSIONS_EXT.read_text()
|
||||
src = SESSIONS_EXT.read_text(encoding="utf-8")
|
||||
assert '"platform"' in src
|
||||
assert '"channel_id"' in src
|
||||
@@ -22,7 +22,7 @@ CHAT_JS = FRONTEND / "views" / "chat.js"
|
||||
|
||||
class TestDashboardSessionSearch:
|
||||
def _src(self) -> str:
|
||||
return DASHBOARD_JS.read_text()
|
||||
return DASHBOARD_JS.read_text(encoding="utf-8")
|
||||
|
||||
def test_ctrl_k_keydown_handler_present(self):
|
||||
# The Ctrl+K binding is owned by the command palette, which exposes a
|
||||
@@ -107,7 +107,7 @@ class TestDashboardSessionSearch:
|
||||
|
||||
class TestChatViewSessionSelectListener:
|
||||
def _src(self) -> str:
|
||||
return CHAT_JS.read_text()
|
||||
return CHAT_JS.read_text(encoding="utf-8")
|
||||
|
||||
def test_chat_listens_for_session_select(self):
|
||||
src = self._src()
|
||||
|
||||
@@ -48,41 +48,41 @@ class TestI18nKeys:
|
||||
|
||||
class TestHelpersJs:
|
||||
def test_helpers_export_functions(self):
|
||||
src = HELPERS_JS.read_text()
|
||||
src = HELPERS_JS.read_text(encoding="utf-8")
|
||||
assert "export function diffLines(" in src
|
||||
assert "export function renderMarkdownPreview(" in src
|
||||
assert "export function isSkillApproval(" in src
|
||||
|
||||
def test_diff_normalises_crlf(self):
|
||||
src = HELPERS_JS.read_text()
|
||||
src = HELPERS_JS.read_text(encoding="utf-8")
|
||||
assert "replace(/\\r\\n?/g, '\\n')" in src
|
||||
|
||||
def test_preview_escapes_first(self):
|
||||
src = HELPERS_JS.read_text()
|
||||
src = HELPERS_JS.read_text(encoding="utf-8")
|
||||
assert "esc(s)" in src
|
||||
|
||||
|
||||
class TestSkillsJs:
|
||||
def test_exports_render_and_cleanup(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
assert "export async function render" in src
|
||||
assert "export function cleanup" in src
|
||||
|
||||
def test_tab_shell_present(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
assert "class=\"ss-tab\"" in src
|
||||
assert "tabBtn('pending'" in src
|
||||
assert "tabBtn('installed'" in src
|
||||
assert "tabBtn('history'" in src
|
||||
|
||||
def test_url_state_and_deep_link(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
assert "approval_id" in src
|
||||
assert "currentApprovalId(" in src
|
||||
assert "openDiff(" in src
|
||||
|
||||
def test_uses_existing_apis(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
assert "/api/approvals/pending" in src
|
||||
assert "/api/approvals/history" in src
|
||||
assert "/api/skills" in src
|
||||
@@ -90,30 +90,30 @@ class TestSkillsJs:
|
||||
assert "/api/approvals/${id}/${verb}" in src
|
||||
|
||||
def test_imports_skill_helpers(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
assert "isSkillApproval" in src
|
||||
assert "diffLines" in src
|
||||
assert "renderMarkdownPreview" in src
|
||||
|
||||
def test_no_browser_eval(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
assert "eval(" not in src
|
||||
|
||||
def test_proposed_content_escaped_in_diff(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
# Diff rows must escape line text; no raw interpolation of r.text.
|
||||
assert "esc(r.text)" in src
|
||||
assert "${r.text}" not in src
|
||||
|
||||
def test_empty_state_and_no_existing_labels(self):
|
||||
src = SKILLS_JS.read_text()
|
||||
src = SKILLS_JS.read_text(encoding="utf-8")
|
||||
assert "No pending skill writes" in src
|
||||
assert "No existing skill" in src
|
||||
|
||||
|
||||
class TestApprovalsCrossLink:
|
||||
def test_skill_row_links_to_studio(self):
|
||||
src = APPROVALS_JS.read_text()
|
||||
src = APPROVALS_JS.read_text(encoding="utf-8")
|
||||
assert "isSkillApproval" in src
|
||||
assert "/skills?tab=pending&approval_id=" in src
|
||||
|
||||
|
||||
Reference in new issue
Block a user