From a1858705f56f0109b448e1d5872cab326c9cc0ce Mon Sep 17 00:00:00 2001 From: nelsonduarte Date: Sun, 13 Sep 2026 14:48:36 +0100 Subject: [PATCH] fix: point the Help button at the canonical documentation URL The Help ("?") button in the title bar opened https://nelsonduarte.github.io/PDFApps/#guide, a URL whose target section no longer exists. This is shipped in 1.15.0, so every user on the current release has a dead Help button. History of the breakage, verified with git log -S rather than assumed: * b14d11d added the button and, in the same commit, the
it pointed at, together with its nav entry. The link worked when written. * 6847858 rewrote the repo path from PDFApps-en to PDFApps two days later. That is the form that shipped. * 350a419 replaced the single-page site with the five-page layout and deleted both the #guide section and its nav link. Nothing re-added them. The button therefore worked for about two weeks and broke in the website redesign. The later migration to Cloudflare Pages was not the cause; it changed the canonical host but did not remove the anchor. The failure is silent, which is why it survived this long. The old nelsonduarte.github.io host still answers 200 and serves the current five-page content, so there is no 404 to expose it. An unknown fragment is not an error in HTTP or in any browser: the user simply landed at the top of a page that no longer had the section, with nothing to signal the link was dead. The new target is the canonical URL the site itself declares, so it also avoids the 308 redirect the old host would issue. Adds tests/test_help_url_domain.py, which guards the whole class of stale-domain regressions by forbidding the retired host across Python and JSON sources under app/, and pins the Help URL to the canonical host and docs path while leaving the section anchor free to change. No version strings were touched. Co-Authored-By: Claude Opus 4.8 --- app/window.py | 2 +- tests/test_help_url_domain.py | 116 ++++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 tests/test_help_url_domain.py diff --git a/app/window.py b/app/window.py index 4693ce4..715e209 100644 --- a/app/window.py +++ b/app/window.py @@ -237,7 +237,7 @@ def _a11y(btn, tip): self._help_btn.setObjectName("theme_btn") _a11y(self._help_btn, t("help.tip")) self._help_btn.setFixedSize(28, 28) - self._help_btn.clicked.connect(lambda: __import__('webbrowser').open("https://nelsonduarte.github.io/PDFApps/#guide")) + self._help_btn.clicked.connect(lambda: __import__('webbrowser').open("https://pdf-apps.com/docs#first-steps")) wb_h.addWidget(self._help_btn) _lang_labels = {"en": "EN", "pt": "PT", "es": "ES", "fr": "FR", "de": "DE", "zh": "ZH", "it": "IT", "nl": "NL"} diff --git a/tests/test_help_url_domain.py b/tests/test_help_url_domain.py new file mode 100644 index 0000000..2df3ad5 --- /dev/null +++ b/tests/test_help_url_domain.py @@ -0,0 +1,116 @@ +"""Guards against user-facing URLs in app/ pointing at a retired domain. + +History of the bug this test exists for (verified with ``git log -S``): + +* ``b14d11d`` (2026-03-25) added the Help ("?") button and, in the *same* + commit, the ``
`` plus its ``
  • `` + nav entry in ``docs/index.html``. The target the button pointed at was + created alongside it, so the link worked. +* ``6847858`` (2026-03-27) rewrote the URL's repo path from ``PDFApps-en`` + to ``PDFApps``. That is the form that shipped in 1.15.0. (The original + ``/PDFApps-en/`` path is a 404 today, so only this commit made the URL + point at a live page.) +* ``350a419`` (2026-04-08) replaced the single-page site with the five-page + layout and, in doing so, deleted both the ``#guide`` section and its nav + link. Nothing has re-added them since. + +So the Help button worked for about two weeks and broke in the *website +redesign*, not in the later migration to Cloudflare Pages. The migration +changed the canonical host to ``pdf-apps.com`` but was not the cause; do +not re-attribute this to Cloudflare. + +Why the breakage is silent, and therefore why this test exists: the old +``nelsonduarte.github.io/PDFApps/`` host still answers **200**. It does not +404, and it is not a stale snapshot either: it now serves the current +five-page content, whose navigation no longer contains a ``#guide`` anchor. +An unknown fragment is not an error in HTTP or in any browser, so the user +silently landed at the top of a page that no longer had the section, with +nothing anywhere to signal the link was dead. No status code, no log, no +crash can catch this class of bug; only an assertion like the ones below. + +These tests are deliberately scoped to the *host*, not to the full URL: +asserting the exact string would turn any legitimate anchor change (e.g. +``#first-steps`` -> ``#install``) into a red test, which would block a valid +edit. Matching ``github.io`` catches the whole class of stale-domain +regressions in one assertion and survives anchor churn. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +ROOT = Path(__file__).resolve().parent.parent +APP = ROOT / "app" + +# The retired documentation/website host. Note this is intentionally narrow: +# api.github.com and github.com/... links are live and legitimate (updater, +# OCR help), so only the Pages host is forbidden. +DEAD_HOST = "github.io" + +CANONICAL_HOST = "pdf-apps.com" + + +def _python_sources() -> list[Path]: + return sorted(p for p in APP.rglob("*.py") if "__pycache__" not in p.parts) + + +def test_no_app_source_references_the_retired_pages_domain(): + """No Python source under app/ may link to the old GitHub Pages host.""" + offenders: list[str] = [] + for path in _python_sources(): + for lineno, line in enumerate( + path.read_text(encoding="utf-8").splitlines(), start=1 + ): + if DEAD_HOST in line: + offenders.append(f"{path.relative_to(ROOT)}:{lineno}: {line.strip()}") + + assert not offenders, ( + "app/ must not reference the retired " + f"{DEAD_HOST!r} site (canonical host is {CANONICAL_HOST}).\n" + + "\n".join(offenders) + ) + + +def _json_data_files() -> list[Path]: + return sorted(p for p in APP.rglob("*.json") if "__pycache__" not in p.parts) + + +def test_no_json_data_file_references_the_retired_pages_domain(): + """Shipped JSON data under app/ must not carry the dead host either. + + Scoped to every ``*.json`` under app/ rather than to translations.json + alone: today that glob resolves to exactly one file, so this does not + change what is currently checked, but it makes the guard cover any JSON + data file added later instead of silently exempting it. translations.json + remains the motivating case, being the one place a help link could be + reintroduced in 8 languages at once. + """ + offenders: list[str] = [] + for path in _json_data_files(): + if DEAD_HOST in path.read_text(encoding="utf-8"): + offenders.append(str(path.relative_to(ROOT))) + + assert not offenders, ( + f"JSON data under app/ must not reference {DEAD_HOST!r} " + f"(canonical host is {CANONICAL_HOST}): " + ", ".join(offenders) + ) + + +def test_help_button_opens_the_canonical_site(): + """The Help button's URL must live on the canonical host. + + Pinned to the host and the docs path only, so the section anchor stays + free to change without breaking this test. + """ + src = (APP / "window.py").read_text(encoding="utf-8") + match = re.search(r'_help_btn\.clicked\.connect\([^\n]*?open\("([^"]+)"\)', src) + assert match, "Could not locate the Help button's clicked->open(...) wiring." + + url = match.group(1) + assert url.startswith(f"https://{CANONICAL_HOST}/"), ( + f"Help URL {url!r} must point at https://{CANONICAL_HOST}/" + ) + assert "/docs" in url, ( + f"Help URL {url!r} should open the documentation page, not the landing page." + )