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

547 lines
19 KiB
Python

from templates.v2.models.layouts import RawSlideLayout
from templates.v2.schema import (
extract_slide_schema_from_layout,
get_component_schema,
get_template_schema,
)
def test_extract_slide_schema_from_layout_extracts_editable_content():
layout = RawSlideLayout.model_validate(
{
"id": "content_slide",
"description": "Editable content with static decoration.",
"elements": [
{
"type": "rectangle",
"fill": {"color": "#ffffff"},
},
{
"type": "text",
"decorative": False,
"name": "title",
"min_length": 4,
"max_length": 8,
"runs": [{"text": "Title"}],
},
{
"type": "text",
"decorative": True,
"name": "static_label",
"min_length": 1,
"max_length": 2,
"runs": [{"text": "A"}],
},
{
"type": "image",
"decorative": False,
"name": "hero_image",
"data": "/app_data/images/hero.png",
"is_icon": False,
},
{
"type": "container",
"child": {
"type": "text",
"decorative": False,
"name": "caption",
"min_length": 2,
"max_length": 4,
"runs": [{"text": "Caption"}],
},
},
{
"type": "group",
"name": "details",
"children": [
{
"type": "text-list",
"decorative": False,
"name": "bullets",
"min_items": 2,
"max_items": 4,
"min_item_length": 5,
"max_item_length": 10,
"items": [[{"text": "First bullet"}]],
},
{
"type": "chart",
"decorative": False,
"name": "chart",
"chart_type": "bar",
"categories": ["Q1", "Q2"],
"series": [{"name": "Revenue", "values": [10, 12]}],
},
],
},
],
}
)
assert extract_slide_schema_from_layout(layout) == {
"type": "object",
"properties": {
"title": {"type": "string", "minLength": 4, "maxLength": 8},
"hero_image": {
"type": "object",
"properties": {"prompt": {"type": "string"}},
"required": ["prompt"],
"additionalProperties": False,
},
"caption": {"type": "string", "minLength": 2, "maxLength": 4},
"details": {
"type": "object",
"properties": {
"bullets": {
"type": "array",
"minItems": 2,
"maxItems": 4,
"items": {
"type": "string",
"minLength": 5,
"maxLength": 10,
},
},
"chart": {
"type": "object",
"additionalProperties": False,
"properties": {
"title": {"type": ["string", "null"]},
"title_color": {"type": ["string", "null"]},
"categories": {
"type": "array",
"items": {"type": "string"},
"maxItems": 24,
},
"series": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": False,
"properties": {
"name": {"type": "string"},
"values": {
"type": "array",
"items": {"type": "number"},
"maxItems": 24,
},
},
"required": ["name", "values"],
},
"maxItems": 12,
},
},
"required": ["categories", "series"],
},
},
"required": ["bullets", "chart"],
"additionalProperties": False,
},
},
"required": ["title", "hero_image", "caption", "details"],
"additionalProperties": False,
}
def test_extract_slide_schema_from_layout_collapses_repeated_children_to_array():
layout = RawSlideLayout.model_validate(
{
"id": "cards_slide",
"description": "Repeated card layout.",
"elements": [
{
"type": "flex",
"name": "cards",
"position": {"x": 0, "y": 0},
"size": {"width": 1280, "height": 240},
"direction": "row",
"min_children": 2,
"max_children": 4,
"children": [
{
"type": "group",
"name": "card_1",
"children": [
{
"type": "text",
"decorative": False,
"name": "title_1",
"min_length": 3,
"max_length": 6,
"runs": [{"text": "One"}],
},
{
"type": "image",
"decorative": False,
"name": "icon_1",
"data": "/app_data/icons/icon-1.svg",
"is_icon": True,
},
],
},
{
"type": "group",
"name": "card_2",
"children": [
{
"type": "text",
"decorative": False,
"name": "title_2",
"min_length": 3,
"max_length": 6,
"runs": [{"text": "Two"}],
},
{
"type": "image",
"decorative": False,
"name": "icon_2",
"data": "/app_data/icons/icon-2.svg",
"is_icon": True,
},
],
},
],
}
],
}
)
assert extract_slide_schema_from_layout(layout) == {
"type": "object",
"properties": {
"cards": {
"type": "array",
"minItems": 2,
"maxItems": 4,
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"minLength": 3,
"maxLength": 6,
},
"icon": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"],
"additionalProperties": False,
},
},
"required": ["title", "icon"],
"additionalProperties": False,
},
}
},
"required": ["cards"],
"additionalProperties": False,
}
def test_get_component_schema_extracts_generated_component_content():
component = {
"id": "feature_card",
"description": "Reusable feature card component.",
"elements": [
{
"type": "text",
"decorative": False,
"name": "headline",
"min_length": 4,
"max_length": 12,
},
{
"type": "image",
"decorative": False,
"name": "icon",
"data": "/app_data/icons/icon.svg",
"is_icon": True,
},
{
"type": "table",
"decorative": False,
"name": "metrics",
"min_columns": 2,
"max_columns": 4,
"min_rows": 1,
"max_rows": 3,
},
{
"type": "chart",
"decorative": False,
"name": "trend",
"chart_type": "line",
"categories": ["Q1", "Q2"],
"series": [{"name": "Revenue", "values": [10, 12]}],
},
],
}
assert get_component_schema(component) == {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "feature_card",
"description": "Reusable feature card component.",
"additionalProperties": False,
"properties": {
"headline": {
"type": "string",
"minLength": 4,
"maxLength": 12,
"title": "Headline",
"x-element-type": "text",
"x-element-path": "elements.0",
},
"icon": {
"type": "object",
"additionalProperties": False,
"properties": {
"icon_query": {
"type": "string",
"description": "Search query for the replacement icon.",
}
},
"required": ["icon_query"],
"title": "Icon",
"x-element-type": "image",
"x-element-path": "elements.1",
},
"metrics": {
"type": "object",
"additionalProperties": False,
"properties": {
"columns": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
"maxItems": 4,
},
"rows": {
"type": "array",
"items": {
"type": "array",
"items": {"type": "string"},
"minItems": 2,
"maxItems": 4,
},
"minItems": 1,
"maxItems": 3,
},
},
"required": ["columns", "rows"],
"title": "Metrics",
"x-element-type": "table",
"x-element-path": "elements.2",
},
"trend": {
"type": "object",
"additionalProperties": False,
"properties": {
"title": {"type": ["string", "null"]},
"title_color": {"type": ["string", "null"]},
"categories": {
"type": "array",
"items": {"type": "string"},
"maxItems": 24,
},
"series": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": False,
"properties": {
"name": {"type": "string"},
"values": {
"type": "array",
"items": {"type": "number"},
"maxItems": 24,
},
},
"required": ["name", "values"],
},
"maxItems": 12,
},
},
"required": ["categories", "series"],
"title": "Trend",
"x-element-type": "chart",
"x-element-path": "elements.3",
},
},
"required": ["headline", "icon", "metrics", "trend"],
}
def test_get_component_schema_collapses_repeated_component_children_to_array():
component = {
"id": "card_grid",
"description": "Reusable card grid component.",
"elements": [
{
"type": "grid",
"name": "cards",
"min_children": 2,
"max_children": 4,
"children": [
{
"type": "group",
"name": "card_1",
"children": [
{
"type": "text",
"decorative": False,
"name": "title_1",
"min_length": 3,
"max_length": 8,
}
],
},
{
"type": "group",
"name": "card_2",
"children": [
{
"type": "text",
"decorative": False,
"name": "title_2",
"min_length": 5,
"max_length": 12,
}
],
},
],
}
],
}
schema = get_component_schema(component)
assert schema is not None
assert schema["properties"]["cards"] == {
"type": "array",
"minItems": 2,
"maxItems": 4,
"items": {
"type": "object",
"additionalProperties": False,
"properties": {
"title": {
"type": "string",
"minLength": 3,
"maxLength": 12,
"title": "Title",
"x-element-type": "text",
}
},
"required": ["title"],
},
}
def test_get_template_schema_strips_component_metadata():
template = {
"layouts": [
{
"id": "intro",
"description": "Intro slide.",
"components": [
{
"id": "hero",
"description": "Hero image component.",
"elements": [
{
"type": "image",
"decorative": False,
"name": "photo",
"data": "/app_data/images/photo.png",
"is_icon": False,
}
],
}
],
}
]
}
assert get_template_schema(template) == {
"source_file": "template.json",
"layout_count": 1,
"layouts": [
{
"slide": 1,
"layout_id": "intro",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"title": "intro",
"description": "Intro slide.",
"additionalProperties": False,
"properties": {
"hero": {
"type": "object",
"additionalProperties": False,
"properties": {
"photo": {
"type": "object",
"additionalProperties": False,
"properties": {
"image_prompt": {"type": "string"}
},
"required": ["image_prompt"],
}
},
"required": ["photo"],
}
},
"required": ["hero"],
},
}
],
}
def test_get_template_schema_numbers_duplicate_component_fields_from_zero():
template = {
"layouts": [
{
"id": "comparison",
"description": "Comparison slide.",
"components": [
{
"id": "metric_card",
"description": "Metric card component.",
"elements": [
{
"type": "text",
"decorative": False,
"name": "value",
"min_length": 1,
"max_length": 8,
}
],
},
{
"id": "metric_card",
"description": "Metric card component.",
"elements": [
{
"type": "text",
"decorative": False,
"name": "value",
"min_length": 1,
"max_length": 8,
}
],
},
],
}
]
}
schema = get_template_schema(template)["layouts"][0]["schema"]
assert schema is not None
assert list(schema["properties"]) == ["metric_card_0", "metric_card_1"]
assert schema["required"] == ["metric_card_0", "metric_card_1"]
assert schema["properties"]["metric_card_0"] == schema["properties"]["metric_card_1"]