mirror of
https://github.com/Textualize/textual.git
synced 2025-10-17 02:38:12 +03:00
* log * tests * snapshot tests * change to richlog * keep raw lines * disable highlighting by default * simplify * superfluous test * optimization * update cell length * add refresh * write method * version bump * doc fix link * makes lines private * docstring * relax dev dependancy * remove superfluous code [skip ci] * added FAQ [skipci] * fix code in faq [skipci] * fix typo * max lines fix
128 lines
4.2 KiB
Python
128 lines
4.2 KiB
Python
"""Unit tests for the Markdown widget."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Iterator
|
|
|
|
import pytest
|
|
from markdown_it.token import Token
|
|
from rich.style import Style
|
|
from rich.text import Span
|
|
|
|
import textual.widgets._markdown as MD
|
|
from textual.app import App, ComposeResult
|
|
from textual.widget import Widget
|
|
from textual.widgets import Markdown
|
|
from textual.widgets.markdown import MarkdownBlock
|
|
|
|
|
|
class UnhandledToken(MarkdownBlock):
|
|
def __init__(self, markdown: Markdown, token: Token) -> None:
|
|
super().__init__(markdown)
|
|
self._token = token
|
|
|
|
def __repr___(self) -> str:
|
|
return self._token.type
|
|
|
|
|
|
class FussyMarkdown(Markdown):
|
|
def unhandled_token(self, token: Token) -> MarkdownBlock | None:
|
|
return UnhandledToken(self, token)
|
|
|
|
|
|
class MarkdownApp(App[None]):
|
|
def __init__(self, markdown: str) -> None:
|
|
super().__init__()
|
|
self._markdown = markdown
|
|
|
|
def compose(self) -> ComposeResult:
|
|
yield FussyMarkdown(self._markdown)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
["document", "expected_nodes"],
|
|
[
|
|
# Basic markup.
|
|
("", []),
|
|
("# Hello", [MD.MarkdownH1]),
|
|
("## Hello", [MD.MarkdownH2]),
|
|
("### Hello", [MD.MarkdownH3]),
|
|
("#### Hello", [MD.MarkdownH4]),
|
|
("##### Hello", [MD.MarkdownH5]),
|
|
("###### Hello", [MD.MarkdownH6]),
|
|
("---", [MD.MarkdownHorizontalRule]),
|
|
("Hello", [MD.MarkdownParagraph]),
|
|
("Hello\nWorld", [MD.MarkdownParagraph]),
|
|
("> Hello", [MD.MarkdownBlockQuote, MD.MarkdownParagraph]),
|
|
("- One\n-Two", [MD.MarkdownBulletList, MD.MarkdownParagraph]),
|
|
(
|
|
"1. One\n2. Two",
|
|
[MD.MarkdownOrderedList, MD.MarkdownParagraph, MD.MarkdownParagraph],
|
|
),
|
|
(" 1", [MD.MarkdownFence]),
|
|
("```\n1\n```", [MD.MarkdownFence]),
|
|
("```python\n1\n```", [MD.MarkdownFence]),
|
|
("""| One | Two |\n| :- | :- |\n| 1 | 2 |""", [MD.MarkdownTable]),
|
|
# Test for https://github.com/Textualize/textual/issues/2676
|
|
(
|
|
"- One\n```\nTwo\n```\n- Three\n",
|
|
[
|
|
MD.MarkdownBulletList,
|
|
MD.MarkdownParagraph,
|
|
MD.MarkdownFence,
|
|
MD.MarkdownBulletList,
|
|
MD.MarkdownParagraph,
|
|
],
|
|
),
|
|
],
|
|
)
|
|
async def test_markdown_nodes(
|
|
document: str, expected_nodes: list[Widget | list[Widget]]
|
|
) -> None:
|
|
"""A Markdown document should parse into the expected Textual node list."""
|
|
|
|
def markdown_nodes(root: Widget) -> Iterator[MarkdownBlock]:
|
|
for node in root.children:
|
|
if isinstance(node, MarkdownBlock):
|
|
yield node
|
|
yield from markdown_nodes(node)
|
|
|
|
async with MarkdownApp(document).run_test() as pilot:
|
|
assert [
|
|
node.__class__ for node in markdown_nodes(pilot.app.query_one(Markdown))
|
|
] == expected_nodes
|
|
|
|
|
|
async def test_softbreak_split_links_rendered_correctly() -> None:
|
|
"""Test for https://github.com/Textualize/textual/issues/2805"""
|
|
|
|
document = """\
|
|
My site [has
|
|
this
|
|
URL](https://example.com)\
|
|
"""
|
|
async with MarkdownApp(document).run_test() as pilot:
|
|
markdown = pilot.app.query_one(Markdown)
|
|
paragraph = markdown.children[0]
|
|
assert isinstance(paragraph, MD.MarkdownParagraph)
|
|
assert paragraph._text.plain == "My site has this URL"
|
|
expected_spans = [
|
|
Span(8, 11, Style(meta={"@click": "link('https://example.com')"})),
|
|
Span(11, 12, Style(meta={"@click": "link('https://example.com')"})),
|
|
Span(12, 16, Style(meta={"@click": "link('https://example.com')"})),
|
|
Span(16, 17, Style(meta={"@click": "link('https://example.com')"})),
|
|
Span(17, 20, Style(meta={"@click": "link('https://example.com')"})),
|
|
]
|
|
|
|
assert paragraph._text.spans == expected_spans
|
|
|
|
|
|
async def test_load_non_existing_file() -> None:
|
|
"""Loading a file that doesn't exist should result in the obvious error."""
|
|
async with MarkdownApp("").run_test() as pilot:
|
|
with pytest.raises(FileNotFoundError):
|
|
await pilot.app.query_one(Markdown).load(
|
|
Path("---this-does-not-exist---.it.is.not.a.md")
|
|
)
|