229 lines
7.6 KiB
Python
229 lines
7.6 KiB
Python
"""Tests for the startup theme picker and shared preview builders."""
|
||
|
||
from __future__ import annotations
|
||
|
||
import pytest
|
||
from omnigent_ui_sdk.terminal._theme import DARK_THEME, LIGHT_THEME
|
||
|
||
from omnigent.repl._theme_picker import (
|
||
_build_dark_preview,
|
||
_build_light_preview,
|
||
_build_preview,
|
||
_parse_osc11_response,
|
||
_render_theme_picker,
|
||
build_theme_confirmation,
|
||
startup_theme_picker,
|
||
)
|
||
|
||
# ── Preview builders ──────────────────────────────────────────
|
||
|
||
|
||
def test_build_dark_preview_returns_panel() -> None:
|
||
"""Dark preview builder returns a Rich Panel with theme samples."""
|
||
from rich.panel import Panel
|
||
|
||
panel = _build_dark_preview()
|
||
assert isinstance(panel, Panel)
|
||
|
||
|
||
def test_build_light_preview_returns_panel() -> None:
|
||
"""Light preview builder returns a Rich Panel with theme samples."""
|
||
from rich.panel import Panel
|
||
|
||
panel = _build_light_preview()
|
||
assert isinstance(panel, Panel)
|
||
|
||
|
||
def test_build_preview_dispatches_dark() -> None:
|
||
"""_build_preview('dark') returns a panel."""
|
||
from rich.panel import Panel
|
||
|
||
panel = _build_preview("dark")
|
||
assert isinstance(panel, Panel)
|
||
|
||
|
||
def test_build_preview_dispatches_light() -> None:
|
||
"""_build_preview('light') returns a panel."""
|
||
from rich.panel import Panel
|
||
|
||
panel = _build_preview("light")
|
||
assert isinstance(panel, Panel)
|
||
|
||
|
||
def test_build_preview_respects_width() -> None:
|
||
"""Preview panels honor the requested width."""
|
||
panel = _build_dark_preview(width=40)
|
||
assert panel.width == 40
|
||
|
||
|
||
# ── Picker rendering ─────────────────────────────────────────
|
||
|
||
|
||
def test_render_theme_picker_contains_menu_items() -> None:
|
||
"""Rendered picker shows both dark and light options."""
|
||
rendered = _render_theme_picker(0, width=60)
|
||
assert "dark mode" in rendered
|
||
assert "light mode" in rendered
|
||
|
||
|
||
def test_render_theme_picker_highlights_selected_dark() -> None:
|
||
"""When dark (index 0) is selected, its indicator is present."""
|
||
rendered = _render_theme_picker(0, width=60)
|
||
assert "❯ dark mode" in rendered
|
||
|
||
|
||
def test_render_theme_picker_highlights_selected_light() -> None:
|
||
"""When light (index 1) is selected, its indicator is present."""
|
||
rendered = _render_theme_picker(1, width=60)
|
||
assert "❯ light mode" in rendered
|
||
|
||
|
||
def test_render_theme_picker_shows_footer_hints() -> None:
|
||
"""Footer shows navigation hints."""
|
||
rendered = _render_theme_picker(0, width=60)
|
||
assert "navigate" in rendered
|
||
assert "Enter" in rendered
|
||
|
||
|
||
def test_render_theme_picker_shows_preview_for_selected_only() -> None:
|
||
"""Only the selected theme's preview panel appears."""
|
||
rendered_dark = _render_theme_picker(0, width=60)
|
||
rendered_light = _render_theme_picker(1, width=60)
|
||
# Dark selection shows "dark preview"
|
||
assert "dark" in rendered_dark.lower()
|
||
# Light selection shows "light preview"
|
||
assert "light" in rendered_light.lower()
|
||
|
||
|
||
# ── OSC 11 parsing ────────────────────────────────────────────
|
||
|
||
|
||
def test_parse_osc11_dark_background() -> None:
|
||
"""A low-luminance background classifies as dark."""
|
||
# rgb:0000/0000/0000 = pure black.
|
||
response = "\033]11;rgb:0000/0000/0000\033\\"
|
||
assert _parse_osc11_response(response) == "dark"
|
||
|
||
|
||
def test_parse_osc11_light_background() -> None:
|
||
"""A high-luminance background classifies as light."""
|
||
# rgb:ffff/ffff/ffff = pure white.
|
||
response = "\033]11;rgb:ffff/ffff/ffff\033\\"
|
||
assert _parse_osc11_response(response) == "light"
|
||
|
||
|
||
def test_parse_osc11_mid_dark() -> None:
|
||
"""A mid-dark background (common terminal themes) classifies as dark."""
|
||
# rgb:1c1c/1c1c/1c1c — typical dark theme bg.
|
||
response = "\033]11;rgb:1c1c/1c1c/1c1c\033\\"
|
||
assert _parse_osc11_response(response) == "dark"
|
||
|
||
|
||
def test_parse_osc11_mid_light() -> None:
|
||
"""A mid-light background classifies as light."""
|
||
# rgb:e0e0/e0e0/e0e0 — typical light theme bg.
|
||
response = "\033]11;rgb:e0e0/e0e0/e0e0\033\\"
|
||
assert _parse_osc11_response(response) == "light"
|
||
|
||
|
||
def test_parse_osc11_two_digit_components() -> None:
|
||
"""Two-digit hex components (e.g. from some terminals) parse correctly."""
|
||
response = "\033]11;rgb:00/00/00\033\\"
|
||
assert _parse_osc11_response(response) == "dark"
|
||
|
||
response = "\033]11;rgb:ff/ff/ff\033\\"
|
||
assert _parse_osc11_response(response) == "light"
|
||
|
||
|
||
def test_parse_osc11_bel_terminator() -> None:
|
||
"""Response terminated with BEL (\\x07) instead of ST also parses."""
|
||
response = "\033]11;rgb:0000/0000/0000\x07"
|
||
assert _parse_osc11_response(response) == "dark"
|
||
|
||
|
||
def test_parse_osc11_no_rgb() -> None:
|
||
"""Response without rgb: returns None."""
|
||
assert _parse_osc11_response("\033]11;unknown\033\\") is None
|
||
|
||
|
||
def test_parse_osc11_empty_response() -> None:
|
||
"""Empty response returns None."""
|
||
assert _parse_osc11_response("") is None
|
||
|
||
|
||
def test_parse_osc11_malformed_components() -> None:
|
||
"""Malformed RGB components return None."""
|
||
assert _parse_osc11_response("\033]11;rgb:xyz/abc/def\033\\") is None
|
||
|
||
|
||
def test_parse_osc11_wrong_number_of_components() -> None:
|
||
"""Wrong number of RGB components returns None."""
|
||
assert _parse_osc11_response("\033]11;rgb:0000/0000\033\\") is None
|
||
|
||
|
||
# ── Confirmation line ─────────────────────────────────────────
|
||
|
||
|
||
def test_build_theme_confirmation_dark() -> None:
|
||
"""Confirmation for dark theme contains 'dark' and 'saved'."""
|
||
text = build_theme_confirmation(DARK_THEME)
|
||
plain = text.plain
|
||
assert "dark" in plain
|
||
assert "mode (saved)" in plain
|
||
|
||
|
||
def test_build_theme_confirmation_light() -> None:
|
||
"""Confirmation for light theme contains 'light' and 'saved'."""
|
||
text = build_theme_confirmation(LIGHT_THEME)
|
||
plain = text.plain
|
||
assert "light" in plain
|
||
assert "mode (saved)" in plain
|
||
|
||
|
||
# ── Startup picker (non-tty fallback) ────────────────────────
|
||
|
||
|
||
def test_startup_picker_non_tty_defaults_to_light(
|
||
tmp_path,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
"""When stdin is not a tty, the picker skips interactive mode and uses light."""
|
||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||
# Ensure stdin.isatty() returns False.
|
||
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
||
# Mock OSC 11 detection to return None (non-tty can't detect).
|
||
monkeypatch.setattr(
|
||
"omnigent.repl._theme_picker._detect_terminal_background",
|
||
lambda: None,
|
||
)
|
||
|
||
import io
|
||
|
||
out = io.StringIO()
|
||
result = startup_theme_picker(out=out)
|
||
assert result is LIGHT_THEME
|
||
# Should have persisted the choice.
|
||
config = (tmp_path / ".omnigent" / "config.yaml").read_text(encoding="utf-8")
|
||
assert "theme: light" in config
|
||
|
||
|
||
def test_startup_picker_non_tty_respects_dark_detection(
|
||
tmp_path,
|
||
monkeypatch: pytest.MonkeyPatch,
|
||
) -> None:
|
||
"""When OSC 11 detects dark on a non-tty, the picker selects dark."""
|
||
monkeypatch.setattr("pathlib.Path.home", lambda: tmp_path)
|
||
monkeypatch.setattr("sys.stdin.isatty", lambda: False)
|
||
monkeypatch.setattr(
|
||
"omnigent.repl._theme_picker._detect_terminal_background",
|
||
lambda: "dark",
|
||
)
|
||
|
||
import io
|
||
|
||
out = io.StringIO()
|
||
result = startup_theme_picker(out=out)
|
||
assert result is DARK_THEME
|
||
config = (tmp_path / ".omnigent" / "config.yaml").read_text(encoding="utf-8")
|
||
assert "theme: dark" in config
|