mirror of
https://github.com/happycola233/tchMaterial-parser.git
synced 2026-10-18 22:45:51 +08:00
baf3ba3fbc
- 使用 Callable 标注解析、线程调度与主题回调,兼容 Python 3.10 - 对三态复选框应用抗锯齿绘制,并保留封面合成时的透明度 - 补充注解求值及浅色、深色主题下的图标回归测试
159 lines
6.7 KiB
Python
159 lines
6.7 KiB
Python
import unittest
|
|
|
|
from PIL import Image, ImageColor
|
|
|
|
from src.tchmaterial_parser.ui.resource_tree import (
|
|
category_check_state,
|
|
draw_checkbox_image,
|
|
find_tree_node,
|
|
iter_leaf_resources,
|
|
should_check_category,
|
|
)
|
|
from src.tchmaterial_parser.ui.theme import THEME_COLORS
|
|
|
|
|
|
RESOURCE_ITEMS = {
|
|
"books": {
|
|
"display_name": "电子教材",
|
|
"children": {
|
|
"primary": {
|
|
"display_name": "小学",
|
|
"children": {
|
|
"chinese": {
|
|
"display_name": "语文",
|
|
"children": {
|
|
"book-a": {"display_name": "语文 一年级上册", "content_id": "a001"},
|
|
"book-b": {"display_name": "语文 一年级下册", "content_id": "a002"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
"middle": {
|
|
"display_name": "初中",
|
|
"children": {
|
|
"math": {
|
|
"display_name": "数学",
|
|
"children": {
|
|
"book-c": {"display_name": "数学 七年级上册", "content_id": "a003"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
class IterLeafResourcesTest(unittest.TestCase):
|
|
def test_yields_paths_and_data_of_all_leaf_nodes(self) -> None:
|
|
leafs = list(iter_leaf_resources(RESOURCE_ITEMS))
|
|
|
|
self.assertEqual(
|
|
[leaf_id for leaf_id, _leaf_data in leafs],
|
|
["books:primary:chinese:book-a", "books:primary:chinese:book-b", "books:middle:math:book-c"],
|
|
)
|
|
self.assertEqual(leafs[0][1]["content_id"], "a001")
|
|
|
|
def test_yields_only_leaves_under_given_parent_path(self) -> None:
|
|
leafs = list(iter_leaf_resources(RESOURCE_ITEMS["books"]["children"], "books"))
|
|
|
|
self.assertEqual([leaf_id for leaf_id, _leaf_data in leafs], ["books:primary:chinese:book-a", "books:primary:chinese:book-b", "books:middle:math:book-c"])
|
|
|
|
def test_treats_empty_children_as_leaf(self) -> None: # 与 count_resource_items 的口径一致:children 为空字典的节点视为末级资源
|
|
self.assertEqual(list(iter_leaf_resources({})), [])
|
|
leafs = list(iter_leaf_resources({"empty": {"display_name": "空分类", "children": {}}}, "books"))
|
|
self.assertEqual([leaf_id for leaf_id, _leaf_data in leafs], ["books:empty"])
|
|
|
|
|
|
class FindTreeNodeTest(unittest.TestCase):
|
|
def test_locates_node_by_colon_path(self) -> None:
|
|
node = find_tree_node(RESOURCE_ITEMS, "books:primary:chinese:book-b")
|
|
|
|
self.assertEqual(node["content_id"], "a002")
|
|
|
|
def test_locates_category_and_root_nodes(self) -> None:
|
|
self.assertEqual(find_tree_node(RESOURCE_ITEMS, "books")["display_name"], "电子教材")
|
|
self.assertEqual(find_tree_node(RESOURCE_ITEMS, "books:primary:chinese")["display_name"], "语文")
|
|
|
|
def test_returns_none_for_unknown_path(self) -> None:
|
|
self.assertIsNone(find_tree_node(RESOURCE_ITEMS, "books:unknown:chinese"))
|
|
self.assertIsNone(find_tree_node(RESOURCE_ITEMS, "not-exist"))
|
|
|
|
|
|
class CategoryCheckStateTest(unittest.TestCase):
|
|
LEAF_IDS = ["books:primary:chinese:book-a", "books:primary:chinese:book-b", "books:middle:math:book-c"]
|
|
|
|
def test_returns_unchecked_when_no_leaf_is_checked(self) -> None:
|
|
self.assertEqual(category_check_state(self.LEAF_IDS, set()), "unchecked")
|
|
self.assertEqual(category_check_state([], {"books:middle:math:book-c"}), "unchecked")
|
|
|
|
def test_returns_partial_when_some_leaves_are_checked(self) -> None:
|
|
checked = {"books:primary:chinese:book-a"}
|
|
|
|
self.assertEqual(category_check_state(self.LEAF_IDS, checked), "partial")
|
|
|
|
def test_returns_checked_when_all_leaves_are_checked(self) -> None:
|
|
checked = set(self.LEAF_IDS)
|
|
|
|
self.assertEqual(category_check_state(self.LEAF_IDS, checked), "checked")
|
|
|
|
def test_ignores_checked_items_outside_the_category(self) -> None:
|
|
checked = {"books:primary:chinese:book-a", "books:primary:chinese:book-b", "books:other:book-x"}
|
|
|
|
self.assertEqual(category_check_state(self.LEAF_IDS[:2], checked), "checked")
|
|
|
|
|
|
class ShouldCheckCategoryTest(unittest.TestCase):
|
|
LEAF_IDS = ["book-a", "book-b"]
|
|
|
|
def test_checks_when_not_all_leaves_are_checked(self) -> None:
|
|
self.assertTrue(should_check_category(self.LEAF_IDS, set()))
|
|
self.assertTrue(should_check_category(self.LEAF_IDS, {"book-a"}))
|
|
|
|
def test_unchecks_when_all_leaves_are_already_checked(self) -> None:
|
|
self.assertFalse(should_check_category(self.LEAF_IDS, {"book-a", "book-b"}))
|
|
|
|
|
|
class DrawCheckboxImageTest(unittest.TestCase):
|
|
def test_draws_all_states_on_transparent_canvas(self) -> None:
|
|
colors = THEME_COLORS["light"]
|
|
|
|
for state in ("checked", "partial", "unchecked"):
|
|
with self.subTest(state=state):
|
|
image = draw_checkbox_image(18, state, colors)
|
|
|
|
self.assertIsInstance(image, Image.Image)
|
|
self.assertEqual(image.size, (18, 18))
|
|
self.assertEqual(image.mode, "RGBA")
|
|
# 中心区域被复选框底色填充(不透明)
|
|
self.assertEqual(image.getpixel((9, 9))[3], 255)
|
|
|
|
def test_edges_are_antialiased_in_both_themes_at_common_scales(self) -> None:
|
|
for theme_name, colors in THEME_COLORS.items():
|
|
for size in (18, 23, 27, 36):
|
|
for state in ("checked", "partial", "unchecked"):
|
|
with self.subTest(theme=theme_name, size=size, state=state):
|
|
image = draw_checkbox_image(size, state, colors)
|
|
|
|
self.assertEqual(image.size, (size, size))
|
|
self.assertEqual(image.getpixel((0, 0))[3], 0)
|
|
self.assertGreater(sum(image.getchannel("A").histogram()[1:255]), 0)
|
|
|
|
def test_checkmark_has_smooth_color_transitions(self) -> None:
|
|
for theme_name, colors in THEME_COLORS.items():
|
|
with self.subTest(theme=theme_name):
|
|
image = draw_checkbox_image(27, "checked", colors)
|
|
background = ImageColor.getrgb(colors["selbg"])
|
|
foreground = ImageColor.getrgb(colors["selfg"])
|
|
# 检查内部对勾的颜色过渡,避免仅外框抗锯齿而对勾仍有锯齿。
|
|
interior = image.crop((6, 6, 21, 21))
|
|
pixels = (interior.getpixel((x, y)) for y in range(interior.height) for x in range(interior.width))
|
|
self.assertTrue(any(
|
|
alpha == 255 and all(low < channel < high for channel, low, high in zip((r, g, b), background, foreground))
|
|
for r, g, b, alpha in pixels
|
|
))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|