"""US09-01: the manuals, in the browser, under the application's real policy. Everything here runs against a real ``photo_pipeline serve`` process serving the committed ``docs/`` tree and the vendored renderer — no fixture markdown, no stubbed fetch. What that buys is the assertion the story actually cares about: the pages render, the links between them work, a diagram becomes a diagram, and the console stays empty, including of CSP violations. The offline contract — reachability, dead links, pinned checksums — is ``tests/integration/test_documentation.py``. """ from __future__ import annotations from pathlib import Path import pytest from tests.e2e._pipeline_harness import Server, seed_library # Every check here belongs to the documentation gate (US09-05). pytestmark = pytest.mark.phase_i @pytest.fixture(scope="module") def server(tmp_path_factory): seeded = seed_library(tmp_path_factory.mktemp("docs"), {"a": 1}, {}) running = Server(seeded).start() try: yield running finally: running.stop() @pytest.fixture def quiet(page): """Any console error, page error, or failed request fails the test that caused it. A CSP violation arrives as a console error, which is the point.""" problems: list[str] = [] page.on("console", lambda m: problems.append(m.text) if m.type == "error" else None) page.on("pageerror", lambda error: problems.append(str(error))) page.on("requestfailed", lambda request: problems.append(f"failed request: {request.url}")) yield problems def test_the_documentation_opens_from_the_navigation(page, server, quiet): page.goto(f"{server.base}/app/#/workflow") page.locator('nav a[data-nav="docs"]').click() page.get_by_test_id("doc").wait_for() assert page.get_by_test_id("doc").get_attribute("data-page") == "index" # The sidebar is the index's own reading order, not a second list to maintain. assert page.get_by_test_id("doc-pages").get_by_role("link", name="Overview").is_visible() assert quiet == [] def test_a_link_between_documents_stays_inside_the_application(page, server, quiet): page.goto(f"{server.base}/app/#/docs") page.get_by_test_id("doc").wait_for() page.get_by_test_id("doc").get_by_role("link", name="Overview").click() page.wait_for_selector('[data-testid="doc"][data-page="overview"]') assert "#/docs?page=overview" in page.url, "a relative .md link must not leave the app" # And back again, by the link the document itself carries. page.get_by_test_id("doc").get_by_role("link", name="← Documentation index").click() page.wait_for_selector('[data-testid="doc"][data-page="index"]') assert quiet == [] def test_a_deep_link_to_a_heading_lands_on_that_heading(page, server, quiet): page.goto(f"{server.base}/app/#/docs?page=overview&anchor=the-workflow") heading = page.locator("#the-workflow") heading.wait_for() assert heading.inner_text().strip() == "The workflow" assert heading.evaluate("node => node.getBoundingClientRect().top < window.innerHeight") assert quiet == [] def test_a_mermaid_block_becomes_a_diagram_under_the_unchanged_script_policy(page, server, quiet): page.goto(f"{server.base}/app/#/docs?page=overview") diagram = page.get_by_test_id("diagram").first diagram.wait_for() # A real drawing, not the source text and not an error node. assert diagram.locator("svg").count() == 1 assert diagram.evaluate("node => node.querySelector('svg').getBBox().width") > 100 assert page.locator("pre code.language-mermaid").count() == 0 assert quiet == [], "rendering a diagram must not violate the policy" policy = page.evaluate( "async () => (await fetch('/app/')).headers.get('content-security-policy')" ) assert "script-src 'self';" in policy assert "unsafe-eval" not in policy def test_every_diagram_in_the_architecture_overview_draws(page, server, quiet): """Five diagrams, three of them state machines (US09-03). A mermaid block with a syntax error renders an error node instead of throwing, so a page that merely loaded proves nothing — count the drawings and read the source for the states.""" source = (Path(__file__).resolve().parents[2] / "docs" / "architecture.md").read_text() expected = source.count("```mermaid") assert expected >= 5, "the overview lost its diagrams" page.goto(f"{server.base}/app/#/docs?page=architecture") page.get_by_test_id("diagram").first.wait_for() page.wait_for_function( "count => document.querySelectorAll('[data-testid=\"diagram\"] svg').length === count", arg=expected, ) drawn = page.locator('[data-testid="diagram"] svg') assert drawn.count() == expected for index in range(expected): diagram = drawn.nth(index) # Labels may be or HTML inside a depending on the # diagram type, so ask for the rendered text rather than a specific element. labels = diagram.evaluate("node => node.textContent.trim().length") assert labels > 0, f"diagram {index} drew no labels" assert diagram.locator(".error-icon, .error-text").count() == 0, f"diagram {index} errored" assert page.locator("pre code.language-mermaid").count() == 0 assert quiet == [] def test_an_unknown_page_says_so_without_naming_a_path(page, server, quiet): page.goto(f"{server.base}/app/#/docs?page=no-such-manual") message = page.get_by_test_id("doc-not-found") message.wait_for() text = message.inner_text() assert "does not exist" in text assert "/" not in text.replace("Back to the documentation index", ""), text message.get_by_role("link").click() page.wait_for_selector('[data-testid="doc"][data-page="index"]') # The missing page is a 404 and the browser says so; nothing else may go wrong, # and in particular the view must not throw on the way to its own message. assert all("404" in problem for problem in quiet), quiet def test_the_documentation_is_readable_without_a_session(page, server, quiet): """The troubleshooting page is needed most by whoever is locked out.""" page.goto(f"{server.base}/app/#/docs") page.get_by_test_id("doc").wait_for() unauthenticated = page.evaluate( """async () => { const response = await fetch('/docs/index.md', { credentials: 'omit' }); return { status: response.status, length: (await response.text()).length }; }""" ) assert unauthenticated["status"] == 200 assert unauthenticated["length"] > 100