Skip to content

Commit

Permalink
Add adaptive cards message formatting and unit tests
Browse files Browse the repository at this point in the history
- Introduced new `adaptive_cards.py` module for formatting messages as adaptive cards, including functions for various block types such as headers, lines, facts, and expandable sections.
- Implemented utility functions to format message blocks and handle different styles and colors.
- Added comprehensive unit tests for adaptive card formatting, covering various scenarios including headers, icons, text styles, and expandable blocks.
- Created fixture files for expected JSON outputs to validate the formatting logic.
MikaKerman committed Jan 27, 2025
1 parent 066e712 commit cc614cf
Showing 27 changed files with 1,454 additions and 0 deletions.
Empty file.
229 changes: 229 additions & 0 deletions elementary/messages/formats/adaptive_cards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import uuid
from typing import Any, Dict, List, Optional

from elementary.messages.blocks import (
CodeBlock,
DividerBlock,
ExpandableBlock,
FactListBlock,
HeaderBlock,
Icon,
IconBlock,
InlineBlock,
LineBlock,
LinesBlock,
LinkBlock,
TextBlock,
TextStyle,
)
from elementary.messages.message_body import Color, MessageBlock, MessageBody

ICON_TO_HTML = {
Icon.RED_TRIANGLE: "🔺",
Icon.X: "❌",
Icon.WARNING: "⚠️",
Icon.EXCLAMATION: "❗",
Icon.CHECK: "✅",
Icon.MAGNIFYING_GLASS: "🔎",
Icon.HAMMER_AND_WRENCH: "🛠️",
Icon.POLICE_LIGHT: "🚨",
Icon.INFO: "ℹ️",
Icon.EYE: "👁️",
Icon.GEAR: "⚙️",
Icon.BELL: "🔔",
}

COLOR_TO_STYLE = {
Color.RED: "Attention",
Color.YELLOW: "Warning",
Color.GREEN: "Good",
}


def format_icon(icon: Icon) -> str:
return ICON_TO_HTML[icon]


def format_text_block(block: TextBlock) -> str:
if block.style == TextStyle.BOLD:
return f"**{block.text}**"
elif block.style == TextStyle.ITALIC:
return f"_{block.text}_"
else:
return block.text


def format_inline_block(block: InlineBlock) -> str:
if isinstance(block, IconBlock):
return format_icon(block.icon)
elif isinstance(block, TextBlock):
return format_text_block(block)
elif isinstance(block, LinkBlock):
return f"[{block.text}]({block.url})"
else:
raise ValueError(f"Unsupported inline block type: {type(block)}")


def format_line_block_text(block: LineBlock) -> str:
return block.sep.join([format_inline_block(inline) for inline in block.inlines])


def format_line_block(block: LineBlock) -> Dict[str, Any]:
text = format_line_block_text(block)

return {
"type": "TextBlock",
"text": text,
"wrap": True,
}


def format_lines_block(block: LinesBlock) -> List[Dict[str, Any]]:
return [format_line_block(line_block) for line_block in block.lines]


def format_header_block(
block: HeaderBlock, color: Optional[Color] = None
) -> Dict[str, Any]:
return {
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": block.text,
"weight": "bolder",
"size": "large",
"wrap": True,
}
],
"style": COLOR_TO_STYLE[color] if color else "Default",
}


def format_code_block(block: CodeBlock) -> Dict[str, Any]:
return {
"type": "Container",
"style": "emphasis",
"items": [
{
"type": "RichTextBlock",
"inlines": [
{
"type": "TextRun",
"text": block.text,
"fontType": "Monospace",
}
],
}
],
}


def format_fact_list_block(block: FactListBlock) -> Dict[str, Any]:
return {
"type": "FactSet",
"facts": [
{
"title": format_line_block_text(fact.title),
"value": format_line_block_text(fact.value),
}
for fact in block.facts
],
}


def format_message_block(
block: MessageBlock, color: Optional[Color] = None
) -> List[Dict[str, Any]]:
if isinstance(block, HeaderBlock):
return [format_header_block(block, color)]
elif isinstance(block, CodeBlock):
return [format_code_block(block)]
elif isinstance(block, LinesBlock):
return format_lines_block(block)
elif isinstance(block, FactListBlock):
return [format_fact_list_block(block)]
elif isinstance(block, ExpandableBlock):
return format_expandable_block(block)
else:
raise ValueError(f"Unsupported message block type: {type(block)}")


def split_message_blocks_by_divider(
blocks: List[MessageBlock],
) -> List[List[MessageBlock]]:
first_divider_index = next(
(i for i, block in enumerate(blocks) if isinstance(block, DividerBlock)),
None,
)
if first_divider_index is None:
return [blocks] if blocks else []
return [
blocks[:first_divider_index],
*split_message_blocks_by_divider(blocks[first_divider_index + 1 :]),
]


def format_divided_message_blocks(
blocks: List[MessageBlock],
divider: bool = False,
color: Optional[Color] = None,
) -> Dict[str, Any]:
return {
"type": "Container",
"separator": divider,
"items": [
item for block in blocks for item in format_message_block(block, color)
],
}


def format_expandable_block(block: ExpandableBlock) -> List[Dict[str, Any]]:
block_title = block.title
expandable_target_id = f"expandable-{uuid.uuid4()}"
return [
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": block_title,
"targetElements": [expandable_target_id],
}
],
},
{
"type": "Container",
"id": expandable_target_id,
"items": format_message_blocks(block.body),
"isVisible": block.expanded,
},
]


def format_message_blocks(
blocks: List[MessageBlock], color: Optional[Color] = None
) -> List[Dict[str, Any]]:
if not blocks:
return []

message_blocks = split_message_blocks_by_divider(blocks)
# The divider is not a block in adaptive cards, it's a property of the container.
return [
format_divided_message_blocks(blocks, divider=True, color=color)
for blocks in message_blocks
]


def format_adaptive_card_body(message: MessageBody) -> List[Dict[str, Any]]:
return format_message_blocks(message.blocks, message.color)


def format_adaptive_card(message: MessageBody, version: str = "1.6") -> Dict[str, Any]:
if version < "1.2" or version > "1.6":
raise ValueError(f"Version {version} is not supported")
return {
"type": "AdaptiveCard",
"body": format_adaptive_card_body(message),
"version": version,
}
Empty file added tests/unit/messages/__init__.py
Empty file.
Empty file.
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Main Header",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Good"
},
{
"type": "TextBlock",
"text": "Normal text **Bold text** _Italic text_",
"wrap": true
},
{
"type": "TextBlock",
"text": "- First bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "- Second bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "\u2705 Check item",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "Status",
"value": "Passed"
},
{
"title": "Tags",
"value": "test, example"
}
]
},
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": "Show Details",
"targetElements": [
"expandable-00000000-0000-0000-0000-000000000001"
]
}
]
},
{
"type": "Container",
"id": "expandable-00000000-0000-0000-0000-000000000001",
"items": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "\ud83d\udd0e **Details Section**",
"wrap": true
},
{
"type": "TextBlock",
"text": "Here's some content with a [link](https://example.com)",
"wrap": true
}
]
}
],
"isVisible": false
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Main Header",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Default"
},
{
"type": "TextBlock",
"text": "Normal text **Bold text** _Italic text_",
"wrap": true
},
{
"type": "TextBlock",
"text": "- First bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "- Second bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "\u2705 Check item",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "Status",
"value": "Passed"
},
{
"title": "Tags",
"value": "test, example"
}
]
},
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": "Show Details",
"targetElements": [
"expandable-00000000-0000-0000-0000-000000000001"
]
}
]
},
{
"type": "Container",
"id": "expandable-00000000-0000-0000-0000-000000000001",
"items": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "\ud83d\udd0e **Details Section**",
"wrap": true
},
{
"type": "TextBlock",
"text": "Here's some content with a [link](https://example.com)",
"wrap": true
}
]
}
],
"isVisible": false
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Main Header",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Attention"
},
{
"type": "TextBlock",
"text": "Normal text **Bold text** _Italic text_",
"wrap": true
},
{
"type": "TextBlock",
"text": "- First bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "- Second bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "\u2705 Check item",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "Status",
"value": "Passed"
},
{
"title": "Tags",
"value": "test, example"
}
]
},
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": "Show Details",
"targetElements": [
"expandable-00000000-0000-0000-0000-000000000001"
]
}
]
},
{
"type": "Container",
"id": "expandable-00000000-0000-0000-0000-000000000001",
"items": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "\ud83d\udd0e **Details Section**",
"wrap": true
},
{
"type": "TextBlock",
"text": "Here's some content with a [link](https://example.com)",
"wrap": true
}
]
}
],
"isVisible": false
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Main Header",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Warning"
},
{
"type": "TextBlock",
"text": "Normal text **Bold text** _Italic text_",
"wrap": true
},
{
"type": "TextBlock",
"text": "- First bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "- Second bullet point",
"wrap": true
},
{
"type": "TextBlock",
"text": "\u2705 Check item",
"wrap": true
},
{
"type": "FactSet",
"facts": [
{
"title": "Status",
"value": "Passed"
},
{
"title": "Tags",
"value": "test, example"
}
]
},
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": "Show Details",
"targetElements": [
"expandable-00000000-0000-0000-0000-000000000001"
]
}
]
},
{
"type": "Container",
"id": "expandable-00000000-0000-0000-0000-000000000001",
"items": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "\ud83d\udd0e **Details Section**",
"wrap": true
},
{
"type": "TextBlock",
"text": "Here's some content with a [link](https://example.com)",
"wrap": true
}
]
}
],
"isVisible": false
}
]
}
],
"version": "1.6"
}
17 changes: 17 additions & 0 deletions tests/unit/messages/formats/adaptive_cards/fixtures/all_icons.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "RED_TRIANGLE \ud83d\udd3a X \u274c WARNING \u26a0\ufe0f EXCLAMATION \u2757 CHECK \u2705 MAGNIFYING_GLASS \ud83d\udd0e HAMMER_AND_WRENCH \ud83d\udee0\ufe0f POLICE_LIGHT \ud83d\udea8 INFO \u2139\ufe0f EYE \ud83d\udc41\ufe0f GEAR \u2699\ufe0f BELL \ud83d\udd14",
"wrap": true
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "- First bullet",
"wrap": true
},
{
"type": "TextBlock",
"text": "- Second bullet",
"wrap": true
},
{
"type": "TextBlock",
"text": "\u2705 Check item 1",
"wrap": true
},
{
"type": "TextBlock",
"text": "\u2705 Check item 2",
"wrap": true
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"style": "emphasis",
"items": [
{
"type": "RichTextBlock",
"inlines": [
{
"type": "TextRun",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, c",
"fontType": "Monospace"
}
]
}
]
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"style": "emphasis",
"items": [
{
"type": "RichTextBlock",
"inlines": [
{
"type": "TextRun",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing",
"fontType": "Monospace"
}
]
}
]
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"style": "emphasis",
"items": [
{
"type": "RichTextBlock",
"inlines": [
{
"type": "TextRun",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adip",
"fontType": "Monospace"
}
]
}
]
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Test Header",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Good"
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "First Section",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Default"
}
]
},
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Second Section",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Default"
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": "Show More",
"targetElements": [
"expandable-00000000-0000-0000-0000-000000000001"
]
}
]
},
{
"type": "Container",
"id": "expandable-00000000-0000-0000-0000-000000000001",
"items": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "Hidden content",
"wrap": true
}
]
}
],
"isVisible": false
}
]
}
],
"version": "1.6"
}
21 changes: 21 additions & 0 deletions tests/unit/messages/formats/adaptive_cards/fixtures/fact_list.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "FactSet",
"facts": [
{
"title": "Status",
"value": "Passed"
}
]
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": "Outer Block",
"targetElements": [
"expandable-00000000-0000-0000-0000-000000000001"
]
}
]
},
{
"type": "Container",
"id": "expandable-00000000-0000-0000-0000-000000000001",
"items": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "\ud83d\udd0e **Title with Icon**",
"wrap": true
},
{
"type": "TextBlock",
"text": "Some content with a [link](https://example.com)",
"wrap": true
},
{
"type": "ActionSet",
"actions": [
{
"type": "Action.ToggleVisibility",
"title": "Inner Block",
"targetElements": [
"expandable-00000000-0000-0000-0000-000000000002"
]
}
]
},
{
"type": "Container",
"id": "expandable-00000000-0000-0000-0000-000000000002",
"items": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "Inner content",
"wrap": true
}
]
}
],
"isVisible": true
}
]
}
],
"isVisible": false
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "Container",
"items": [
{
"type": "TextBlock",
"text": "Test Header",
"weight": "bolder",
"size": "large",
"wrap": true
}
],
"style": "Default"
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, con",
"wrap": true
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, c",
"wrap": true
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing",
"wrap": true
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Lorem ipsum dolor sit amet, consectetur adip",
"wrap": true
}
]
}
],
"version": "1.6"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "Container",
"separator": true,
"items": [
{
"type": "TextBlock",
"text": "Normal text **Bold text** _Italic text_",
"wrap": true
}
]
}
],
"version": "1.6"
}
373 changes: 373 additions & 0 deletions tests/unit/messages/formats/adaptive_cards/test_adaptive_cards.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
"""
Tests for adaptive cards message format.
The expected results are saved as JSON files in the resources/ directory.
These files can be viewed and tested using:
1. VS Code's Adaptive Cards extension
2. Adaptive Cards Designer (https://adaptivecards.io/designer/) - useful for checking how cards
look across different products, devices, themes and schema versions
"""

import uuid
from pathlib import Path
from typing import List, Union

import pytest

from elementary.messages.block_builders import BulletListBlock
from elementary.messages.blocks import (
CodeBlock,
DividerBlock,
ExpandableBlock,
FactBlock,
FactListBlock,
HeaderBlock,
Icon,
IconBlock,
LineBlock,
LinesBlock,
LinkBlock,
TextBlock,
TextStyle,
)
from elementary.messages.formats.adaptive_cards import format_adaptive_card
from elementary.messages.message_body import Color, MessageBody
from tests.unit.messages.utils import assert_expected_json, get_expected_json_path

FIXTURES_DIR = Path(__file__).resolve().parent / "fixtures"


@pytest.fixture(autouse=True)
def mock_uuid(monkeypatch):
class MockUUID:
def __init__(self):
self.counter = 0

def __call__(self):
self.counter += 1
return uuid.UUID(
f"00000000-0000-0000-0000-{self.counter:012d}" # noqa: E231
)

mock = MockUUID()
monkeypatch.setattr(uuid, "uuid4", mock)
return mock


def test_format_message_body_simple_header():
message_body = MessageBody(blocks=[HeaderBlock(text="Test Header")], color=None)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "simple_header.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


def test_format_message_body_colored_header():
message_body = MessageBody(
blocks=[HeaderBlock(text="Test Header")], color=Color.GREEN
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "colored_header.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


def test_format_message_body_all_icons():
icon_blocks: List[Union[TextBlock, IconBlock]] = []
for icon in Icon:
icon_blocks.append(TextBlock(text=icon.name))
icon_blocks.append(IconBlock(icon=icon))
message_body = MessageBody(
blocks=[LinesBlock(lines=[LineBlock(inlines=icon_blocks)])]
)
result = format_adaptive_card(message_body)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "all_icons.json")
assert_expected_json(result, expected_json_path)


def test_format_message_body_text_styles():
message_body = MessageBody(
blocks=[
LinesBlock(
lines=[
LineBlock(
inlines=[
TextBlock(text="Normal text"),
TextBlock(text="Bold text", style=TextStyle.BOLD),
TextBlock(text="Italic text", style=TextStyle.ITALIC),
]
)
]
)
]
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "text_styles.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


def test_format_message_body_fact_list():
message_body = MessageBody(
blocks=[
FactListBlock(
facts=[
FactBlock(
title=LineBlock(inlines=[TextBlock(text="Status")]),
value=LineBlock(inlines=[TextBlock(text="Passed")]),
),
]
)
]
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "fact_list.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


def test_format_message_body_expandable_block():
message_body = MessageBody(
blocks=[
ExpandableBlock(
title="Show More",
body=[
LinesBlock(
lines=[LineBlock(inlines=[TextBlock(text="Hidden content")])]
)
],
expanded=False,
)
]
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "expandable_block.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


def test_format_message_body_divider_blocks():
message_body = MessageBody(
blocks=[
HeaderBlock(text="First Section"),
DividerBlock(),
HeaderBlock(text="Second Section"),
]
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "divider_blocks.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


def test_format_message_body_bullet_lists():
message_body = MessageBody(
blocks=[
BulletListBlock(
icon="-",
lines=[
LineBlock(inlines=[TextBlock(text="First bullet")]),
LineBlock(inlines=[TextBlock(text="Second bullet")]),
],
),
BulletListBlock(
icon=Icon.CHECK,
lines=[
LineBlock(inlines=[TextBlock(text="Check item 1")]),
LineBlock(inlines=[TextBlock(text="Check item 2")]),
],
),
]
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "bullet_list.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


def test_format_message_body_nested_expandable():
message_body = MessageBody(
blocks=[
ExpandableBlock(
title="Outer Block",
body=[
LinesBlock(
lines=[
LineBlock(
inlines=[
IconBlock(icon=Icon.MAGNIFYING_GLASS),
TextBlock(
text="Title with Icon", style=TextStyle.BOLD
),
]
),
LineBlock(
inlines=[
TextBlock(text="Some content with a"),
LinkBlock(text="link", url="https://example.com"),
]
),
]
),
ExpandableBlock(
title="Inner Block",
body=[
LinesBlock(
lines=[
LineBlock(inlines=[TextBlock(text="Inner content")])
]
)
],
expanded=True,
),
],
expanded=False,
)
]
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, "nested_expandable.json")
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


@pytest.mark.parametrize(
"color,expected_file",
[
pytest.param(None, "all_blocks_no_color.json", id="no_color"),
pytest.param(Color.RED, "all_blocks_red.json", id="red"),
pytest.param(Color.YELLOW, "all_blocks_yellow.json", id="yellow"),
pytest.param(Color.GREEN, "all_blocks_green.json", id="green"),
],
)
def test_format_message_body_all_blocks(color, expected_file):
"""Test a comprehensive message that includes all block types with different colors."""
message_body = MessageBody(
blocks=[
HeaderBlock(text="Main Header"),
LinesBlock(
lines=[
LineBlock(
inlines=[
TextBlock(text="Normal text"),
TextBlock(text="Bold text", style=TextStyle.BOLD),
TextBlock(text="Italic text", style=TextStyle.ITALIC),
]
)
]
),
BulletListBlock(
icon="-",
lines=[
LineBlock(inlines=[TextBlock(text="First bullet point")]),
LineBlock(inlines=[TextBlock(text="Second bullet point")]),
],
),
BulletListBlock(
icon=Icon.CHECK,
lines=[LineBlock(inlines=[TextBlock(text="Check item")])],
),
FactListBlock(
facts=[
FactBlock(
title=LineBlock(inlines=[TextBlock(text="Status")]),
value=LineBlock(inlines=[TextBlock(text="Passed")]),
),
FactBlock(
title=LineBlock(inlines=[TextBlock(text="Tags")]),
value=LineBlock(inlines=[TextBlock(text="test, example")]),
),
]
),
ExpandableBlock(
title="Show Details",
body=[
LinesBlock(
lines=[
LineBlock(
inlines=[
IconBlock(icon=Icon.MAGNIFYING_GLASS),
TextBlock(
text="Details Section", style=TextStyle.BOLD
),
]
),
LineBlock(
inlines=[
TextBlock(text="Here's some content with a"),
LinkBlock(text="link", url="https://example.com"),
]
),
]
)
],
expanded=False,
),
],
color=color,
)
expected_json_path = get_expected_json_path(FIXTURES_DIR, expected_file)
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


@pytest.mark.parametrize(
"version,should_raise",
[
pytest.param("1.6", False, id="supported_version"),
pytest.param("1.1", True, id="unsupported_version_low"),
pytest.param("1.7", True, id="unsupported_version_high"),
],
)
def test_format_version_validation(version, should_raise):
message_body = MessageBody(blocks=[HeaderBlock(text="Test")])

if should_raise:
try:
format_adaptive_card(message_body, version=version)
assert False, f"Expected ValueError for version {version}"
except ValueError:
pass
else:
result = format_adaptive_card(message_body, version=version)
assert result["version"] == version
assert result["type"] == "AdaptiveCard"


@pytest.mark.parametrize(
"text_length",
[
pytest.param(50, id="short_code"),
pytest.param(200, id="medium_code"),
pytest.param(500, id="long_code"),
],
)
def test_format_message_body_code_block(text_length: int):
lorem_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * (
(text_length + 49) // 50
)
lorem_text = lorem_text[:text_length]

message_body = MessageBody(blocks=[CodeBlock(text=lorem_text)])
expected_json_path = get_expected_json_path(
FIXTURES_DIR, f"code_block_{text_length}.json"
)
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)


@pytest.mark.parametrize(
"text_length",
[
pytest.param(50, id="short_text"),
pytest.param(200, id="medium_text"),
pytest.param(500, id="long_text"),
pytest.param(1000, id="very_long_text"),
],
)
def test_format_message_body_text_length(text_length: int):
lorem_text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. " * (
(text_length + 49) // 50
)
lorem_text = lorem_text[:text_length]

message_body = MessageBody(
blocks=[LinesBlock(lines=[LineBlock(inlines=[TextBlock(text=lorem_text)])])]
)
expected_json_path = get_expected_json_path(
FIXTURES_DIR, f"text_length_{text_length}.json"
)
result = format_adaptive_card(message_body)
assert_expected_json(result, expected_json_path)
28 changes: 28 additions & 0 deletions tests/unit/messages/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import json
import os
from pathlib import Path

from elementary.utils.log import get_logger

logger = get_logger(__name__)


# Set to True to override the expected JSON files with the actual results
# This is useful for updating the expected JSON files with the actual results, for development purposes only!
OVERRIDE = os.getenv("OVERRIDE", "false").lower() == "true"


def get_expected_json_path(fixture_dir: Path, filename: str) -> Path:
path = fixture_dir / filename
if not path.exists():
path.write_text(json.dumps({}))
return path


def assert_expected_json(result: dict, expected_json_path: Path) -> None:
if OVERRIDE:
logger.warning(f"Overriding expected JSON file: {expected_json_path}")
print("writing to file", OVERRIDE)
expected_json_path.write_text(json.dumps(result, indent=2))
expected = json.loads(expected_json_path.read_text())
assert json.dumps(result, indent=2) == json.dumps(expected, indent=2)

0 comments on commit cc614cf

Please sign in to comment.