feat(gui): one-click Close in its own bottom sidebar section

Close is now a direct shutdown trigger: visiting the Close page (the
sidebar entry) fires shutdown_app() immediately — no confirm step, no
intermediate body. The farewell overlay paints and os._exit(0) lands
~1s later from a daemon thread.

Layout: Close moved into its own bottom-of-sidebar section so the
destructive action is visually separated from Account/Activate.

- New shutdown_app() in components/_legacy.py replaces quit_button.
  os._exit thread is skipped when "pytest" is in sys.modules so the
  test suite doesn't suicide on rendering 99_Close.
- pages/99_Close.py shrinks to set_page_config + chrome + shutdown_app.
- app.py nav grows a new "Close" section header (new
  nav.section_close key in en/es packs) pinned at the bottom of the
  navigation dict.

Tests updated:
- TestQuitButtonRenders → TestClosePageShutsDownImmediately.
  Assert the shutdown caption renders + no confirm button exists.
- test_smoke EXPECTED_SUBSTRINGS["99_Close"] now pins
  "Shutting down" / "Cerrando" (the visible page body) instead of
  the removed page title.

2008 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 20:17:14 +00:00
parent ff2eaeb6c4
commit c568aec8a7
8 changed files with 59 additions and 63 deletions

View File

@@ -122,38 +122,33 @@ class TestLocalizedChrome:
# Quit / Close page
# ---------------------------------------------------------------------------
class TestQuitButtonRenders:
"""The Close page must show the localized title, body, and the
Close-the-app button. We don't actually click the button — that
would call ``os._exit(0)`` and kill the test process. We only
assert the button is present and its label is localized."""
class TestClosePageShutsDownImmediately:
"""The Close page no longer renders a confirm button — visiting the
page IS the close action. Under pytest the ``os._exit`` thread is
skipped (so the test runner doesn't suicide), but the farewell
success caption still renders and we assert against it."""
def test_close_page_english(self, app_factory):
def test_english_close_renders_shutdown_message(self, app_factory):
app = app_factory("99_Close")
app.run()
text = collected_text(app)
assert "Close DataTools" in text
assert "Shutting down" in text or "Goodbye" in text, (
f"Close page missing shutdown caption; got:\n{text[:300]}"
)
# No confirm button — the page is the action.
labels = [b.label for b in app.button]
assert any("Close the app" in lbl for lbl in labels), (
f"Close-the-app button missing; buttons: {labels}"
assert not any("Close the app" in lbl for lbl in labels), (
f"Close page should not render a confirm button; got: {labels}"
)
def test_close_page_spanish(self, app_factory):
def test_spanish_close_renders_localized_shutdown_message(self, app_factory):
app = app_factory("99_Close")
with_language(app, "es")
app.run()
text = collected_text(app)
assert "Cerrar DataTools" in text
labels = [b.label for b in app.button]
assert any("Cerrar la app" in lbl for lbl in labels), (
f"Spanish Close button missing; buttons: {labels}"
# ``quit.shutting_down`` in the es pack.
assert "Cerrando" in text or "Apagando" in text or "Adiós" in text, (
f"Spanish Close page missing localized shutdown caption; got:\n{text[:300]}"
)
def test_close_body_describes_unsaved_work_warning_es(self, app_factory):
app = app_factory("99_Close")
with_language(app, "es")
app.run()
text = collected_text(app)
assert "trabajo sin guardar" in text

View File

@@ -61,7 +61,7 @@ EXPECTED_SUBSTRINGS: dict[str, dict[str, str]] = {
"7_Multi_File_Merger": {"en": "Combine Files", "es": "Combine Files"},
"8_Validator_Reporter": {"en": "Quality Check", "es": "Quality Check"},
"9_Pipeline_Runner": {"en": "Automated", "es": "Automated"},
"99_Close": {"en": "Close DataTools", "es": "Cerrar DataTools"},
"99_Close": {"en": "Shutting down", "es": "Cerrando"},
}