Connector / google-workspace /upstream /source /tests /gdocs /test_docs_markdown.py
smlflg's picture
Initial public upload from Projekte/Connector (part 16)
bf06a0d verified
Raw History Blame Contribute Delete
39.4 kB
"""Tests for the Google Docs to Markdown converter."""
import sys
import os
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
from gdocs.docs_markdown import (
convert_doc_to_markdown,
format_comments_appendix,
format_comments_inline,
parse_drive_comments,
)
# --- Fixtures ---
SIMPLE_DOC = {
"title": "Simple Test",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Hello world\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "This is ", "textStyle": {}}},
{"textRun": {"content": "bold", "textStyle": {"bold": True}}},
{"textRun": {"content": " and ", "textStyle": {}}},
{
"textRun": {
"content": "italic",
"textStyle": {"italic": True},
}
},
{"textRun": {"content": " text.\n", "textStyle": {}}},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
]
},
}
HEADINGS_DOC = {
"title": "Headings",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [{"textRun": {"content": "Title\n", "textStyle": {}}}],
"paragraphStyle": {"namedStyleType": "TITLE"},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Heading one\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "HEADING_1"},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Heading two\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "HEADING_2"},
}
},
]
},
}
TABLE_DOC = {
"title": "Table Test",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"table": {
"rows": 2,
"columns": 2,
"tableRows": [
{
"tableCells": [
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Name\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Age\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
]
},
{
"tableCells": [
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Alice\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
{
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "30\n",
"textStyle": {},
}
}
],
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT"
},
}
}
]
},
]
},
],
}
},
]
},
}
LIST_DOC = {
"title": "List Test",
"lists": {
"kix.list001": {
"listProperties": {
"nestingLevels": [
{"glyphType": "GLYPH_TYPE_UNSPECIFIED", "glyphSymbol": "\u2022"},
]
}
}
},
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Item one\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.list001", "nestingLevel": 0},
}
},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Item two\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.list001", "nestingLevel": 0},
}
},
]
},
}
# --- Converter tests ---
class TestTextFormatting:
def test_plain_text(self):
md = convert_doc_to_markdown(SIMPLE_DOC)
assert "Hello world" in md
def test_bold(self):
md = convert_doc_to_markdown(SIMPLE_DOC)
assert "**bold**" in md
def test_italic(self):
md = convert_doc_to_markdown(SIMPLE_DOC)
assert "*italic*" in md
class TestHeadings:
def test_title(self):
md = convert_doc_to_markdown(HEADINGS_DOC)
assert "# Title" in md
def test_h1(self):
md = convert_doc_to_markdown(HEADINGS_DOC)
assert "# Heading one" in md
def test_h2(self):
md = convert_doc_to_markdown(HEADINGS_DOC)
assert "## Heading two" in md
class TestTables:
def test_table_header(self):
md = convert_doc_to_markdown(TABLE_DOC)
assert "| Name | Age |" in md
def test_table_separator(self):
md = convert_doc_to_markdown(TABLE_DOC)
assert "| --- | --- |" in md
def test_table_row(self):
md = convert_doc_to_markdown(TABLE_DOC)
assert "| Alice | 30 |" in md
class TestLists:
def test_unordered(self):
md = convert_doc_to_markdown(LIST_DOC)
assert "- Item one" in md
assert "- Item two" in md
CHECKLIST_DOC = {
"title": "Checklist Test",
"lists": {
"kix.checklist001": {
"listProperties": {
"nestingLevels": [
{"glyphType": "GLYPH_TYPE_UNSPECIFIED"},
]
}
}
},
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Buy groceries\n", "textStyle": {}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.checklist001", "nestingLevel": 0},
}
},
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Walk the dog\n",
"textStyle": {"strikethrough": True},
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
"bullet": {"listId": "kix.checklist001", "nestingLevel": 0},
}
},
]
},
}
class TestChecklists:
def test_unchecked(self):
md = convert_doc_to_markdown(CHECKLIST_DOC)
assert "- [ ] Buy groceries" in md
def test_checked(self):
md = convert_doc_to_markdown(CHECKLIST_DOC)
assert "- [x] Walk the dog" in md
def test_checked_no_strikethrough(self):
"""Checked items should not have redundant ~~strikethrough~~ markdown."""
md = convert_doc_to_markdown(CHECKLIST_DOC)
assert "~~Walk the dog~~" not in md
def test_regular_bullet_not_checklist(self):
"""Bullet lists with glyphSymbol should remain as plain bullets."""
md = convert_doc_to_markdown(LIST_DOC)
assert "[ ]" not in md
assert "[x]" not in md
PERSON_CHIP_DOC = {
"title": "Person Chip Test",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "Assigned to ", "textStyle": {}}},
{
"person": {
"personProperties": {
"name": "Alice Smith",
"email": "alice@example.com",
}
}
},
{"textRun": {"content": " for review.\n", "textStyle": {}}},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
]
},
}
RICH_LINK_CHIP_DOC = {
"title": "Rich Link Chip Test",
"body": {
"content": [
{"sectionBreak": {"sectionStyle": {}}},
{
"paragraph": {
"elements": [
{"textRun": {"content": "See ", "textStyle": {}}},
{
"richLink": {
"richLinkProperties": {
"title": "Project Plan",
"uri": "https://docs.google.com/document/d/abc123",
}
}
},
{"textRun": {"content": " for details.\n", "textStyle": {}}},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
]
},
}
class TestSmartChips:
def test_person_chip(self):
md = convert_doc_to_markdown(PERSON_CHIP_DOC)
assert "[Alice Smith](mailto:alice@example.com)" in md
def test_person_chip_in_context(self):
md = convert_doc_to_markdown(PERSON_CHIP_DOC)
assert "Assigned to [Alice Smith](mailto:alice@example.com) for review." in md
def test_person_chip_name_only(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{"person": {"personProperties": {"name": "Bob Jones"}}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "Bob Jones" in md
def test_person_chip_email_only(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"person": {
"personProperties": {"email": "bob@example.com"}
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "[bob@example.com](mailto:bob@example.com)" in md
def test_rich_link_chip(self):
md = convert_doc_to_markdown(RICH_LINK_CHIP_DOC)
assert "[Project Plan](https://docs.google.com/document/d/abc123)" in md
def test_rich_link_chip_in_context(self):
md = convert_doc_to_markdown(RICH_LINK_CHIP_DOC)
assert (
"See [Project Plan](https://docs.google.com/document/d/abc123) for details."
in md
)
def test_rich_link_uri_only(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"richLink": {
"richLinkProperties": {
"uri": "https://example.com"
}
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert md == "https://example.com\n"
def test_date_chip_display_text(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Due by ",
"textStyle": {},
}
},
{
"dateElement": {
"dateElementProperties": {
"displayText": "Mar 31, 2026",
"timestamp": "2026-03-31T00:00:00Z",
}
}
},
{
"textRun": {
"content": " please.\n",
"textStyle": {},
}
},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "Due by Mar 31, 2026 please." in md
def test_date_chip_timestamp_fallback(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"dateElement": {
"dateElementProperties": {
"timestamp": "2026-01-15T00:00:00Z",
}
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "2026-01-15" in md
def test_inline_image(self):
doc = {
"title": "Test",
"inlineObjects": {
"kix.obj1": {
"inlineObjectProperties": {
"embeddedObject": {
"title": "Logo",
"imageProperties": {
"contentUri": "https://example.com/logo.png"
},
}
}
}
},
"body": {
"content": [
{
"paragraph": {
"elements": [
{"inlineObjectElement": {"inlineObjectId": "kix.obj1"}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "![Logo](https://example.com/logo.png)" in md
def test_inline_image_no_uri(self):
doc = {
"title": "Test",
"inlineObjects": {
"kix.obj1": {
"inlineObjectProperties": {"embeddedObject": {"title": "Chart"}}
}
},
"body": {
"content": [
{
"paragraph": {
"elements": [
{"inlineObjectElement": {"inlineObjectId": "kix.obj1"}}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "[Image: Chart]" in md
def test_footnote_reference(self):
doc = {
"title": "Test",
"footnotes": {
"kix.fn1": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "See the appendix.\n",
"textStyle": {},
}
}
]
}
}
]
}
},
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Important claim",
"textStyle": {},
}
},
{"footnoteReference": {"footnoteId": "kix.fn1"}},
{
"textRun": {
"content": " and more text.\n",
"textStyle": {},
}
},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "Important claim[^kix.fn1] and more text." in md
assert "[^kix.fn1]: See the appendix." in md
def test_footnote_reference_preserves_rich_inline_content(self):
doc = {
"title": "Test",
"inlineObjects": {
"kix.inline1": {
"inlineObjectProperties": {
"embeddedObject": {
"title": "Chart",
"imageProperties": {
"contentUri": "https://cdn.example.com/chart.png"
},
}
}
}
},
"footnotes": {
"kix.fn1": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "See ",
"textStyle": {},
}
},
{
"textRun": {
"content": "styled",
"textStyle": {"bold": True},
}
},
{
"textRun": {
"content": " ",
"textStyle": {},
}
},
{
"textRun": {
"content": "link",
"textStyle": {
"link": {
"url": "https://example.com/link"
}
},
}
},
{
"textRun": {
"content": " ",
"textStyle": {},
}
},
{
"person": {
"personProperties": {
"name": "Ada Lovelace",
"email": "ada@example.com",
}
}
},
{
"textRun": {
"content": " ",
"textStyle": {},
}
},
{
"richLink": {
"richLinkProperties": {
"title": "Project Plan",
"uri": "https://example.com/plan",
}
}
},
{
"textRun": {
"content": " ",
"textStyle": {},
}
},
{
"inlineObjectElement": {
"inlineObjectId": "kix.inline1"
}
},
{
"textRun": {
"content": "\n",
"textStyle": {},
}
},
]
}
}
]
}
},
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Important claim",
"textStyle": {},
}
},
{"footnoteReference": {"footnoteId": "kix.fn1"}},
{
"textRun": {
"content": ".\n",
"textStyle": {},
}
},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "Important claim[^kix.fn1]." in md
assert (
"[^kix.fn1]: See **styled** [link](https://example.com/link) "
"[Ada Lovelace](mailto:ada@example.com) "
"[Project Plan](https://example.com/plan) "
"![Chart](https://cdn.example.com/chart.png)"
) in md
def test_horizontal_rule(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Above\n",
"textStyle": {},
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
{
"paragraph": {
"elements": [{"horizontalRule": {}}],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Below\n",
"textStyle": {},
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
},
]
},
}
md = convert_doc_to_markdown(doc)
assert "---" in md
def test_auto_text_page_number(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Page ",
"textStyle": {},
}
},
{"autoText": {"type": "PAGE_NUMBER"}},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "Page [Page #]" in md
def test_equation_placeholder(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "The formula is ",
"textStyle": {},
}
},
{"equation": {}},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "The formula is [Equation]" in md
def test_page_break_skipped(self):
doc = {
"title": "Test",
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": "Before",
"textStyle": {},
}
},
{"pageBreak": {}},
{
"textRun": {
"content": "After\n",
"textStyle": {},
}
},
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
},
}
md = convert_doc_to_markdown(doc)
assert "BeforeAfter" in md
class TestEmptyDoc:
def test_empty(self):
md = convert_doc_to_markdown({"title": "Empty", "body": {"content": []}})
assert md.strip() == ""
def _make_tab(title, tab_id, text):
"""Helper to build a tab structure with a single paragraph."""
return {
"tabProperties": {"title": title, "tabId": tab_id},
"documentTab": {
"body": {
"content": [
{
"paragraph": {
"elements": [
{
"textRun": {
"content": f"{text}\n",
"textStyle": {},
}
}
],
"paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
}
}
]
}
},
}
class TestDocumentTabs:
def test_single_tab_no_heading(self):
"""A single-tab doc should render without a tab heading."""
doc = {"tabs": [_make_tab("Main", "t1", "Hello world")]}
md = convert_doc_to_markdown(doc)
assert "Hello world" in md
assert "# Main" not in md
def test_multi_tab_headings(self):
"""Multi-tab docs should get a heading per tab."""
doc = {
"tabs": [
_make_tab("Overview", "t1", "First tab content"),
_make_tab("Details", "t2", "Second tab content"),
]
}
md = convert_doc_to_markdown(doc)
assert "# Overview" in md
assert "First tab content" in md
assert "# Details" in md
assert "Second tab content" in md
def test_multi_tab_keeps_empty_tabs(self):
"""Empty tabs should still render a heading in multi-tab docs."""
doc = {
"tabs": [
_make_tab("Overview", "t1", "First tab content"),
_make_tab("Empty", "t2", ""),
]
}
md = convert_doc_to_markdown(doc)
assert "# Overview" in md
assert "First tab content" in md
assert "# Empty" in md
def test_child_tabs(self):
"""Child tabs should be flattened and rendered."""
parent = _make_tab("Parent", "t1", "Parent content")
child = _make_tab("Child", "t2", "Child content")
parent["childTabs"] = [child]
doc = {"tabs": [parent]}
md = convert_doc_to_markdown(doc)
assert "# Parent" in md
assert "Parent content" in md
assert "# Child" in md
assert "Child content" in md
def test_legacy_body_fallback(self):
"""Docs without tabs should still work via legacy body field."""
md = convert_doc_to_markdown(SIMPLE_DOC)
assert "Hello world" in md
# --- Comment parsing tests ---
class TestParseComments:
def test_filters_resolved(self):
response = {
"comments": [
{
"content": "open",
"resolved": False,
"author": {"displayName": "A"},
"replies": [],
},
{
"content": "closed",
"resolved": True,
"author": {"displayName": "B"},
"replies": [],
},
]
}
result = parse_drive_comments(response, include_resolved=False)
assert len(result) == 1
assert result[0]["content"] == "open"
def test_includes_resolved(self):
response = {
"comments": [
{
"content": "open",
"resolved": False,
"author": {"displayName": "A"},
"replies": [],
},
{
"content": "closed",
"resolved": True,
"author": {"displayName": "B"},
"replies": [],
},
]
}
result = parse_drive_comments(response, include_resolved=True)
assert len(result) == 2
def test_anchor_text(self):
response = {
"comments": [
{
"content": "note",
"resolved": False,
"author": {"displayName": "A"},
"quotedFileContent": {"value": "highlighted text"},
"replies": [],
}
]
}
result = parse_drive_comments(response)
assert result[0]["anchor_text"] == "highlighted text"
# --- Comment formatting tests ---
class TestInlineComments:
def test_inserts_footnote(self):
md = "Some text here."
comments = [
{
"author": "Alice",
"content": "Note.",
"anchor_text": "text",
"replies": [],
"resolved": False,
}
]
result = format_comments_inline(md, comments)
assert "text[^c1]" in result
assert "[^c1]: **Alice**: Note." in result
def test_unmatched_goes_to_appendix(self):
md = "No match."
comments = [
{
"author": "Alice",
"content": "Note.",
"anchor_text": "missing",
"replies": [],
"resolved": False,
}
]
result = format_comments_inline(md, comments)
assert "## Comments" in result
assert "> missing" in result
class TestAppendixComments:
def test_structure(self):
comments = [
{
"author": "Alice",
"content": "Note.",
"anchor_text": "some text",
"replies": [],
"resolved": False,
}
]
result = format_comments_appendix(comments)
assert "## Comments" in result
assert "> some text" in result
assert "**Alice**: Note." in result
def test_empty(self):
assert format_comments_appendix([]).strip() == ""