Support `st.exception` in testing framework
diff --git a/lib/streamlit/testing/element_tree.py b/lib/streamlit/testing/element_tree.py
index 404a45d67fb9..3dcd0c9d077b 100644
--- a/lib/streamlit/testing/element_tree.py
+++ b/lib/streamlit/testing/element_tree.py
@@ -26,6 +26,7 @@
 from streamlit.proto.Button_pb2 import Button as ButtonProto
 from streamlit.proto.Checkbox_pb2 import Checkbox as CheckboxProto
 from streamlit.proto.Element_pb2 import Element as ElementProto
+from streamlit.proto.Exception_pb2 import Exception as ExceptionProto
 from streamlit.proto.ForwardMsg_pb2 import ForwardMsg
 from streamlit.proto.Heading_pb2 import Heading as HeadingProto
 from streamlit.proto.Markdown_pb2 import Markdown as MarkdownProto
@@ -187,6 +188,30 @@ def __init__(self, proto: MarkdownProto, root: ElementTree):
         self.type = "code"
 
 
+@dataclass
+class Exception(Element):
+    type: str
+    message: str
+    is_markdown: bool
+    stack_trace: list[str]
+    is_warning: bool
+
+    def __init__(self, proto: ExceptionProto, root: ElementTree):
+        self.key = None
+        self.root = root
+        self.proto = proto
+        self.type = "exception"
+
+        self.message = proto.message
+        self.is_markdown = proto.message_is_markdown
+        self.stack_trace = list(proto.stack_trace)
+        self.is_warning = proto.is_warning
+
+    @property
+    def value(self) -> str:
+        return self.message
+
+
 @runtime_checkable
 class Widget(Protocol):
     id: str
@@ -730,6 +755,10 @@ def get(self, element_type: Literal["header"]) -> Sequence[Header]:
     def get(self, element_type: Literal["subheader"]) -> Sequence[Subheader]:
         ...
 
+    @overload
+    def get(self, element_type: Literal["exception"]) -> Sequence[Exception]:
+        ...
+
     @overload
     def get(self, element_type: Literal["radio"]) -> Sequence[Radio[Any]]:
         ...
@@ -887,6 +916,8 @@ def parse_tree_from_messages(messages: list[ForwardMsg]) -> ElementTree:
                     new_node = Subheader(elt.heading, root=root)
                 else:
                     raise ValueError(f"Unknown heading type with tag {elt.heading.tag}")
+            elif elt.WhichOneof("type") == "exception":
+                new_node = Exception(elt.exception, root=root)
             elif elt.WhichOneof("type") == "radio":
                 new_node = Radio(elt.radio, root=root)
             elif elt.WhichOneof("type") == "checkbox":
diff --git a/lib/tests/streamlit/testing/element_tree_test.py b/lib/tests/streamlit/testing/element_tree_test.py
index eab021f392ac..1f92bcb1a78c 100644
--- a/lib/tests/streamlit/testing/element_tree_test.py
+++ b/lib/tests/streamlit/testing/element_tree_test.py
@@ -361,3 +361,31 @@ def test_value(self):
 
         with pytest.raises(IndexError):
             sr6.get("selectbox")[0].select_index(42).run()
+
+
+class ExceptionTest(InteractiveScriptTests):
+    def test_value(self):
+        script = self.script_from_string(
+            "exception.py",
+            """
+            import streamlit as st
+
+            st.exception(RuntimeError("foo"))
+            """,
+        )
+        sr = script.run()
+
+        assert sr.get("exception")[0].value == "foo"
+
+    def test_markdown(self):
+        script = self.script_from_string(
+            "exception2.py",
+            """
+            import streamlit as st
+
+            st.exception(st.errors.MarkdownFormattedException("# Oh no"))
+            """,
+        )
+        sr = script.run()
+
+        assert sr.get("exception")[0].is_markdown


## 📚 Context
- What kind of change does this PR introduce?

  - [ ] Bugfix
  - [x] Feature
  - [ ] Refactoring
  - [ ] Other, please describe:

## 🧠 Description of Changes

  - [ ] This is a breaking API change
  - [x] This is a visible (user-facing) change

## 🧪 Testing Done

- [ ] Screenshots included
- [x] Added/Updated unit tests
- [ ] Added/Updated e2e tests

