"""Тесты консольного форматтера таблицы режима --my-tasks (#71).""" import re from typing import cast import pytest from redmine_reporter.formatters.task_table import TaskTableFormatter from redmine_reporter.types import TaskRow BASE_URL = "https://red.eltex.loc" # OSC 8-гиперссылка: esc-последовательность до текста, текст, закрытие ссылки. OSC8_RE = re.compile(r"\x1b\]8;;[^\x1b]*\x1b\\") def make_row(**overrides) -> TaskRow: row: TaskRow = { "issue_id": 42, "subject": "Починить баг", "status_ru": "Новая", "priority": "P2", "version_name": "v2.5.0", "due_text": "2026-10-15", "overdue": False, } # **-распаковка dict[str, Any] в TypedDict-литерале mypy не поддерживает, # а kwargs-словарь типизируется только как dict[str, Any], поэтому приведение. row.update(cast(TaskRow, overrides)) return row def expected_hyperlink(issue_id, base_url=BASE_URL): return f"\x1b]8;;{base_url}/issues/{issue_id}\x1b\\{issue_id}\x1b]8;;\x1b\\" # -- OSC 8-гиперссылка на ID -- def test_id_wrapped_in_exact_osc8_sequence(): output = TaskTableFormatter(base_url=BASE_URL).format([make_row()]) assert expected_hyperlink(42) in output def test_base_url_trailing_slash_not_duplicated(): """base_url с завершающим слэшем не даёт двойного слэша перед /issues.""" output = TaskTableFormatter(base_url=BASE_URL + "/").format([make_row()]) assert expected_hyperlink(42) in output assert "//issues/" not in output def test_hyperlink_text_is_issue_number_only(): """Текст ссылки -- только номер, тема не попадает внутрь OSC 8.""" output = TaskTableFormatter(base_url=BASE_URL).format( [make_row(issue_id=7, subject="Тема задачи")] ) assert expected_hyperlink(7) in output assert "Тема задачи" in output start = output.index("Тема задачи") assert "\x1b" not in output[:start].replace(expected_hyperlink(7), "") def test_hyperlink_uses_row_issue_id(): output = TaskTableFormatter(base_url=BASE_URL).format([make_row(issue_id=1234)]) assert expected_hyperlink(1234) in output # -- Заполнение пустых полей -- def test_empty_version_rendered_as_dash(): output = TaskTableFormatter(base_url=BASE_URL).format([make_row(version_name="")]) assert "-" in output def test_empty_due_rendered_as_dash(): output = TaskTableFormatter(base_url=BASE_URL).format([make_row(due_text="")]) assert "-" in output def test_nonempty_fields_not_replaced_by_dash(): output = TaskTableFormatter(base_url=BASE_URL).format([make_row()]) assert "v2.5.0" in output assert "2026-10-15" in output # -- Маркер просрочки -- def test_overdue_due_appends_marker(): output = TaskTableFormatter(base_url=BASE_URL).format( [make_row(due_text="2026-10-15", overdue=True)] ) assert "2026-10-15 (!)" in output def test_not_overdue_due_without_marker(): output = TaskTableFormatter(base_url=BASE_URL).format( [make_row(due_text="2026-10-15", overdue=False)] ) assert "2026-10-15" in output assert "(!)" not in output # -- Заголовки и содержимое -- def test_column_headers_present(): output = TaskTableFormatter(base_url=BASE_URL).format([make_row()]) for header in ("ID", "Тема", "Статус", "Приор.", "Версия", "Срок"): assert header in output def test_status_passed_through_as_is(): """status_ru из TaskRow попадает в таблицу без изменений.""" output = TaskTableFormatter(base_url=BASE_URL).format( [make_row(status_ru="В работе")] ) assert "В работе" in output def test_subject_and_priority_present(): output = TaskTableFormatter(base_url=BASE_URL).format( [make_row(subject="Обновить драйвер", priority="P1")] ) assert "Обновить драйвер" in output assert "P1" in output def test_multiple_rows_rendered(): rows = [make_row(issue_id=1), make_row(issue_id=2)] output = TaskTableFormatter(base_url=BASE_URL).format(rows) assert expected_hyperlink(1) in output assert expected_hyperlink(2) in output def test_empty_rows_renders_headers_only(): output = TaskTableFormatter(base_url=BASE_URL).format([]) assert "ID" in output assert expected_hyperlink(42) not in output # -- ESC только внутри ссылок -- def test_esc_only_inside_hyperlinks(): """После вырезания OSC 8-ссылок в строке не остаётся ESC-символов.""" rows = [ make_row(issue_id=1), make_row(issue_id=2, version_name=""), make_row(issue_id=3, due_text="", overdue=True), ] output = TaskTableFormatter(base_url=BASE_URL).format(rows) cleaned = OSC8_RE.sub("", output) assert "\x1b" not in cleaned def test_save_raises_not_implemented(): formatter = TaskTableFormatter(base_url=BASE_URL) with pytest.raises(NotImplementedError): formatter.save([], "unused.xlsx")