Files
2026-07-13 13:08:41 +08:00

400 lines
12 KiB
Python

import pytest
from pydantic import ValidationError
from templates.v2.models.elements import (
Chart,
Container,
Image,
Infographic,
Table,
Text,
TextList,
)
from templates.v2.models.layouts import RawSlideLayout
def test_chart_accepts_legacy_boolean_data_labels():
chart = Chart.model_validate(
{
"type": "chart",
"decorative": False,
"name": "legacy_chart",
"chart_type": "bar",
"data_labels": True,
}
)
assert chart.data_labels is not None
assert chart.data_labels.value == "top"
hidden = Chart.model_validate(
{
"type": "chart",
"decorative": False,
"name": "legacy_chart",
"chart_type": "bar",
"data_labels": False,
}
)
assert hidden.data_labels is None
layout = RawSlideLayout.model_validate(
{
"id": "legacy_chart_data_labels",
"description": "Layout with legacy boolean chart data labels.",
"elements": [
{
"type": "chart",
"decorative": False,
"name": "legacy_chart",
"chart_type": "bar",
"data_labels": True,
}
],
}
)
layout_chart = layout.elements[0]
assert isinstance(layout_chart, Chart)
assert layout_chart.data_labels is not None
assert layout_chart.data_labels.value == "top"
def test_image_element_accepts_flip_flags():
image = Image.model_validate(
{
"type": "image",
"decorative": False,
"name": "hero",
"is_icon": False,
"data": "/app_data/images/hero.png",
"flip_h": True,
"flip_v": False,
"focus_x": 20.0,
"focus_y": 75.0,
"crop_scale": 1.8,
"clip_path": "path('M 0 0 L 100 0 L 100 100 Z')",
}
)
assert image.flip_h is True
assert image.flip_v is False
assert image.focus_x == 20.0
assert image.focus_y == 75.0
assert image.crop_scale == 1.8
assert image.clip_path == "path('M 0 0 L 100 0 L 100 100 Z')"
layout = RawSlideLayout.model_validate(
{
"id": "flipped_image_slide",
"description": "Layout with a flipped image.",
"elements": [
{
"type": "image",
"decorative": False,
"name": "hero",
"is_icon": False,
"data": "/app_data/images/hero.png",
"flip_h": True,
"flip_v": False,
"focus_x": 20.0,
"focus_y": 75.0,
"crop_scale": 1.8,
"clip_path": "path('M 0 0 L 100 0 L 100 100 Z')",
}
],
}
)
layout_image = layout.elements[0]
assert layout_image.flip_h is True
assert layout_image.flip_v is False
assert layout_image.focus_x == 20.0
assert layout_image.focus_y == 75.0
assert layout_image.crop_scale == 1.8
assert layout_image.clip_path == "path('M 0 0 L 100 0 L 100 100 Z')"
assert (
layout.model_dump(mode="json")["elements"][0]["clip_path"]
== "path('M 0 0 L 100 0 L 100 100 Z')"
)
def test_element_models_match_export_schema_changes():
assert "decorative" not in Container.model_fields
assert not {
"data",
"color",
"label_color",
"show_values",
}.intersection(Chart.model_fields)
assert "axis_color" in Chart.model_fields
assert "title_color" in Chart.model_fields
assert "legend" in Chart.model_fields
assert {"base_color", "highlight_color"}.issubset(Infographic.model_fields)
chart = Chart.model_validate(
{
"type": "chart",
"decorative": False,
"name": "model_chart",
"chart_type": "bar",
"title": "Revenue",
"title_color": "#102030",
}
)
assert chart.title_color == "#102030"
with pytest.raises(ValidationError, match="runs"):
Text.model_validate(
{
"type": "text",
"decorative": False,
"name": "title",
"min_length": 1,
"max_length": 10,
}
)
with pytest.raises(ValidationError, match="items"):
TextList.model_validate(
{
"type": "text-list",
"decorative": False,
"name": "bullets",
"min_items": 1,
"max_items": 2,
"min_item_length": 1,
"max_item_length": 10,
}
)
with pytest.raises(ValidationError, match="data"):
Image.model_validate(
{
"type": "image",
"decorative": False,
"name": "hero",
"is_icon": False,
}
)
table = Table.model_validate(
{
"type": "table",
"decorative": False,
"name": "metrics",
"columns": [
{"runs": [{"text": "Metric"}]},
{"runs": [{"text": "Value"}]},
],
"rows": [
[
{"runs": [{"text": "Revenue"}]},
{"runs": [{"text": "$12M"}]},
]
],
"min_columns": 1,
"max_columns": 2,
"min_rows": 1,
"max_rows": 1,
}
)
assert [cell.runs[0].text for cell in table.columns] == ["Metric", "Value"]
with pytest.raises(ValidationError):
Table.model_validate(
{
"type": "table",
"decorative": False,
"name": "metrics",
"columns": [{"text": "Metric"}],
"rows": [["Revenue"]],
"min_columns": 1,
"max_columns": 1,
"min_rows": 1,
"max_rows": 1,
}
)
infographic = Infographic.model_validate(
{
"type": "infographic",
"decorative": False,
"name": "progress",
"infographic_type": "progress_bar",
"min_value": 0,
"max_value": 100,
"value": 70,
"base_color": "E5E7EB",
"highlight_color": "2563EB",
}
)
assert infographic.type == "infographic"
assert infographic.decorative is False
assert infographic.infographic_type.value == "progress_bar"
assert infographic.base_color == "E5E7EB"
assert infographic.highlight_color == "2563EB"
with pytest.raises(ValidationError):
Infographic.model_validate(
{
"type": "infographics",
"decorative": False,
"name": "progress",
"infographics_type": "progress_bar",
"min_value": 0,
"max_value": 100,
"value": 70,
}
)
def test_chart_rejects_tiny_explicit_size():
with pytest.raises(ValidationError, match="chart size"):
Chart.model_validate(
{
"type": "chart",
"decorative": False,
"name": "model_chart",
"chart_type": "bar",
"position": {"x": 0.25, "y": 0.2},
"size": {"width": 0.6, "height": 0.5},
"categories": ["GPT OSS 20B", "GPT OSS 120B"],
"series": [{"name": "Score", "values": [20, 120]}],
}
)
chart = Chart.model_validate(
{
"type": "chart",
"decorative": False,
"name": "model_chart",
"chart_type": "bar",
"position": {"x": 0, "y": 0},
"size": {"width": 640, "height": 300},
"categories": ["GPT OSS 20B", "GPT OSS 120B"],
"series": [{"name": "Score", "values": [20, 120]}],
"legend": False,
}
)
assert chart.size.width == 640
assert chart.legend is False
def test_pie_and_donut_ignore_additional_series():
for chart_type in ("pie", "donut"):
chart = Chart(
type="chart",
decorative=False,
name="single-series-chart",
chart_type=chart_type,
colors=["#FF0000", "#00FF00"],
categories=["A", "B"],
series=[
{"name": "Used", "values": [10, 20]},
{"name": "Ignored", "values": [30, 40]},
],
)
assert chart.series is not None
assert [series.name for series in chart.series] == ["Used"]
def test_raw_layout_accepts_reference_converter_element_models():
layout = RawSlideLayout.model_validate(
{
"id": "pptx_slide",
"description": "Layout converted from a PowerPoint source slide.",
"elements": [
{
"type": "image",
"decorative": True,
"name": "background",
"is_icon": False,
"opacity": 0.42,
"data": "/app_data/images/background.png",
},
{
"type": "table",
"decorative": False,
"name": "financials",
"columns": [
{"runs": [{"text": "Metric"}]},
{"runs": [{"text": "Value"}]},
],
"rows": [
[
{"runs": [{"text": "Revenue"}]},
{"runs": [{"text": "$12M"}]},
]
],
"min_columns": 1,
"max_columns": 2,
"min_rows": 1,
"max_rows": 1,
},
{
"type": "chart",
"decorative": False,
"name": "revenue_chart",
"chart_type": "bar",
"title": "Revenue",
"colors": ["#445566", "#778899"],
"x_axis": False,
"y_axis": False,
"categories": ["Q1", "Q2"],
"series": [{"name": "Revenue", "values": [10.0, 12.0]}],
"data_labels": "top",
"x_axis_grid": True,
"y_axis_grid": False,
},
],
}
)
image, table, chart = layout.elements
assert image.opacity == 0.42
assert [cell.runs[0].text for cell in table.columns] == ["Metric", "Value"]
assert chart.colors == ["#445566", "#778899"]
assert chart.x_axis_grid is True
assert chart.y_axis_grid is False
assert chart.series[0].values == [10.0, 12.0]
def test_flow_layout_children_can_omit_geometry():
layout = RawSlideLayout.model_validate(
{
"id": "flow_slide",
"description": "Layout with flex-computed child geometry.",
"elements": [
{
"type": "flex",
"name": "cards",
"direction": "row",
"min_children": 1,
"max_children": 2,
"children": [
{
"type": "grid",
"name": "metric_grid",
"columns": 2,
"min_children": 1,
"max_children": 2,
"children": [
{
"type": "text",
"decorative": False,
"name": "metric",
"min_length": 2,
"max_length": 4,
"runs": [{"text": "42"}],
}
],
}
],
}
],
}
)
flex = layout.elements[0]
grid = flex.children[0]
assert flex.position is None
assert grid.size is None