feat: 实现字体主题系统和东亚字体支持
实现完整的字体主题系统,支持可复用字体配置、预设类别和扩展属性。 同时修复中文字体渲染问题,确保 Source Han Sans 等东亚字体正确显示。 核心功能: - 字体主题配置:metadata.fonts 和 fonts_default - 三种引用方式:整体引用、继承覆盖、独立定义 - 预设字体类别:sans、serif、mono、cjk-sans、cjk-serif - 扩展字体属性:family、underline、strikethrough、line_spacing、 space_before、space_after、baseline、caps - 表格字体字段:font 和 header_font 替代旧的 style.font_size - 引用循环检测和属性继承链 - 模板字体继承支持 东亚字体修复: - 添加 _set_font_with_eastasian() 方法 - 同时设置拉丁字体、东亚字体和复杂脚本字体 - 修复中文字符使用默认字体的问题 测试: - 58 个单元测试覆盖所有字体系统功能 - 3 个集成测试验证端到端场景 - 移除旧语法相关测试 文档: - 更新 README.md 添加字体主题系统使用说明 - 更新 README_DEV.md 添加技术文档 - 创建 4 个示例 YAML 文件 - 同步 delta specs 到主 specs 归档: - 归档 font-theme-system 变更到 openspec/changes/archive/ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
649
tests/unit/test_font_system.py
Normal file
649
tests/unit/test_font_system.py
Normal file
@@ -0,0 +1,649 @@
|
||||
"""
|
||||
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 # 当前定义
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -382,47 +382,6 @@ class TestRenderShape:
|
||||
class TestRenderTable:
|
||||
"""_render_table 方法测试类"""
|
||||
|
||||
@patch("renderers.pptx_renderer.PptxPresentation")
|
||||
def test_render_table(self, mock_prs_class):
|
||||
"""测试渲染表格"""
|
||||
mock_slide = self._setup_mock_slide_for_table()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = TableElement(
|
||||
position=[1, 1],
|
||||
col_widths=[2, 2, 2],
|
||||
data=[["A", "B", "C"], ["1", "2", "3"]],
|
||||
style={"font_size": 14},
|
||||
)
|
||||
|
||||
gen._render_table(mock_slide, elem)
|
||||
|
||||
# 验证添加了表格
|
||||
mock_slide.shapes.add_table.assert_called_once()
|
||||
|
||||
@patch("renderers.pptx_renderer.PptxPresentation")
|
||||
def test_render_table_with_header_style(self, mock_prs_class):
|
||||
"""测试带表头样式的表格"""
|
||||
mock_slide = self._setup_mock_slide_for_table()
|
||||
mock_prs_class.return_value = Mock()
|
||||
mock_prs_class.return_value.slide_layouts = [None] * 7 + [Mock()]
|
||||
mock_prs_class.return_value.slides.add_slide.return_value = mock_slide
|
||||
|
||||
gen = PptxGenerator()
|
||||
elem = TableElement(
|
||||
position=[1, 1],
|
||||
col_widths=[2, 2],
|
||||
data=[["H1", "H2"], ["D1", "D2"]],
|
||||
style={"font_size": 14, "header_bg": "#4a90e2", "header_color": "#ffffff"},
|
||||
)
|
||||
|
||||
gen._render_table(mock_slide, elem)
|
||||
|
||||
mock_slide.shapes.add_table.assert_called_once()
|
||||
|
||||
@patch("renderers.pptx_renderer.PptxPresentation")
|
||||
def test_render_table_col_widths_mismatch(self, mock_prs_class):
|
||||
"""测试列宽不匹配"""
|
||||
@@ -436,7 +395,6 @@ class TestRenderTable:
|
||||
position=[1, 1],
|
||||
col_widths=[2, 3, 4], # 3 列
|
||||
data=[["A", "B"]], # 2 列数据
|
||||
style={},
|
||||
)
|
||||
|
||||
with pytest.raises(YAMLError, match="列宽数量"):
|
||||
|
||||
Reference in New Issue
Block a user