test_table_parser.py 6.17 KB
"""Unit tests for table_parser module."""

import pytest

from hermes_feishu.table_parser import (
    ParsedTable,
    TableColumn,
    TableCell,
    contains_table,
    parse_table,
    split_table_and_text,
    _infer_column_type,
    _parse_row,
)


class TestParseRow:
    def test_basic_row(self):
        result = _parse_row("| Name | Age |")
        assert result == ["Name", "Age"]

    def test_row_with_spaces(self):
        result = _parse_row("|  Hello World  |  Test  |")
        assert result == ["Hello World", "Test"]

    def test_single_column(self):
        result = _parse_row("| Only |")
        assert result == ["Only"]

    def test_empty_cells(self):
        result = _parse_row("| A | | C |")
        assert result == ["A", "", "C"]

    def test_not_a_row(self):
        result = _parse_row("This is not a table row")
        assert result == []

    def test_cell_with_pipe_in_content(self):
        result = _parse_row("| a | b | c | d |")
        assert len(result) == 4

    def test_cell_with_special_chars(self):
        result = _parse_row("| $100 | 50% | 1,000 |")
        assert result == ["$100", "50%", "1,000"]


class TestInferColumnType:
    def test_all_numbers(self):
        values = ["1", "2", "3", "10.5"]
        assert _infer_column_type(values) == "number"

    def test_mixed(self):
        values = ["1", "hello", "3"]
        assert _infer_column_type(values) == "text"

    def test_all_text(self):
        values = ["apple", "banana", "cherry"]
        assert _infer_column_type(values) == "text"

    def test_empty(self):
        assert _infer_column_type([]) == "text"

    def test_numbers_with_commas(self):
        values = ["1,000", "2,500", "10,000"]
        assert _infer_column_type(values) == "number"

    def test_numbers_with_percent(self):
        values = ["50%", "75%", "100%"]
        assert _infer_column_type(values) == "number"

    def test_all_empty_strings(self):
        assert _infer_column_type(["", "", ""]) == "text"


class TestContainsTable:
    def test_has_table(self):
        md = "| Name | Age |\n| --- | --- |\n| Alice | 30 |"
        assert contains_table(md) is True

    def test_no_table(self):
        md = "Just some text without a table."
        assert contains_table(md) is False

    def test_table_in_text(self):
        md = "Here is some data:\n| A | B |\n| --- | --- |\n| 1 | 2 |\n\nDone."
        assert contains_table(md) is True

    def test_pipe_in_code_block(self):
        md = "```\n| this | is | code |\n```\n| real | table |\n| --- | --- |"
        # The code block pipes don't form a valid table (no separator row),
        # but the real table after the code block should be detected.
        assert contains_table(md) is True


class TestParseTable:
    def test_basic_table(self):
        md = "| Name | Age |\n| --- | --- |\n| Alice | 30 |\n| Bob | 25 |"
        tables = parse_table(md)
        assert len(tables) == 1

        table = tables[0]
        assert len(table.headers) == 2
        assert table.headers[0].name == "Name"
        assert table.headers[1].name == "Age"
        assert len(table.rows) == 2
        assert table.rows[0][0].text == "Alice"
        assert table.rows[0][1].text == "30"

    def test_table_with_alignments(self):
        md = "| Left | Center | Right |\n| :--- | :---: | ---: |\n| A | B | C |"
        tables = parse_table(md)
        assert len(tables) == 1
        assert len(tables[0].headers) == 3
        assert len(tables[0].rows) == 1

    def test_single_row_table(self):
        md = "| H1 | H2 |\n| --- | --- |"
        tables = parse_table(md)
        assert len(tables) == 1
        assert len(tables[0].headers) == 2
        assert len(tables[0].rows) == 0

    def test_multiple_tables(self):
        md = "| A | B |\n| --- | --- |\n| 1 | 2 |\n\n| X | Y |\n| --- | --- |\n| a | b |"
        tables = parse_table(md)
        assert len(tables) == 2

    def test_column_type_inference(self):
        md = "| Name | Score | Count |\n| --- | --- | --- |\n| Alice | 95.5 | 100 |\n| Bob | 87 | 200 |"
        tables = parse_table(md)
        table = tables[0]
        assert table.headers[0].field_type == "text"
        assert table.headers[1].field_type == "number"
        assert table.headers[2].field_type == "number"

    def test_table_with_empty_cells(self):
        md = "| A | B |\n| --- | --- |\n| hello | |\n|  | world |"
        tables = parse_table(md)
        table = tables[0]
        assert table.rows[0][1].text == ""
        assert table.rows[1][0].text == ""

    def test_table_with_markdown_in_cells(self):
        md = "| Feature | Status |\n| --- | --- |\n| **Bold** | `code` |\n| *Italic* | [link](url) |"
        tables = parse_table(md)
        table = tables[0]
        assert table.rows[0][0].text == "**Bold**"
        assert table.rows[1][0].text == "*Italic*"

    def test_no_table_returns_empty(self):
        tables = parse_table("No tables here")
        assert tables == []

    def test_preserves_raw_markdown(self):
        md = "| A | B |\n| --- | --- |\n| 1 | 2 |"
        tables = parse_table(md)
        assert tables[0].raw_markdown == md


class TestSplitTableAndText:
    def test_text_before_table(self):
        md = "Here is the data:\n| A | B |\n| --- | --- |\n| 1 | 2 |"
        table_blocks, text_segments = split_table_and_text(md)
        assert len(table_blocks) == 1
        assert len(text_segments) == 1
        assert "Here is the data" in text_segments[0]

    def test_table_surrounded_by_text(self):
        md = "Intro\n| A | B |\n| --- | --- |\n| 1 | 2 |\nOutro"
        table_blocks, text_segments = split_table_and_text(md)
        assert len(table_blocks) == 1
        assert len(text_segments) == 2
        assert "Intro" in text_segments[0]
        assert "Outro" in text_segments[1]

    def test_no_table(self):
        md = "Just text here"
        table_blocks, text_segments = split_table_and_text(md)
        assert len(table_blocks) == 0
        assert len(text_segments) == 1

    def test_multiple_tables_with_text(self):
        md = "First:\n| A | B |\n| --- | --- |\n| 1 | 2 |\n\nSecond:\n| X | Y |\n| --- | --- |\n| a | b |\nEnd"
        table_blocks, text_segments = split_table_and_text(md)
        assert len(table_blocks) == 2
        assert len(text_segments) == 3