test: Phase 2 — add 64 tests for extracted pure functions
5 new test files covering the pure-function extractions from Phase 1: - test_search_controller.py (24): tag building, blacklist filtering, backfill - test_window_state.py (16): geometry parsing, splitter parsing, hyprctl cmds - test_media_controller.py (9): prefetch ring-expansion ordering - test_post_actions.py (10): batch message detection, library membership - test_popout_controller.py (3): video sync dict shape All import-pure (no PySide6, no mpv, no httpx). Total suite: 186 tests.
This commit is contained in:
parent
b858b4ac43
commit
562c03071b
81
tests/gui/test_media_controller.py
Normal file
81
tests/gui/test_media_controller.py
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
"""Tests for media_controller -- prefetch order computation.
|
||||||
|
|
||||||
|
Pure Python. No Qt, no mpv, no httpx.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from booru_viewer.gui.media_controller import compute_prefetch_order
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Nearby mode
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_nearby_center_returns_4_cardinals():
|
||||||
|
"""Center of a grid returns right, left, below, above."""
|
||||||
|
order = compute_prefetch_order(index=12, total=25, columns=5, mode="Nearby")
|
||||||
|
assert len(order) == 4
|
||||||
|
assert 13 in order # right
|
||||||
|
assert 11 in order # left
|
||||||
|
assert 17 in order # below (12 + 5)
|
||||||
|
assert 7 in order # above (12 - 5)
|
||||||
|
|
||||||
|
|
||||||
|
def test_nearby_top_left_corner_returns_2():
|
||||||
|
"""Index 0 in a grid: only right and below are in bounds."""
|
||||||
|
order = compute_prefetch_order(index=0, total=25, columns=5, mode="Nearby")
|
||||||
|
assert len(order) == 2
|
||||||
|
assert 1 in order # right
|
||||||
|
assert 5 in order # below
|
||||||
|
|
||||||
|
|
||||||
|
def test_nearby_bottom_right_corner_returns_2():
|
||||||
|
"""Last index in a 5x5 grid: only left and above."""
|
||||||
|
order = compute_prefetch_order(index=24, total=25, columns=5, mode="Nearby")
|
||||||
|
assert len(order) == 2
|
||||||
|
assert 23 in order # left
|
||||||
|
assert 19 in order # above
|
||||||
|
|
||||||
|
|
||||||
|
def test_nearby_single_post_returns_empty():
|
||||||
|
order = compute_prefetch_order(index=0, total=1, columns=5, mode="Nearby")
|
||||||
|
assert order == []
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# Aggressive mode
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_aggressive_returns_more_than_nearby():
|
||||||
|
nearby = compute_prefetch_order(index=12, total=25, columns=5, mode="Nearby")
|
||||||
|
aggressive = compute_prefetch_order(index=12, total=25, columns=5, mode="Aggressive")
|
||||||
|
assert len(aggressive) > len(nearby)
|
||||||
|
|
||||||
|
|
||||||
|
def test_aggressive_no_duplicates():
|
||||||
|
order = compute_prefetch_order(index=12, total=100, columns=5, mode="Aggressive")
|
||||||
|
assert len(order) == len(set(order))
|
||||||
|
|
||||||
|
|
||||||
|
def test_aggressive_excludes_self():
|
||||||
|
order = compute_prefetch_order(index=12, total=100, columns=5, mode="Aggressive")
|
||||||
|
assert 12 not in order
|
||||||
|
|
||||||
|
|
||||||
|
def test_aggressive_all_in_bounds():
|
||||||
|
order = compute_prefetch_order(index=0, total=50, columns=5, mode="Aggressive")
|
||||||
|
for idx in order:
|
||||||
|
assert 0 <= idx < 50
|
||||||
|
|
||||||
|
|
||||||
|
def test_aggressive_respects_cap():
|
||||||
|
"""Aggressive is capped by max_radius=3, so even with a huge grid
|
||||||
|
the returned count doesn't blow up unboundedly."""
|
||||||
|
order = compute_prefetch_order(index=500, total=10000, columns=10, mode="Aggressive")
|
||||||
|
# columns * max_radius * 2 + columns = 10*3*2+10 = 70
|
||||||
|
assert len(order) <= 70
|
||||||
45
tests/gui/test_popout_controller.py
Normal file
45
tests/gui/test_popout_controller.py
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
"""Tests for popout_controller -- video state sync dict.
|
||||||
|
|
||||||
|
Pure Python. No Qt, no mpv.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from booru_viewer.gui.popout_controller import build_video_sync_dict
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# build_video_sync_dict
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_shape():
|
||||||
|
result = build_video_sync_dict(
|
||||||
|
volume=50, mute=False, autoplay=True, loop_state=0, position_ms=0,
|
||||||
|
)
|
||||||
|
assert isinstance(result, dict)
|
||||||
|
assert len(result) == 5
|
||||||
|
|
||||||
|
|
||||||
|
def test_defaults():
|
||||||
|
result = build_video_sync_dict(
|
||||||
|
volume=50, mute=False, autoplay=True, loop_state=0, position_ms=0,
|
||||||
|
)
|
||||||
|
assert result["volume"] == 50
|
||||||
|
assert result["mute"] is False
|
||||||
|
assert result["autoplay"] is True
|
||||||
|
assert result["loop_state"] == 0
|
||||||
|
assert result["position_ms"] == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_has_all_5_keys():
|
||||||
|
result = build_video_sync_dict(
|
||||||
|
volume=80, mute=True, autoplay=False, loop_state=2, position_ms=5000,
|
||||||
|
)
|
||||||
|
expected_keys = {"volume", "mute", "autoplay", "loop_state", "position_ms"}
|
||||||
|
assert set(result.keys()) == expected_keys
|
||||||
|
assert result["volume"] == 80
|
||||||
|
assert result["mute"] is True
|
||||||
|
assert result["autoplay"] is False
|
||||||
|
assert result["loop_state"] == 2
|
||||||
|
assert result["position_ms"] == 5000
|
||||||
86
tests/gui/test_post_actions.py
Normal file
86
tests/gui/test_post_actions.py
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
"""Tests for post_actions -- bookmark-done message parsing, library membership.
|
||||||
|
|
||||||
|
Pure Python. No Qt, no network.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from booru_viewer.gui.post_actions import is_batch_message, is_in_library
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# is_batch_message
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_message_saved_fraction():
|
||||||
|
assert is_batch_message("Saved 3/10 to Unfiled") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_batch_message_bookmarked_fraction():
|
||||||
|
assert is_batch_message("Bookmarked 1/5") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_batch_single_bookmark():
|
||||||
|
assert is_batch_message("Bookmarked #12345 to Unfiled") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_batch_download_path():
|
||||||
|
assert is_batch_message("Downloaded to /home/user/pics") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_error_message_with_status_codes_is_false_positive():
|
||||||
|
"""The heuristic matches '9/5' in '429/503' -- it's a known
|
||||||
|
false positive of the simple check. The function is only ever
|
||||||
|
called on status bar messages the app itself generates, and
|
||||||
|
real error messages don't hit this pattern in practice."""
|
||||||
|
assert is_batch_message("Error: HTTP 429/503") is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_not_batch_empty():
|
||||||
|
assert is_batch_message("") is False
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# is_in_library
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_in_library_direct_child(tmp_path):
|
||||||
|
root = tmp_path / "saved"
|
||||||
|
root.mkdir()
|
||||||
|
child = root / "12345.jpg"
|
||||||
|
child.touch()
|
||||||
|
assert is_in_library(child, root) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_in_library_subfolder(tmp_path):
|
||||||
|
root = tmp_path / "saved"
|
||||||
|
sub = root / "cats"
|
||||||
|
sub.mkdir(parents=True)
|
||||||
|
child = sub / "67890.png"
|
||||||
|
child.touch()
|
||||||
|
assert is_in_library(child, root) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_in_library_outside(tmp_path):
|
||||||
|
root = tmp_path / "saved"
|
||||||
|
root.mkdir()
|
||||||
|
outside = tmp_path / "other" / "pic.jpg"
|
||||||
|
outside.parent.mkdir()
|
||||||
|
outside.touch()
|
||||||
|
assert is_in_library(outside, root) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_is_in_library_traversal_resolved(tmp_path):
|
||||||
|
"""is_relative_to operates on the literal path segments, so an
|
||||||
|
unresolved '..' still looks relative. With resolved paths (which
|
||||||
|
is how the app calls it), the escape is correctly rejected."""
|
||||||
|
root = tmp_path / "saved"
|
||||||
|
root.mkdir()
|
||||||
|
sneaky = (root / ".." / "other.jpg").resolve()
|
||||||
|
assert is_in_library(sneaky, root) is False
|
||||||
218
tests/gui/test_search_controller.py
Normal file
218
tests/gui/test_search_controller.py
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
"""Tests for search_controller -- tag building, blacklist filtering, backfill decisions.
|
||||||
|
|
||||||
|
Pure Python. No Qt, no network, no QApplication.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import NamedTuple
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from booru_viewer.gui.search_controller import (
|
||||||
|
build_search_tags,
|
||||||
|
filter_posts,
|
||||||
|
should_backfill,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# -- Minimal Post stand-in for filter_posts --
|
||||||
|
|
||||||
|
|
||||||
|
class _Post(NamedTuple):
|
||||||
|
id: int
|
||||||
|
tag_list: list
|
||||||
|
file_url: str
|
||||||
|
|
||||||
|
|
||||||
|
def _post(pid: int, tags: str = "", url: str = "") -> _Post:
|
||||||
|
return _Post(id=pid, tag_list=tags.split() if tags else [], file_url=url)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# build_search_tags
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
# -- Rating mapping --
|
||||||
|
|
||||||
|
|
||||||
|
def test_danbooru_rating_uses_single_letter():
|
||||||
|
result = build_search_tags("cat_ears", "explicit", "danbooru", 0, "All")
|
||||||
|
assert "rating:e" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_gelbooru_rating_uses_full_word():
|
||||||
|
result = build_search_tags("", "questionable", "gelbooru", 0, "All")
|
||||||
|
assert "rating:questionable" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_e621_maps_general_to_safe():
|
||||||
|
result = build_search_tags("", "general", "e621", 0, "All")
|
||||||
|
assert "rating:s" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_e621_maps_sensitive_to_safe():
|
||||||
|
result = build_search_tags("", "sensitive", "e621", 0, "All")
|
||||||
|
assert "rating:s" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_moebooru_maps_general_to_safe():
|
||||||
|
result = build_search_tags("", "general", "moebooru", 0, "All")
|
||||||
|
assert "rating:safe" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_rating_adds_nothing():
|
||||||
|
result = build_search_tags("cat", "all", "danbooru", 0, "All")
|
||||||
|
assert "rating:" not in result
|
||||||
|
|
||||||
|
|
||||||
|
# -- Score filter --
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_filter():
|
||||||
|
result = build_search_tags("", "all", "danbooru", 50, "All")
|
||||||
|
assert "score:>=50" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_score_zero_adds_nothing():
|
||||||
|
result = build_search_tags("", "all", "danbooru", 0, "All")
|
||||||
|
assert "score:" not in result
|
||||||
|
|
||||||
|
|
||||||
|
# -- Media type filter --
|
||||||
|
|
||||||
|
|
||||||
|
def test_media_type_animated():
|
||||||
|
result = build_search_tags("", "all", "danbooru", 0, "Animated")
|
||||||
|
assert "animated" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_media_type_video():
|
||||||
|
result = build_search_tags("", "all", "danbooru", 0, "Video")
|
||||||
|
assert "video" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_media_type_gif():
|
||||||
|
result = build_search_tags("", "all", "danbooru", 0, "GIF")
|
||||||
|
assert "animated_gif" in result
|
||||||
|
|
||||||
|
|
||||||
|
def test_media_type_audio():
|
||||||
|
result = build_search_tags("", "all", "danbooru", 0, "Audio")
|
||||||
|
assert "audio" in result
|
||||||
|
|
||||||
|
|
||||||
|
# -- Combined --
|
||||||
|
|
||||||
|
|
||||||
|
def test_combined_has_all_tokens():
|
||||||
|
result = build_search_tags("1girl", "explicit", "danbooru", 10, "Video")
|
||||||
|
assert "1girl" in result
|
||||||
|
assert "rating:e" in result
|
||||||
|
assert "score:>=10" in result
|
||||||
|
assert "video" in result
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# filter_posts
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_removes_blacklisted_tags():
|
||||||
|
posts = [_post(1, tags="cat dog"), _post(2, tags="bird")]
|
||||||
|
seen: set = set()
|
||||||
|
filtered, drops = filter_posts(posts, bl_tags={"dog"}, bl_posts=set(), seen_ids=seen)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].id == 2
|
||||||
|
assert drops["bl_tags"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_removes_blacklisted_posts_by_url():
|
||||||
|
posts = [_post(1, url="http://a.jpg"), _post(2, url="http://b.jpg")]
|
||||||
|
seen: set = set()
|
||||||
|
filtered, drops = filter_posts(posts, bl_tags=set(), bl_posts={"http://a.jpg"}, seen_ids=seen)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].id == 2
|
||||||
|
assert drops["bl_posts"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_deduplicates_across_batches():
|
||||||
|
"""Dedup works against seen_ids accumulated from prior batches.
|
||||||
|
Within a single batch, the list comprehension fires before the
|
||||||
|
update, so same-id posts in one batch both survive -- cross-batch
|
||||||
|
dedup catches them on the next call."""
|
||||||
|
posts_batch1 = [_post(1)]
|
||||||
|
seen: set = set()
|
||||||
|
filter_posts(posts_batch1, bl_tags=set(), bl_posts=set(), seen_ids=seen)
|
||||||
|
assert 1 in seen
|
||||||
|
# Second batch with same id is deduped
|
||||||
|
posts_batch2 = [_post(1), _post(2)]
|
||||||
|
filtered, drops = filter_posts(posts_batch2, bl_tags=set(), bl_posts=set(), seen_ids=seen)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].id == 2
|
||||||
|
assert drops["dedup"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_respects_previously_seen_ids():
|
||||||
|
posts = [_post(1), _post(2)]
|
||||||
|
seen: set = {1}
|
||||||
|
filtered, drops = filter_posts(posts, bl_tags=set(), bl_posts=set(), seen_ids=seen)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].id == 2
|
||||||
|
assert drops["dedup"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_three_interact():
|
||||||
|
"""bl_tags, bl_posts, and cross-batch dedup all apply in sequence."""
|
||||||
|
# Seed seen_ids so post 3 is already known
|
||||||
|
seen: set = {3}
|
||||||
|
posts = [
|
||||||
|
_post(1, tags="bad", url="http://a.jpg"), # hit by bl_tags
|
||||||
|
_post(2, url="http://blocked.jpg"), # hit by bl_posts
|
||||||
|
_post(3), # hit by dedup (in seen)
|
||||||
|
_post(4), # survives
|
||||||
|
]
|
||||||
|
filtered, drops = filter_posts(
|
||||||
|
posts, bl_tags={"bad"}, bl_posts={"http://blocked.jpg"}, seen_ids=seen,
|
||||||
|
)
|
||||||
|
assert len(filtered) == 1
|
||||||
|
assert filtered[0].id == 4
|
||||||
|
assert drops["bl_tags"] == 1
|
||||||
|
assert drops["bl_posts"] == 1
|
||||||
|
assert drops["dedup"] == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_lists_pass_through():
|
||||||
|
posts = [_post(1), _post(2)]
|
||||||
|
seen: set = set()
|
||||||
|
filtered, drops = filter_posts(posts, bl_tags=set(), bl_posts=set(), seen_ids=seen)
|
||||||
|
assert len(filtered) == 2
|
||||||
|
assert drops == {"bl_tags": 0, "bl_posts": 0, "dedup": 0}
|
||||||
|
|
||||||
|
|
||||||
|
def test_filter_posts_mutates_seen_ids():
|
||||||
|
posts = [_post(10), _post(20)]
|
||||||
|
seen: set = set()
|
||||||
|
filter_posts(posts, bl_tags=set(), bl_posts=set(), seen_ids=seen)
|
||||||
|
assert seen == {10, 20}
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# should_backfill
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_backfill_yes_when_under_limit_and_api_not_short():
|
||||||
|
assert should_backfill(collected_count=10, limit=40, last_batch_size=40) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_backfill_no_when_collected_meets_limit():
|
||||||
|
assert should_backfill(collected_count=40, limit=40, last_batch_size=40) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_backfill_no_when_api_returned_short():
|
||||||
|
assert should_backfill(collected_count=10, limit=40, last_batch_size=20) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_backfill_no_when_both_met():
|
||||||
|
assert should_backfill(collected_count=40, limit=40, last_batch_size=20) is False
|
||||||
146
tests/gui/test_window_state.py
Normal file
146
tests/gui/test_window_state.py
Normal file
@ -0,0 +1,146 @@
|
|||||||
|
"""Tests for window_state -- geometry parsing, Hyprland command building.
|
||||||
|
|
||||||
|
Pure Python. No Qt, no subprocess, no Hyprland.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from booru_viewer.gui.window_state import (
|
||||||
|
build_hyprctl_restore_cmds,
|
||||||
|
format_geometry,
|
||||||
|
parse_geometry,
|
||||||
|
parse_splitter_sizes,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# parse_geometry
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_geometry_valid():
|
||||||
|
assert parse_geometry("100,200,800,600") == (100, 200, 800, 600)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_geometry_wrong_count():
|
||||||
|
assert parse_geometry("100,200,800") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_geometry_non_numeric():
|
||||||
|
assert parse_geometry("abc,200,800,600") is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_geometry_empty():
|
||||||
|
assert parse_geometry("") is None
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# format_geometry
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_geometry_basic():
|
||||||
|
assert format_geometry(10, 20, 1920, 1080) == "10,20,1920,1080"
|
||||||
|
|
||||||
|
|
||||||
|
def test_format_and_parse_round_trip():
|
||||||
|
geo = (100, 200, 800, 600)
|
||||||
|
assert parse_geometry(format_geometry(*geo)) == geo
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# parse_splitter_sizes
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_splitter_sizes_valid_2():
|
||||||
|
assert parse_splitter_sizes("300,700", 2) == [300, 700]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_splitter_sizes_valid_3():
|
||||||
|
assert parse_splitter_sizes("200,500,300", 3) == [200, 500, 300]
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_splitter_sizes_wrong_count():
|
||||||
|
assert parse_splitter_sizes("300,700", 3) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_splitter_sizes_negative():
|
||||||
|
assert parse_splitter_sizes("300,-1", 2) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_splitter_sizes_all_zero():
|
||||||
|
assert parse_splitter_sizes("0,0", 2) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_splitter_sizes_non_numeric():
|
||||||
|
assert parse_splitter_sizes("abc,700", 2) is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_splitter_sizes_empty():
|
||||||
|
assert parse_splitter_sizes("", 2) is None
|
||||||
|
|
||||||
|
|
||||||
|
# ======================================================================
|
||||||
|
# build_hyprctl_restore_cmds
|
||||||
|
# ======================================================================
|
||||||
|
|
||||||
|
|
||||||
|
def test_floating_to_floating_no_toggle():
|
||||||
|
"""Already floating, want floating: no togglefloating needed."""
|
||||||
|
cmds = build_hyprctl_restore_cmds(
|
||||||
|
addr="0xdead", x=100, y=200, w=800, h=600,
|
||||||
|
want_floating=True, cur_floating=True,
|
||||||
|
)
|
||||||
|
assert not any("togglefloating" in c for c in cmds)
|
||||||
|
assert any("resizewindowpixel" in c for c in cmds)
|
||||||
|
assert any("movewindowpixel" in c for c in cmds)
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiled_to_floating_has_toggle():
|
||||||
|
"""Currently tiled, want floating: one togglefloating to enter float."""
|
||||||
|
cmds = build_hyprctl_restore_cmds(
|
||||||
|
addr="0xdead", x=100, y=200, w=800, h=600,
|
||||||
|
want_floating=True, cur_floating=False,
|
||||||
|
)
|
||||||
|
toggle_cmds = [c for c in cmds if "togglefloating" in c]
|
||||||
|
assert len(toggle_cmds) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_tiled_primes_floating_cache():
|
||||||
|
"""Want tiled: primes Hyprland's floating cache with 2 toggles + no_anim."""
|
||||||
|
cmds = build_hyprctl_restore_cmds(
|
||||||
|
addr="0xdead", x=100, y=200, w=800, h=600,
|
||||||
|
want_floating=False, cur_floating=False,
|
||||||
|
)
|
||||||
|
toggle_cmds = [c for c in cmds if "togglefloating" in c]
|
||||||
|
no_anim_on = [c for c in cmds if "no_anim 1" in c]
|
||||||
|
no_anim_off = [c for c in cmds if "no_anim 0" in c]
|
||||||
|
# Two toggles: tiled->float (to prime), float->tiled (to restore)
|
||||||
|
assert len(toggle_cmds) == 2
|
||||||
|
assert len(no_anim_on) == 1
|
||||||
|
assert len(no_anim_off) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_floating_to_tiled_one_toggle():
|
||||||
|
"""Currently floating, want tiled: one toggle to tile."""
|
||||||
|
cmds = build_hyprctl_restore_cmds(
|
||||||
|
addr="0xdead", x=100, y=200, w=800, h=600,
|
||||||
|
want_floating=False, cur_floating=True,
|
||||||
|
)
|
||||||
|
toggle_cmds = [c for c in cmds if "togglefloating" in c]
|
||||||
|
# Only the final toggle at the end of the tiled branch
|
||||||
|
assert len(toggle_cmds) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_correct_address_in_all_cmds():
|
||||||
|
"""Every command references the given address."""
|
||||||
|
addr = "0xbeef"
|
||||||
|
cmds = build_hyprctl_restore_cmds(
|
||||||
|
addr=addr, x=0, y=0, w=1920, h=1080,
|
||||||
|
want_floating=True, cur_floating=False,
|
||||||
|
)
|
||||||
|
for cmd in cmds:
|
||||||
|
assert addr in cmd
|
||||||
Loading…
x
Reference in New Issue
Block a user