Download google-workspace/upstream/source/tests/gdocs/test_docs_markdown.py from smlflg/Connector: direct link, hf CLI and curl.
- Browser
- Download file 39.4 kB
-
https://huggingface.co/smlflg/Connector/resolve/main/google-workspace/upstream/source/tests/gdocs/test_docs_markdown.py
- Command line
-
hf download hf://smlflg/Connector/google-workspace/upstream/source/tests/gdocs/test_docs_markdown.py
-
curl -L -o test_docs_markdown.py https://huggingface.co/smlflg/Connector/resolve/main/google-workspace/upstream/source/tests/gdocs/test_docs_markdown.py
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 "" 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) " | |
| "" | |
| ) 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() == "" | |