From 0e26c37b19ee9c99ab0bef90e7e88d51fe5f3eb1 Mon Sep 17 00:00:00 2001 From: Mohammed Anas Nathani Date: Thu, 23 Jul 2026 05:56:13 +0530 Subject: [PATCH] fix(a11y): descriptive image alt text in docs and generated tutorials - README badges: CI tests status, Contributor count vs avatars - examples/README: describe what each demo shows, not the filename - tutorial.md.j2: interpolate {{ title }} into the demo.gif alt so every generated tutorial is unique for screen readers and SEO Closes #197 --- README.md | 6 +- examples/README.md | 4 +- src/readme2demo/templates/tutorial.md.j2 | 2 +- tests/test_tutorial.py | 73 ++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f04630d..c7c2993 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # readme2demo — verified tutorials & demo videos from your README -[![tests](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml/badge.svg)](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml) +[![CI tests status](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml/badge.svg)](https://github.com/alphacrack/readme2demo/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/alphacrack/readme2demo/blob/main/LICENSE) [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://github.com/alphacrack/readme2demo/blob/main/pyproject.toml) @@ -183,9 +183,9 @@ MIT licensed. The CLI and verification pipeline are, and will stay, free and ope A huge thank you to everyone who has contributed to readme2demo! -[![Contributors](https://img.shields.io/github/contributors/alphacrack/readme2demo?style=flat-square)](https://github.com/alphacrack/readme2demo/graphs/contributors) +[![Contributor count](https://img.shields.io/github/contributors/alphacrack/readme2demo?style=flat-square)](https://github.com/alphacrack/readme2demo/graphs/contributors) - Contributors + Contributor avatars diff --git a/examples/README.md b/examples/README.md index faa5851..adacf16 100644 --- a/examples/README.md +++ b/examples/README.md @@ -29,11 +29,11 @@ readme2demo run against its own repo: install, run the tests, explore the CLI, and read a verified example — each step replayed in a fresh container before this was rendered. -![readme2demo self-demo](readme2demo/demo.gif) +![Verified demo: readme2demo installing and running its own CLI in a clean container](readme2demo/demo.gif) ### toolhive — another example -![toolhive demo](toolhive/demo.gif) +![Verified demo: installing and running stacklok/toolhive step by step](toolhive/demo.gif) ## What's in each folder diff --git a/src/readme2demo/templates/tutorial.md.j2 b/src/readme2demo/templates/tutorial.md.j2 index 1ed5641..01f3457 100644 --- a/src/readme2demo/templates/tutorial.md.j2 +++ b/src/readme2demo/templates/tutorial.md.j2 @@ -43,7 +43,7 @@ Expected output: {% if has_video %} ## Demo -![Demo video: every step above executing in a clean container](demo.gif) +![Demo video: {{ title }} — every step above executing in a clean container](demo.gif) Full video: [demo.mp4](demo.mp4) diff --git a/tests/test_tutorial.py b/tests/test_tutorial.py index 580a27d..d6486d1 100644 --- a/tests/test_tutorial.py +++ b/tests/test_tutorial.py @@ -456,6 +456,79 @@ def test_tutorial_md_front_matter_and_provenance(tmp_path, monkeypatch): assert "generator: readme2demo" in sbs +def test_tutorial_md_demo_alt_is_repo_specific(tmp_path, monkeypatch): + """Regression (#197): demo.gif alt must include the tutorial title, not a generic constant. + + Every generated tutorial used the same hardcoded alt string, so screen + readers (and SEO) could not tell one repo's demo from another. + """ + from readme2demo import llm as llm_mod + from readme2demo.tutorial import run_tutorial + from readme2demo.types import ( + AgentResult, CommandLog, Plan, SuccessCriteria, TutorialOutline, + TutorialStep, + ) + + outline = TutorialOutline( + title="Install ToolHive", + intro="A container tool.", + steps=[TutorialStep(title="Run", command="thv --help", explanation="Shows help.")], + ) + monkeypatch.setattr( + llm_mod, "complete_json", lambda *a, **k: (outline.model_copy(deep=True), 0.01) + ) + plan = Plan( + quickstart_summary="q", + success_criteria=SuccessCriteria(command="thv --help"), + prereqs=[], + ) + log = CommandLog(engine="claude-code", result=AgentResult(outcome="success")) + # has_video is a filesystem check — create a gif so the Demo block renders. + (tmp_path / "demo.gif").write_bytes(b"GIF89a" + b"\x00" * 100) + run_tutorial( + tmp_path, plan, log, outline, "m", verified=True, base_image="img", + commit_sha="abc1234", repo_url="https://github.com/stacklok/toolhive", + ) + text = (tmp_path / "tutorial.md").read_text(encoding="utf-8") + assert "![Demo video: Install ToolHive" in text + assert "every step above executing in a clean container](demo.gif)" in text + # Must not ship the old generic-only alt (title missing). + assert "![Demo video: every step above executing in a clean container](demo.gif)" not in text + + +def test_tutorial_md_demo_alt_guide_only_has_title(tmp_path, monkeypatch): + """Regression (#197): guide-only runs (empty repo_url) still get sensible alt.""" + from readme2demo import llm as llm_mod + from readme2demo.tutorial import run_tutorial + from readme2demo.types import ( + AgentResult, CommandLog, Plan, SuccessCriteria, TutorialOutline, + TutorialStep, + ) + + outline = TutorialOutline( + title="Guide-only walkthrough", + intro="No repo.", + steps=[TutorialStep(title="Run", command="echo hi", explanation="Says hi.")], + ) + monkeypatch.setattr( + llm_mod, "complete_json", lambda *a, **k: (outline.model_copy(deep=True), 0.01) + ) + plan = Plan( + quickstart_summary="q", + success_criteria=SuccessCriteria(command="echo hi"), + ) + log = CommandLog(engine="claude-code", result=AgentResult(outcome="success")) + (tmp_path / "demo.gif").write_bytes(b"GIF89a" + b"\x00" * 100) + run_tutorial( + tmp_path, plan, log, outline, "m", verified=True, base_image="img", + commit_sha=None, repo_url="", + ) + text = (tmp_path / "tutorial.md").read_text(encoding="utf-8") + assert "![Demo video: Guide-only walkthrough" in text + assert "— —" not in text # no stray double dash from empty fields + + + def test_guide_front_matter_does_not_break_tape_parsing(tmp_path): from readme2demo.distill import parse_guide_steps