From 1ac9d7337ed459ce84b85a9393470de03cf7f430 Mon Sep 17 00:00:00 2001 From: Christopher Toth Date: Thu, 29 Oct 2026 12:13:18 -0600 Subject: [PATCH] Resolve existing localized documentation before opening its URI --- agent-notes/improve-documentation-opening.md | 26 +++++++++ app_elements/interface.py | 26 ++++----- tests/test_documentation.py | 56 ++++++++++++++++++++ 3 files changed, 95 insertions(+), 13 deletions(-) create mode 100644 agent-notes/improve-documentation-opening.md create mode 100644 tests/test_documentation.py diff --git a/agent-notes/improve-documentation-opening.md b/agent-notes/improve-documentation-opening.md new file mode 100644 index 0000000..ad37e0d --- /dev/null +++ b/agent-notes/improve-documentation-opening.md @@ -0,0 +1,26 @@ +# Goal + +Open an existing localized documentation file through a valid file URI. + +# State + +Isolated branch based on improve/integration-contract (PR #1). Existing action +name and bundled/source layouts are preserved. Tests cover exact, base-language +and root fallback, missing documentation and URI escaping. + +# Decisions and why + +Check readme.html itself rather than the locale directory. Use pathlib as_uri +for Windows separators, Unicode and reserved URL characters. An incomplete +locale folder no longer blocks a valid language/root document. Show a translated +warning when no document exists. No manifest field or remote fallback added. + +# Surprises + +The existing function opened nonexistent files whenever a localized directory +existed without readme.html. No actual packaged app was launched during this work. + +# Next step + +Publish and read PR CI; controlled browser calls and real temporary filesystem +fixtures do not establish packaged-product or live screen-reader behavior. diff --git a/app_elements/interface.py b/app_elements/interface.py index dd4dab6..6f80283 100644 --- a/app_elements/interface.py +++ b/app_elements/interface.py @@ -2,9 +2,9 @@ logger = getLogger("app_elements.interface") import datetime -import os import sys import webbrowser +from pathlib import Path import app_framework import app_framework.shutdown @@ -25,19 +25,19 @@ def view_documentation(): if paths.is_frozen(): - docpath = paths.embedded_data_path() + docpath = Path(paths.embedded_data_path()) else: - docpath = os.path.join(paths.app_path(), "..", "documentation") - locale = application.locale - if os.path.exists(os.path.join(docpath, locale)): - docpath = os.path.join(docpath, locale) - else: - locale = locale.split("_")[0] - if os.path.exists(os.path.join(docpath, locale)): - docpath = os.path.join(docpath, locale) - docpath = os.path.join(docpath, "readme.html") - docpath = "file://%s" % os.path.abspath(docpath) - webbrowser.open(docpath) + docpath = Path(paths.app_path()).parent / "documentation" + locale = application.locale.split(".")[0].split("@")[0].replace("-", "_") + for language in (locale, locale.split("_")[0], ""): + document = docpath / language / "readme.html" + if document.is_file(): + webbrowser.open(document.resolve().as_uri()) + return + popups.warning_message( + title=_("Documentation unavailable"), + message=_("Documentation could not be found. Check that the documentation files are installed with this application."), + ) def show_about_dialog(): diff --git a/tests/test_documentation.py b/tests/test_documentation.py new file mode 100644 index 0000000..92a8937 --- /dev/null +++ b/tests/test_documentation.py @@ -0,0 +1,56 @@ +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +from support import load_component + + +class DocumentationTests(unittest.TestCase): + def test_exact_then_language_then_root_fallback_uses_existing_file(self): + for available, expected in [ + (["fr_CA", "fr", ""], "fr_CA"), + (["fr", ""], "fr"), + ([""], ""), + ]: + with self.subTest(available=available), tempfile.TemporaryDirectory() as directory: + root = Path(directory) / "My App #1 café" + root.mkdir() + for language in available: + folder = root / language + folder.mkdir(exist_ok=True) + (folder / "readme.html").touch() + (root / "fr_CA").mkdir(exist_ok=True) + with load_component("app_elements.interface", locale="fr-CA.UTF-8") as (component, _, modules): + paths = modules["platform_utils.paths"] + paths.is_frozen.return_value = True + paths.embedded_data_path.return_value = str(root) + with patch.object(component.webbrowser, "open") as browser: + component.view_documentation() + browser.assert_called_once_with((root / expected / "readme.html").resolve().as_uri()) + modules["wx_utils.popups"].warning_message.assert_not_called() + + def test_source_run_uses_sibling_documentation_directory(self): + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + application_path = root / "Example" + application_path.mkdir() + docs = root / "documentation" + docs.mkdir() + (docs / "readme.html").touch() + with load_component("app_elements.interface") as (component, _, modules): + modules["platform_utils.paths"].app_path.return_value = str(application_path) + with patch.object(component.webbrowser, "open") as browser: + component.view_documentation() + browser.assert_called_once_with((docs / "readme.html").as_uri()) + + def test_missing_document_warns_instead_of_opening_a_broken_url(self): + with tempfile.TemporaryDirectory() as directory: + with load_component("app_elements.interface") as (component, _, modules): + paths = modules["platform_utils.paths"] + paths.is_frozen.return_value = True + paths.embedded_data_path.return_value = directory + with patch.object(component.webbrowser, "open") as browser: + component.view_documentation() + browser.assert_not_called() + modules["wx_utils.popups"].warning_message.assert_called_once()