wapdown/tests/test_config.py
randogoth 3b728d9d74 Make --- divide cards, and stop drawing ASCII rules
WML has no horizontal rule element -- its entire %layout entity is
`<br>` -- so a thematic break was being rendered as `<p>------------</p>`,
a hardcoded twelve-dash guess at screen width that wraps into nonsense on
a narrow device and is short on a wide one. It was the one construct
where the renderer invented a width out of nothing.

A `---` already looks like a divider and reads as one in every Markdown
document ever written, so it now is one: it starts a new card, by
default. Rules are never drawn, whether or not they divide.

Thematic breaks and `{.card Title}` markers are now a single mechanism --
an explicit divider, optionally carrying a title -- and compose freely in
one document. Both still win over --split-level. A section takes its
title from its own first heading when the divider does not supply one, so
the common shape needs no titles at all:

    Intro on the menu.

    ---

    ## Weather

    Cold and clear.

`--no-split-on-rule` / `split_on_rule: false` opts out of the splitting;
it does not bring the dashes back.

Three edges this opened, each fixed here rather than left to bite:

- Setext headings. `Heading` over `---` is an H2 in Markdown, but the
  parser was ATX-only, so it produced a paragraph plus a rule. Once a
  rule divides cards, a setext document would have split at every heading
  and stranded each heading as body copy on the card before it. Setext
  headings are now parsed, so such a document keeps its structure.
- Empty sections. A trailing `---`, or two in a row, produced an empty
  "Untitled" card and a menu entry leading to it. Sections with no
  renderable content are dropped.
- The multi-card threshold was two sections, so a document with a single
  divider collapsed back into one card. Any explicit divider now makes a
  multi-card deck: `A --- B` asks for two screens.

Also fixes a latent frontmatter bug this made far more likely: a document
opening with `---` and no `key: value` lines had its opening swallowed as
if the break were a frontmatter fence. A fenced block with no keys in it
is not frontmatter.

222 tests. Rendering examples/trail.md still matches md2txt's committed
output byte for byte; no golden file contains an ASCII rule any more.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-23 09:31:51 +03:00

96 lines
3.8 KiB
Python

"""Configuration layering: CLI flag > frontmatter > default."""
from __future__ import annotations
import pytest
from wapdown.config import DEFAULT_MAX_CARD_BYTES, ConfigError, DeckConfig
class TestPrecedence:
def test_defaults_when_nothing_is_given(self):
config = DeckConfig.resolve()
assert config.split_level == 0
assert config.max_card_bytes == DEFAULT_MAX_CARD_BYTES
assert config.menu is True
assert config.menu_style == "links"
assert config.title is None
def test_frontmatter_beats_default(self):
config = DeckConfig.resolve({"split_level": "2", "title": "Trail"})
assert config.split_level == 2
assert config.title == "Trail"
def test_cli_beats_frontmatter(self):
config = DeckConfig.resolve({"split_level": "2"}, {"split_level": 3})
assert config.split_level == 3
def test_absent_cli_option_does_not_clobber_frontmatter(self):
# argparse hands every unset option through as None; that must mean
# "not given" rather than "set to nothing".
config = DeckConfig.resolve({"title": "Trail"}, {"title": None, "menu": None})
assert config.title == "Trail"
assert config.menu is True
def test_unknown_frontmatter_key_warns_but_does_not_fail(self, capsys):
config = DeckConfig.resolve({"hyphenate": "true", "title": "T"})
assert config.title == "T"
assert "unknown frontmatter key 'hyphenate'" in capsys.readouterr().err
class TestCoercion:
@pytest.mark.parametrize("raw", ["true", "yes", "1", "on", "TRUE"])
def test_truthy_strings(self, raw):
assert DeckConfig.resolve({"menu": raw}).menu is True
@pytest.mark.parametrize("raw", ["false", "no", "0", "off", "FALSE"])
def test_falsy_strings(self, raw):
assert DeckConfig.resolve({"menu": raw}).menu is False
def test_menu_style_is_case_insensitive(self):
assert DeckConfig.resolve({"menu_style": "SELECT"}).menu_style == "select"
def test_hyphenated_frontmatter_key_is_accepted(self):
assert DeckConfig.resolve({"split-level": "2"}).split_level == 2
def test_empty_title_becomes_none(self):
assert DeckConfig.resolve({"title": ""}).title is None
class TestValidation:
@pytest.mark.parametrize("value", [-1, 7])
def test_split_level_out_of_range(self, value):
with pytest.raises(ConfigError, match="split_level"):
DeckConfig.resolve(overrides={"split_level": value})
def test_negative_byte_budget(self):
with pytest.raises(ConfigError, match="max_card_bytes"):
DeckConfig.resolve(overrides={"max_card_bytes": -1})
def test_unknown_menu_style(self):
with pytest.raises(ConfigError, match="menu_style"):
DeckConfig.resolve(overrides={"menu_style": "carousel"})
def test_unknown_image_policy(self):
with pytest.raises(ConfigError, match="images"):
DeckConfig.resolve(overrides={"images": "wbmp"})
def test_non_numeric_split_level_in_frontmatter(self):
# Previously an uncaught ValueError escaping as a traceback.
with pytest.raises(ConfigError, match="split_level must be a number"):
DeckConfig.resolve({"split_level": "deep"})
def test_non_boolean_menu_in_frontmatter(self):
with pytest.raises(ConfigError, match="menu must be a boolean"):
DeckConfig.resolve({"menu": "sometimes"})
class TestSplitOnRule:
def test_defaults_to_on(self):
assert DeckConfig.resolve().split_on_rule is True
def test_frontmatter_can_disable(self):
assert DeckConfig.resolve({"split_on_rule": "false"}).split_on_rule is False
def test_flag_overrides_frontmatter(self):
config = DeckConfig.resolve({"split_on_rule": "true"}, {"split_on_rule": False})
assert config.split_on_rule is False