""" FontConfig 和 FontResolver 单元测试 本测试文件包含字体主题系统的完整单元测试,覆盖以下功能: 1. FontConfig 数据类 - 所有字体属性的创建和访问 - baseline 和 caps 枚举值验证 2. 预设字体类别映射 - 五种预设类别(sans、serif、mono、cjk-sans、cjk-serif) - 映射到跨平台通用字体 3. FontResolver 字体引用 - 整体引用(@xxx) - 引用不存在时的错误处理 - None 值处理和 fonts_default 使用 4. FontResolver 继承覆盖 - parent 字段继承 - 独立定义 - 引用错误处理 5. FontResolver 属性继承链 - parent → 当前 → fonts_default → 系统默认 - 多层继承 - 覆盖优先级 6. FontResolver 引用循环检测 - 直接循环引用(自引用) - 间接循环引用 - 深度限制 7. 预设字体类别解析 - 在 family 字段中识别预设类别 - 与 fonts 配置结合 - 与继承结合 8. 表格字体字段 - font 和 header_font 字段支持 - 数据结构验证 9. 扩展字体属性 - family、underline、strikethrough - 属性继承 10. 段落属性 - line_spacing、space_before、space_after - 属性继承 11. baseline 和 caps 属性 - 枚举值验证 - 属性继承 注意: - 旧语法测试(style.font_size、style.header_color)已移除 - 新语法使用 font 和 header_font 字段替代 - 所有测试使用 FontConfig 和 FontResolver 进行字体配置 """ import pytest from core.elements import FontConfig from utils.font_resolver import FontResolver, PRESET_FONT_MAPPING class TestFontConfig: """FontConfig 数据类测试""" def test_create_font_config_with_all_properties(self): """测试创建包含所有属性的 FontConfig""" config = FontConfig( parent="@base", family="Arial", size=24, bold=True, italic=False, underline=True, strikethrough=False, color="#333333", align="center", line_spacing=1.5, space_before=12, space_after=6, baseline="normal", caps="allcaps" ) assert config.parent == "@base" assert config.family == "Arial" assert config.size == 24 assert config.bold is True assert config.italic is False assert config.underline is True assert config.strikethrough is False assert config.color == "#333333" assert config.align == "center" assert config.line_spacing == 1.5 assert config.space_before == 12 assert config.space_after == 6 assert config.baseline == "normal" assert config.caps == "allcaps" def test_create_font_config_with_minimal_properties(self): """测试创建最小属性的 FontConfig""" config = FontConfig(size=18) assert config.size == 18 assert config.family is None assert config.bold is None assert config.color is None def test_baseline_validation_normal(self): """测试 baseline 为 normal 时验证通过""" config = FontConfig(baseline="normal") assert config.baseline == "normal" def test_baseline_validation_superscript(self): """测试 baseline 为 superscript 时验证通过""" config = FontConfig(baseline="superscript") assert config.baseline == "superscript" def test_baseline_validation_subscript(self): """测试 baseline 为 subscript 时验证通过""" config = FontConfig(baseline="subscript") assert config.baseline == "subscript" def test_baseline_validation_invalid_raises_error(self): """测试 baseline 为无效值时抛出错误""" with pytest.raises(ValueError, match="baseline 必须是"): FontConfig(baseline="invalid") def test_caps_validation_normal(self): """测试 caps 为 normal 时验证通过""" config = FontConfig(caps="normal") assert config.caps == "normal" def test_caps_validation_allcaps(self): """测试 caps 为 allcaps 时验证通过""" config = FontConfig(caps="allcaps") assert config.caps == "allcaps" def test_caps_validation_smallcaps(self): """测试 caps 为 smallcaps 时验证通过""" config = FontConfig(caps="smallcaps") assert config.caps == "smallcaps" def test_caps_validation_invalid_raises_error(self): """测试 caps 为无效值时抛出错误""" with pytest.raises(ValueError, match="caps 必须是"): FontConfig(caps="invalid") class TestPresetFontMapping: """预设字体类别映射测试""" def test_preset_mapping_sans(self): """测试 sans 映射到 Arial""" assert PRESET_FONT_MAPPING["sans"] == "Arial" def test_preset_mapping_serif(self): """测试 serif 映射到 Times New Roman""" assert PRESET_FONT_MAPPING["serif"] == "Times New Roman" def test_preset_mapping_mono(self): """测试 mono 映射到 Courier New""" assert PRESET_FONT_MAPPING["mono"] == "Courier New" def test_preset_mapping_cjk_sans(self): """测试 cjk-sans 映射到 Microsoft YaHei""" assert PRESET_FONT_MAPPING["cjk-sans"] == "Microsoft YaHei" def test_preset_mapping_cjk_serif(self): """测试 cjk-serif 映射到 SimSun""" assert PRESET_FONT_MAPPING["cjk-serif"] == "SimSun" class TestFontResolverReference: """FontResolver 整体引用功能测试""" def test_resolve_string_reference(self): """测试整体引用字符串格式""" fonts = { "title": {"family": "Arial", "size": 44, "bold": True} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@title") assert result.family == "Arial" assert result.size == 44 assert result.bold is True def test_resolve_reference_not_found_raises_error(self): """测试引用不存在的字体配置时抛出错误""" fonts = {"title": {"size": 44}} resolver = FontResolver(fonts=fonts) with pytest.raises(ValueError, match="引用的字体配置不存在"): resolver.resolve_font("@nonexistent") def test_resolve_reference_without_at_raises_error(self): """测试引用格式不正确时抛出错误""" resolver = FontResolver(fonts={}) with pytest.raises(ValueError, match="字体引用必须以 @ 开头"): resolver.resolve_font("title") def test_resolve_none_returns_empty_config(self): """测试解析 None 返回空配置""" resolver = FontResolver(fonts={}) result = resolver.resolve_font(None) assert isinstance(result, FontConfig) assert result.family is None assert result.size is None def test_resolve_none_with_default_uses_default(self): """测试解析 None 时使用 fonts_default""" fonts = { "body": {"family": "Arial", "size": 18} } resolver = FontResolver(fonts=fonts, fonts_default="@body") result = resolver.resolve_font(None) assert result.family == "Arial" assert result.size == 18 class TestFontResolverInheritance: """FontResolver 继承覆盖功能测试""" def test_resolve_dict_with_parent(self): """测试字典形式的 parent 继承""" fonts = { "title": {"family": "Arial", "size": 44, "bold": True} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font({"parent": "@title", "size": 60}) assert result.family == "Arial" # 继承 assert result.size == 60 # 覆盖 assert result.bold is True # 继承 def test_resolve_dict_without_parent(self): """测试字典形式的独立定义""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"family": "SimSun", "size": 24}) assert result.family == "SimSun" assert result.size == 24 def test_parent_reference_not_found_raises_error(self): """测试 parent 引用不存在时抛出错误""" resolver = FontResolver(fonts={}) with pytest.raises(ValueError, match="引用的字体配置不存在"): resolver.resolve_font({"parent": "@nonexistent"}) def test_parent_without_at_raises_error(self): """测试 parent 格式不正确时抛出错误""" fonts = {"title": {"size": 44}} resolver = FontResolver(fonts=fonts) with pytest.raises(ValueError, match="字体引用必须以 @ 开头"): resolver.resolve_font({"parent": "title"}) class TestFontResolverInheritanceChain: """FontResolver 属性继承链测试""" def test_inheritance_chain_parent_to_current(self): """测试 parent → 当前 的继承链""" fonts = { "base": {"family": "Arial", "size": 18, "color": "#333333"}, "title": {"parent": "@base", "size": 44, "bold": True} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@title") assert result.family == "Arial" # 从 base 继承 assert result.size == 44 # title 覆盖 assert result.color == "#333333" # 从 base 继承 assert result.bold is True # title 定义 def test_inheritance_chain_with_fonts_default(self): """测试继承链包含 fonts_default""" fonts = { "body": {"family": "Arial", "size": 18, "color": "#666666"} } resolver = FontResolver(fonts=fonts, fonts_default="@body") result = resolver.resolve_font({"bold": True}) assert result.family == "Arial" # 从 fonts_default 继承 assert result.size == 18 # 从 fonts_default 继承 assert result.color == "#666666" # 从 fonts_default 继承 assert result.bold is True # 当前定义 def test_inheritance_chain_current_overrides_default(self): """测试当前定义覆盖 fonts_default""" fonts = { "body": {"family": "Arial", "size": 18} } resolver = FontResolver(fonts=fonts, fonts_default="@body") result = resolver.resolve_font({"family": "SimSun", "size": 24}) assert result.family == "SimSun" # 当前覆盖 assert result.size == 24 # 当前覆盖 def test_multi_level_parent_inheritance(self): """测试多层 parent 继承""" fonts = { "base": {"family": "Arial", "size": 18}, "heading": {"parent": "@base", "bold": True}, "title": {"parent": "@heading", "size": 44} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@title") assert result.family == "Arial" # 从 base 继承 assert result.bold is True # 从 heading 继承 assert result.size == 44 # title 覆盖 class TestFontResolverCircularReference: """FontResolver 引用循环检测测试""" def test_direct_circular_reference_raises_error(self): """测试直接循环引用(自引用)""" fonts = { "title": {"parent": "@title", "size": 44} } resolver = FontResolver(fonts=fonts) with pytest.raises(ValueError, match="检测到字体引用循环"): resolver.resolve_font("@title") def test_indirect_circular_reference_raises_error(self): """测试间接循环引用""" fonts = { "a": {"parent": "@b", "size": 44}, "b": {"parent": "@a", "size": 18} } resolver = FontResolver(fonts=fonts) with pytest.raises(ValueError, match="检测到字体引用循环"): resolver.resolve_font("@a") def test_three_way_circular_reference_raises_error(self): """测试三方循环引用""" fonts = { "a": {"parent": "@b"}, "b": {"parent": "@c"}, "c": {"parent": "@a"} } resolver = FontResolver(fonts=fonts) with pytest.raises(ValueError, match="检测到字体引用循环"): resolver.resolve_font("@a") def test_max_depth_exceeded_raises_error(self): """测试引用深度超限""" # 创建一个很深的继承链 fonts = {} for i in range(15): if i == 0: fonts[f"level{i}"] = {"size": 18} else: fonts[f"level{i}"] = {"parent": f"@level{i-1}"} resolver = FontResolver(fonts=fonts) with pytest.raises(ValueError, match="引用深度超过限制"): resolver.resolve_font("@level14") class TestFontResolverPresetCategories: """FontResolver 预设字体类别解析测试""" def test_resolve_preset_sans(self): """测试解析 sans 预设类别""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"family": "sans", "size": 18}) assert result.family == "Arial" assert result.size == 18 def test_resolve_preset_serif(self): """测试解析 serif 预设类别""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"family": "serif", "size": 18}) assert result.family == "Times New Roman" def test_resolve_preset_mono(self): """测试解析 mono 预设类别""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"family": "mono", "size": 18}) assert result.family == "Courier New" def test_resolve_preset_cjk_sans(self): """测试解析 cjk-sans 预设类别""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"family": "cjk-sans", "size": 18}) assert result.family == "Microsoft YaHei" def test_resolve_preset_cjk_serif(self): """测试解析 cjk-serif 预设类别""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"family": "cjk-serif", "size": 18}) assert result.family == "SimSun" def test_resolve_preset_in_fonts_definition(self): """测试在 fonts 配置中使用预设类别""" fonts = { "body": {"family": "sans", "size": 18} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@body") assert result.family == "Arial" # sans 被解析为 Arial def test_resolve_preset_with_inheritance(self): """测试预设类别与继承结合""" fonts = { "base": {"family": "cjk-sans", "size": 18}, "title": {"parent": "@base", "size": 44} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@title") assert result.family == "Microsoft YaHei" # 从 base 继承并解析预设 assert result.size == 44 class TestTableFontFields: """表格字体字段测试""" def test_table_with_font_field(self): """测试表格 font 字段""" from core.elements import TableElement elem = TableElement( position=[1, 1], col_widths=[2, 2], data=[["A", "B"], ["C", "D"]], font={"family": "Arial", "size": 14} ) assert elem.font is not None assert isinstance(elem.font, dict) def test_table_with_header_font_field(self): """测试表格 header_font 字段""" from core.elements import TableElement elem = TableElement( position=[1, 1], col_widths=[2, 2], data=[["A", "B"], ["C", "D"]], header_font={"bold": True, "color": "#ffffff"} ) assert elem.header_font is not None assert isinstance(elem.header_font, dict) def test_table_with_both_font_fields(self): """测试表格同时定义 font 和 header_font""" from core.elements import TableElement elem = TableElement( position=[1, 1], col_widths=[2, 2], data=[["A", "B"], ["C", "D"]], font={"size": 14}, header_font={"bold": True} ) assert elem.font is not None assert elem.header_font is not None def test_table_header_font_inherits_from_font(self): """测试表格 header_font 继承 font(在渲染器中实现)""" # 这个测试验证数据结构支持,实际继承逻辑在渲染器中 from core.elements import TableElement elem = TableElement( position=[1, 1], col_widths=[2, 2], data=[["A", "B"], ["C", "D"]], font={"family": "Arial", "size": 14}, header_font=None # 未定义,应该继承 font ) assert elem.font is not None assert elem.header_font is None # 数据结构层面为 None,渲染器会处理继承 class TestExtendedFontProperties: """扩展字体属性测试""" def test_font_family_property(self): """测试 family 属性""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"family": "SimSun", "size": 18}) assert result.family == "SimSun" def test_font_underline_property(self): """测试 underline 属性""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"underline": True}) assert result.underline is True def test_font_strikethrough_property(self): """测试 strikethrough 属性""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"strikethrough": True}) assert result.strikethrough is True def test_extended_properties_with_inheritance(self): """测试扩展属性继承""" fonts = { "base": {"family": "Arial", "underline": True}, "derived": {"parent": "@base", "strikethrough": True} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@derived") assert result.family == "Arial" # 继承 assert result.underline is True # 继承 assert result.strikethrough is True # 当前定义 class TestParagraphProperties: """段落属性测试""" def test_line_spacing_property(self): """测试 line_spacing 属性""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"line_spacing": 1.5}) assert result.line_spacing == 1.5 def test_space_before_property(self): """测试 space_before 属性""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"space_before": 12}) assert result.space_before == 12 def test_space_after_property(self): """测试 space_after 属性""" resolver = FontResolver(fonts={}) result = resolver.resolve_font({"space_after": 6}) assert result.space_after == 6 def test_paragraph_properties_with_inheritance(self): """测试段落属性继承""" fonts = { "base": {"line_spacing": 1.5, "space_before": 12}, "derived": {"parent": "@base", "space_after": 6} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@derived") assert result.line_spacing == 1.5 # 继承 assert result.space_before == 12 # 继承 assert result.space_after == 6 # 当前定义 class TestBaselineAndCapsProperties: """baseline 和 caps 属性测试""" def test_baseline_normal(self): """测试 baseline 为 normal""" config = FontConfig(baseline="normal") assert config.baseline == "normal" def test_baseline_superscript(self): """测试 baseline 为 superscript""" config = FontConfig(baseline="superscript") assert config.baseline == "superscript" def test_baseline_subscript(self): """测试 baseline 为 subscript""" config = FontConfig(baseline="subscript") assert config.baseline == "subscript" def test_caps_normal(self): """测试 caps 为 normal""" config = FontConfig(caps="normal") assert config.caps == "normal" def test_caps_allcaps(self): """测试 caps 为 allcaps""" config = FontConfig(caps="allcaps") assert config.caps == "allcaps" def test_caps_smallcaps(self): """测试 caps 为 smallcaps""" config = FontConfig(caps="smallcaps") assert config.caps == "smallcaps" def test_baseline_caps_with_inheritance(self): """测试 baseline 和 caps 属性继承""" fonts = { "base": {"baseline": "superscript", "caps": "allcaps"}, "derived": {"parent": "@base", "size": 18} } resolver = FontResolver(fonts=fonts) result = resolver.resolve_font("@derived") assert result.baseline == "superscript" # 继承 assert result.caps == "allcaps" # 继承 assert result.size == 18 # 当前定义