diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index bbdf36d..a6dd778 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -1,7 +1,8 @@ name: Demo # Publishes the demo page to GitHub Pages: the CLI collects the made-up scenario in -# demo/scenario.py, so the page and its report.json match the released code. +# demo/scenario.py, so the page and its report.json match the released code. The +# screenshot.png it publishes is the image the README shows. on: push: tags: @@ -22,7 +23,8 @@ jobs: - run: uv python install 3.13 - run: uv python pin 3.13 - run: just install - - run: just demo + - run: uv run --no-sync playwright install --with-deps chromium + - run: just demo-screenshot - uses: actions/upload-pages-artifact@v5 with: path: site/ diff --git a/AGENTS.md b/AGENTS.md index 57eb63b..afe897f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ Besides the translation checks in `tests/test_page.py`, it has browser smoke tes `smoke` and run by `just test-smoke`, not `just test`; still check a page change in a browser against a report from the CLI tests, or against `just demo-serve`. `demo/` holds the made-up scenario behind the published demo page: the CLI collects it against respx routes, and `.github/workflows/demo.yml` publishes it to GitHub Pages on each release -tag. A page change that adds a state belongs in the scenario too. Tests +tag, with the `screenshot.png` the README shows. A page change that adds a state belongs in the scenario too. Tests mock GitLab and Jira only with respx routes (pytest-httpx2): the `gitlab` and `jira` fixtures in `tests/conftest.py` declare one named static route per call of the scenario in `tests/payloads.py`. A test changes a response by re-mocking a named route; add no fakes, callbacks, or stubs. @@ -39,6 +39,9 @@ description, and PyPI does not rewrite relative links, so a relative one 404s on [`skills/release-scope/SKILL.md`](skills/release-scope/SKILL.md) describes the CLI flags, exit codes, and report fields for agents, and pins the minor version it describes. Update it, and its version range, in the same change as any of those. +A change to the report schema bumps `SCHEMA_VERSION` and ships in a new minor release; anything else is a patch. +Move the skill's version range and the README's `release-scope>=X.Y, None: + page.set_viewport_size(_VIEWPORT) + page.goto(f"{url}#issues=SHOP-38") + page.locator("details.service .chip:visible").first.wait_for() + page.screenshot(path=output, clip=_CLIP) + + +def main(site: pathlib.Path) -> None: + with serve(site) as url, sync_playwright() as playwright: + browser: typing.Final = playwright.chromium.launch() + page: typing.Final = browser.new_page(device_scale_factor=2, color_scheme="light", locale="en-US") + capture(page, url, site / "screenshot.png") + browser.close() + + +if __name__ == "__main__": + main(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "site")) diff --git a/demo/server.py b/demo/server.py new file mode 100644 index 0000000..9465630 --- /dev/null +++ b/demo/server.py @@ -0,0 +1,26 @@ +import collections.abc +import contextlib +import functools +import http.server +import pathlib +import threading +import typing + + +class _QuietHandler(http.server.SimpleHTTPRequestHandler): + def log_message(self, format: str, *args: typing.Any) -> None: # noqa: A002, ANN401 + pass + + +@contextlib.contextmanager +def serve(directory: pathlib.Path) -> collections.abc.Generator[str, None, None]: + server: typing.Final = http.server.ThreadingHTTPServer( + ("127.0.0.1", 0), functools.partial(_QuietHandler, directory=str(directory)) + ) + thread: typing.Final = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + try: + yield f"http://127.0.0.1:{server.server_port}/" + finally: + server.shutdown() + server.server_close() diff --git a/docs/images/demo.png b/docs/images/demo.png deleted file mode 100644 index e84cda0..0000000 Binary files a/docs/images/demo.png and /dev/null differ diff --git a/justfile b/justfile index 5d740ab..53af240 100644 --- a/justfile +++ b/justfile @@ -47,6 +47,11 @@ test-smoke *args: demo: uv run --no-sync python -m demo.build site +# Build the demo and save site/screenshot.png, the image the README shows; installs Chromium on first run. +demo-screenshot: demo + uv run --no-sync playwright install chromium + uv run --no-sync python -m demo.screenshot site + # Build the demo and serve it at http://127.0.0.1:8000. demo-serve: demo uv run --no-sync python -m http.server 8000 --bind 127.0.0.1 --directory site diff --git a/tests/test_browser.py b/tests/test_browser.py index da85f2b..fb75d6e 100644 --- a/tests/test_browser.py +++ b/tests/test_browser.py @@ -1,9 +1,6 @@ import collections.abc -import functools -import http.server import pathlib import re -import threading import typing import pytest @@ -12,6 +9,8 @@ from typer.testing import CliRunner from demo.build import build +from demo.screenshot import capture +from demo.server import serve from release_scope import ioc from release_scope.__main__ import MAIN_APP from tests.payloads import ENDPOINT, JIRA_ENDPOINT @@ -20,11 +19,6 @@ pytestmark = pytest.mark.smoke -class _QuietHandler(http.server.SimpleHTTPRequestHandler): - def log_message(self, format: str, *args: typing.Any) -> None: # noqa: A002, ANN401 - pass - - @pytest.fixture def site(jira: respx.Router, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path) -> collections.abc.Iterator[str]: monkeypatch.setenv("GITLAB_TOKEN", "glpat-test") @@ -37,25 +31,16 @@ def site(jira: respx.Router, monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib. result = CliRunner().invoke(MAIN_APP, ["collect", "-g", "team", "-o", str(output)]) assert result.exit_code == 0, result.output assert jira["jira_search"].called - yield from _serve(output) + with serve(output) as url: + yield url @pytest.fixture def demo_site(tmp_path: pathlib.Path) -> collections.abc.Iterator[str]: output: typing.Final = tmp_path / "demo" build(output) - yield from _serve(output) - - -def _serve(directory: pathlib.Path) -> collections.abc.Iterator[str]: - server: typing.Final = http.server.ThreadingHTTPServer( - ("127.0.0.1", 0), functools.partial(_QuietHandler, directory=str(directory)) - ) - thread: typing.Final = threading.Thread(target=server.serve_forever, daemon=True) - thread.start() - yield f"http://127.0.0.1:{server.server_port}/" - server.shutdown() - server.server_close() + with serve(output) as url: + yield url @pytest.fixture @@ -134,3 +119,15 @@ def test_demo_lists_its_services_and_picks_tags_for_an_issue(page: Page, demo_si _issue(page, "SHOP-38").check() expect(page).to_have_url(re.compile(r"#issues=SHOP-38&shop/api=2\.5\.0&shop/web=1\.9\.0$")) assert errors == [] + + +def test_demo_screenshot_shows_an_issue_with_its_picks( + page: Page, demo_site: str, errors: list[str], tmp_path: pathlib.Path +) -> None: + output: typing.Final = tmp_path / "screenshot.png" + + capture(page, demo_site, output) + + assert output.read_bytes().startswith(b"\x89PNG") + expect(page.locator("details.service .chip:visible > span")).to_have_text(["2.5.0", "1.9.0"]) + assert errors == []