Repository: Textualize/textual

Issue:
Screen.dismiss should error if not the top-most screen
At the moment if you call `dismiss` on a screen that is *not* the top-most on the stack, strange things will happen.

I think the only solution here is for that to be an error.

i.e. if I push screens A, B, C on to the stack. Then I call `B.dismiss()` that should raise a `DismissError` (name TBD). Only `C.dismiss()` is valid, because it is a the top of the stack.

Discussion:

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5991f818d..bce29efda 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 - App `title` and `sub_title` attributes can be set to any type https://github.com/Textualize/textual/issues/2521
 - Using `Widget.move_child` where the target and the child being moved are the same is now a no-op https://github.com/Textualize/textual/issues/1743
+- Calling `dismiss` on a screen that is not at the top of the stack now raises an exception https://github.com/Textualize/textual/issues/2575
 
 ### Fixed
 
diff --git a/src/textual/app.py b/src/textual/app.py
index 12f66cf04..c1376cd22 100644
--- a/src/textual/app.py
+++ b/src/textual/app.py
@@ -156,7 +156,7 @@ class ScreenError(Exception):
 
 
 class ScreenStackError(ScreenError):
-    """Raised when attempting to pop the last screen from the stack."""
+    """Raised when trying to manipulate the screen stack incorrectly."""
 
 
 class CssPathError(Exception):
diff --git a/src/textual/screen.py b/src/textual/screen.py
index af0b006be..09505cc53 100644
--- a/src/textual/screen.py
+++ b/src/textual/screen.py
@@ -771,6 +771,10 @@ class Screen(Generic[ScreenResultType], Widget):
         Args:
             result: The optional result to be passed to the result callback.
 
+        Raises:
+            ScreenStackError: If trying to dismiss a screen that is not at the top of
+                the stack.
+
         Note:
             If the screen was pushed with a callback, the callback will be
             called with the given result and then a call to
@@ -778,6 +782,12 @@ class Screen(Generic[ScreenResultType], Widget):
             no callback was provided calling this method is the same as
             simply calling [`App.pop_screen`][textual.app.App.pop_screen].
         """
+        if self is not self.app.screen:
+            from .app import ScreenStackError
+
+            raise ScreenStackError(
+                f"Can't dismiss screen {self} that's not at the top of the stack."
+            )
         if result is not self._NoResult and self._result_callbacks:
             self._result_callbacks[-1](cast(ScreenResultType, result))
         self.app.pop_screen()


Test patch:
diff --git a/tests/test_screens.py b/tests/test_screens.py
index 2e3dbfcbe..5b29b1dd5 100644
--- a/tests/test_screens.py
+++ b/tests/test_screens.py
@@ -192,3 +192,17 @@ async def test_auto_focus():
         assert app.focused is None
         app.pop_screen()
         assert app.focused.id == "two"
+
+
+async def test_dismiss_non_top_screen():
+    class MyApp(App[None]):
+        async def key_p(self) -> None:
+            self.bottom, top = Screen(), Screen()
+            await self.push_screen(self.bottom)
+            await self.push_screen(top)
+
+    app = MyApp()
+    async with app.run_test() as pilot:
+        await pilot.press("p")
+        with pytest.raises(ScreenStackError):
+            app.bottom.dismiss()