mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-24 21:38:09 +02:00
test: add unit tests for blocknote_to_markdown
This commit is contained in:
parent
cd2242147a
commit
5625ca1d5d
1 changed files with 546 additions and 0 deletions
546
surfsense_backend/tests/unit/utils/test_blocknote_to_markdown.py
Normal file
546
surfsense_backend/tests/unit/utils/test_blocknote_to_markdown.py
Normal file
|
|
@ -0,0 +1,546 @@
|
|||
"""Tests for the blocknote_to_markdown conversion module.
|
||||
|
||||
This module contains comprehensive unit tests for the blocknote_to_markdown function,
|
||||
covering all block types, inline styles, lists, tables, images, links, nested content,
|
||||
and edge cases.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from app.utils.blocknote_to_markdown import blocknote_to_markdown
|
||||
|
||||
pytestmark = pytest.mark.unit
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Headings (levels 1 to 6, and clamping for >6 / <1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestHeadingsLevelsAndClamping:
|
||||
"""Test heading conversion with various levels and clamping behavior."""
|
||||
|
||||
def test_heading_level_less_than_1(self):
|
||||
"""Heading level < 1 should be clamped to H1 (#)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 0},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "# My Title"
|
||||
|
||||
def test_heading_level_1(self):
|
||||
"""Heading level 1 should render as H1 (#)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 1},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "# My Title"
|
||||
|
||||
def test_heading_level_2(self):
|
||||
"""Heading level 2 should render as H2 (##)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 2},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "## My Title"
|
||||
|
||||
def test_heading_level_3(self):
|
||||
"""Heading level 3 should render as H3 (###)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 3},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "### My Title"
|
||||
|
||||
def test_heading_level_4(self):
|
||||
"""Heading level 4 should render as H4 (####)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 4},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "#### My Title"
|
||||
|
||||
def test_heading_level_5(self):
|
||||
"""Heading level 5 should render as H5 (#####)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 5},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "##### My Title"
|
||||
|
||||
def test_heading_level_6(self):
|
||||
"""Heading level 6 should render as H6 (######)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 6},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "###### My Title"
|
||||
|
||||
def test_heading_level_greater_than_6(self):
|
||||
"""Heading level > 6 should be clamped to H6 (######)."""
|
||||
|
||||
test_block = {
|
||||
"type": "heading",
|
||||
"props": {"level": 6},
|
||||
"content": [{"type": "text", "text": "My Title"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "###### My Title"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Inline styles: bold, italic, code, strikethrough
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestInlineStyles:
|
||||
"""Test inline text styling conversion."""
|
||||
|
||||
def test_bold_inline_style(self):
|
||||
"""Bold text should be wrapped in double asterisks (**)."""
|
||||
|
||||
test_block = {
|
||||
"type": "paragraph",
|
||||
"styles": {"bold": True},
|
||||
"content": [{"type": "text", "text": "Hello World!", "styles": {}}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "**Hello World!**"
|
||||
|
||||
def test_italic_inline_style(self):
|
||||
"""Italic text should be wrapped in single asterisks (*)."""
|
||||
|
||||
test_block = {
|
||||
"type": "paragraph",
|
||||
"styles": {"italic": True},
|
||||
"content": [{"type": "text", "text": "Hello World!", "styles": {}}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "*Hello World!*"
|
||||
|
||||
def test_code_inline_style(self):
|
||||
"""Code text should be wrapped in backticks (`)."""
|
||||
|
||||
test_block = {
|
||||
"type": "paragraph",
|
||||
"styles": {"code": True},
|
||||
"content": [{"type": "text", "text": "Hello World!", "styles": {}}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "`Hello World!`"
|
||||
|
||||
def test_strikethrough_inline_style(self):
|
||||
"""Strikethrough text should be wrapped in double tildes (~~)."""
|
||||
|
||||
test_block = {
|
||||
"type": "paragraph",
|
||||
"styles": {"strikethrough": True},
|
||||
"content": [{"type": "text", "text": "Hello World!", "styles": {}}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "~~Hello World!~~"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Lists: bullet, numbered (incl. props.start and counter reset), checklist (checked/unchecked)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestBulletAndNumberLists:
|
||||
"""Test bullet and numbered list conversion."""
|
||||
|
||||
def test_bullet_list_item(self):
|
||||
"""Bullet list items should render with dash (-) prefix."""
|
||||
|
||||
test_block = [
|
||||
{"type": "bulletListItem", "content": [{"type": "text", "text": "First"}]},
|
||||
{"type": "bulletListItem", "content": [{"type": "text", "text": "Second"}]},
|
||||
]
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "- First\n- Second"
|
||||
|
||||
def test_numbered_list_item(self):
|
||||
"""Numbered list items should auto-increment from 1."""
|
||||
|
||||
test_block = [
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"content": [{"type": "text", "text": "First"}],
|
||||
},
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"content": [{"type": "text", "text": "Second"}],
|
||||
},
|
||||
]
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "1. First\n2. Second"
|
||||
|
||||
def test_numbered_list_item_with_prop_start(self):
|
||||
"""Numbered list with props.start should begin at specified number."""
|
||||
|
||||
test_block = [
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"props": {"start": 5},
|
||||
"content": [{"type": "text", "text": "First"}],
|
||||
},
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"content": [{"type": "text", "text": "Second"}],
|
||||
},
|
||||
]
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "5. First\n6. Second"
|
||||
|
||||
def test_numbered_list_item_with_counter_reset(self):
|
||||
"""Multiple numbered lists with different start values should reset counters."""
|
||||
|
||||
test_block = [
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"content": [{"type": "text", "text": "First"}],
|
||||
},
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"content": [{"type": "text", "text": "Second"}],
|
||||
},
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"props": {"start": 5},
|
||||
"content": [{"type": "text", "text": "Third"}],
|
||||
},
|
||||
]
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "1. First\n2. Second\n5. Third"
|
||||
|
||||
|
||||
class TestCheckedAndUncheckedChecklist:
|
||||
"""Test checklist item conversion with checked/unchecked states."""
|
||||
|
||||
def test_checked_list_item(self):
|
||||
"""Checked checklist item should render with [x]."""
|
||||
|
||||
test_block = {
|
||||
"type": "checkListItem",
|
||||
"props": {"checked": True},
|
||||
"content": [{"type": "text", "text": "Finish implementing test modules"}],
|
||||
}
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== "- [x] Finish implementing test modules"
|
||||
)
|
||||
|
||||
def test_unchecked_list_item(self):
|
||||
"""Unchecked checklist item should render with [ ]."""
|
||||
|
||||
test_block = {
|
||||
"type": "checkListItem",
|
||||
"props": {"checked": False},
|
||||
"content": [{"type": "text", "text": "Finish implementing test modules"}],
|
||||
}
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== "- [ ] Finish implementing test modules"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Code blocks (with/without language)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCodeBlocksWithOrWithoutLanguage:
|
||||
"""Test code block conversion with optional language tags."""
|
||||
|
||||
def test_code_block_without_language(self):
|
||||
"""Code block without language should render with empty fence (```)."""
|
||||
|
||||
test_block = {
|
||||
"type": "codeBlock",
|
||||
"content": [{"type": "text", "text": "print('hi')"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "```\nprint('hi')\n```"
|
||||
|
||||
def test_code_block_with_language(self):
|
||||
"""Code block with language should render fence with language tag."""
|
||||
|
||||
test_block = {
|
||||
"type": "codeBlock",
|
||||
"props": {"language": "Python"},
|
||||
"content": [{"type": "text", "text": "print('hi')"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "```Python\nprint('hi')\n```"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tables (both dict and list row shapes)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_tables_dict_row_shape():
|
||||
"""Table with dict row shape should render with header separator."""
|
||||
|
||||
test_block = {
|
||||
"type": "table",
|
||||
"content": {
|
||||
"rows": [
|
||||
{"cells": ["Name", "Age", "City"]},
|
||||
{"cells": ["Alice", "25", "NYC"]},
|
||||
{"cells": ["Bob", "30", "LA"]},
|
||||
]
|
||||
},
|
||||
}
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== "| Name | Age | City |\n| --- | --- | --- |\n| Alice | 25 | NYC |\n| Bob | 30 | LA |"
|
||||
)
|
||||
|
||||
|
||||
def test_tables_list_row_shape():
|
||||
"""Table with list row shape should render with header separator."""
|
||||
test_block = {
|
||||
"type": "table",
|
||||
"content": [
|
||||
{"cells": ["Header1", "Header2"]},
|
||||
{"cells": ["Data1", "Data2"]},
|
||||
{"cells": ["Data3", "Data4"]},
|
||||
],
|
||||
}
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== "| Header1 | Header2 |\n| --- | --- |\n| Data1 | Data2 |\n| Data3 | Data4 |"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Images and links
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_image_block_input():
|
||||
"""Image block should render as markdown image syntax ."""
|
||||
|
||||
test_block = {
|
||||
"type": "image",
|
||||
"props": {"url": "https://example.com/pic.jpg", "caption": "A picture"},
|
||||
}
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block) == ""
|
||||
)
|
||||
|
||||
|
||||
def test_link_block_input_with_text():
|
||||
"""Link with content should render as [text](href)."""
|
||||
|
||||
test_block = {
|
||||
"type": "paragraph",
|
||||
"content": [
|
||||
{
|
||||
"type": "link",
|
||||
"href": "https://example.com",
|
||||
"content": [{"type": "text", "text": "Click here"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "[Click here](https://example.com)"
|
||||
|
||||
|
||||
def test_link_block_input_without_text():
|
||||
"""Link without content should use href as link text."""
|
||||
|
||||
test_block = {
|
||||
"type": "paragraph",
|
||||
"content": [{"type": "link", "href": "https://example.com"}],
|
||||
}
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== "[https://example.com](https://example.com)"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Nested children (indentation)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_nested_children_indentation():
|
||||
"""Nested list items should be indented with 2 spaces per level."""
|
||||
|
||||
test_block = {
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Parent"}],
|
||||
"children": [
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 1"}],
|
||||
},
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 2"}],
|
||||
},
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 3"}],
|
||||
},
|
||||
],
|
||||
}
|
||||
prefix = " "
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== f"- Parent\n{prefix}- Child 1\n{prefix}- Child 2\n{prefix}- Child 3"
|
||||
)
|
||||
|
||||
|
||||
def test_deep_nested_children_indentation():
|
||||
"""Deeply nested items (3+ levels) should accumulate indentation."""
|
||||
|
||||
test_block = {
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Parent"}],
|
||||
"children": [
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 1"}],
|
||||
"children": [
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 2"}],
|
||||
"children": [
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 3"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
prefix = " "
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== f"- Parent\n{prefix}- Child 1\n{prefix * 2}- Child 2\n{prefix * 3}- Child 3"
|
||||
)
|
||||
|
||||
|
||||
def test_mixed_deep_nested_children_indentation():
|
||||
"""Mixed block types in deep nesting should preserve indentation."""
|
||||
|
||||
test_block = {
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Parent"}],
|
||||
"children": [
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 1"}],
|
||||
"children": [
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"content": [{"type": "text", "text": "Nested Child 1"}],
|
||||
"children": [
|
||||
{
|
||||
"type": "numberedListItem",
|
||||
"content": [{"type": "text", "text": "Nested Child 2"}],
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 2"}],
|
||||
},
|
||||
{
|
||||
"type": "bulletListItem",
|
||||
"content": [{"type": "text", "text": "Child 3"}],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
prefix = " "
|
||||
|
||||
assert (
|
||||
blocknote_to_markdown(test_block)
|
||||
== f"- Parent\n{prefix}- Child 1\n{prefix * 2}1. Nested Child 1\n{prefix * 3}2. Nested Child 2\n{prefix}- Child 2\n{prefix}- Child 3"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Edge cases: None, empty list, single dict input, unknown block type
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_none_input():
|
||||
"""None input should return None."""
|
||||
|
||||
test_block = None
|
||||
|
||||
assert blocknote_to_markdown(test_block) is None
|
||||
|
||||
|
||||
def test_empty_list_input():
|
||||
"""Empty list input should return None."""
|
||||
|
||||
test_block = []
|
||||
|
||||
assert blocknote_to_markdown(test_block) is None
|
||||
|
||||
|
||||
def test_single_dict_input():
|
||||
"""Single dict block should be processed normally."""
|
||||
|
||||
test_block = {"type": "paragraph", "content": [{"type": "text", "text": "Hello"}]}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "Hello"
|
||||
|
||||
|
||||
def test_unknown_block_type_with_content():
|
||||
"""Unknown block type with content should extract and render the text."""
|
||||
|
||||
test_block = {
|
||||
"type": "customBlockType",
|
||||
"content": [{"type": "text", "text": "Some content"}],
|
||||
}
|
||||
|
||||
assert blocknote_to_markdown(test_block) == "Some content"
|
||||
|
||||
|
||||
def test_unknown_block_type_with_no_content():
|
||||
"""Unknown block type without content should return None."""
|
||||
|
||||
test_block = {"type": "customBlockType"}
|
||||
|
||||
assert blocknote_to_markdown(test_block) is None
|
||||
Loading…
Add table
Add a link
Reference in a new issue