# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations

import inspect
import tempfile
import textwrap
import traceback
from pathlib import Path
from typing import TYPE_CHECKING, Any
from unittest.mock import MagicMock
from urllib import parse

from streamlit.components.v2.component_manager import BidiComponentManager
from streamlit.runtime import Runtime
from streamlit.runtime.caching.storage.dummy_cache_storage import (
    MemoryCacheStorageManager,
)
from streamlit.runtime.dataframe_source_manager import DataframeSourceManager
from streamlit.runtime.media_file_manager import MediaFileManager
from streamlit.runtime.memory_media_file_storage import MemoryMediaFileStorage
from streamlit.runtime.pages_manager import PagesManager
from streamlit.runtime.scriptrunner.script_cache import ScriptCache
from streamlit.runtime.secrets import Secrets
from streamlit.runtime.state import SCRIPT_RUN_WITHOUT_ERRORS_KEY
from streamlit.runtime.state.common import TESTING_KEY
from streamlit.runtime.state.safe_session_state import SafeSessionState
from streamlit.runtime.state.session_state import SessionState
from streamlit.source_util import page_icon_and_name
from streamlit.testing.v1.element_tree import (
    Block,
    BlockList,
    Button,
    ButtonGroup,
    Caption,
    ChatInput,
    ChatMessage,
    Checkbox,
    Code,
    ColorPicker,
    Column,
    Dataframe,
    DateInput,
    DateTimeInput,
    Divider,
    DownloadButton,
    ElementList,
    ElementTree,
    Error,
    Exception,  # noqa: A004
    Expander,
    Feedback,
    FileUploader,
    Header,
    Image,
    Info,
    Json,
    Latex,
    Markdown,
    MenuButton,
    Metric,
    Multiselect,
    Node,
    NumberInput,
    Radio,
    Selectbox,
    SelectSlider,
    Slider,
    Status,
    Subheader,
    Success,
    Tab,
    Table,
    Text,
    TextArea,
    TextInput,
    TimeInput,
    Title,
    Toast,
    Toggle,
    Warning,  # noqa: A004
    WidgetList,
    repr_,
)
from streamlit.testing.v1.local_script_runner import LocalScriptRunner
from streamlit.testing.v1.util import patch_config_options
from streamlit.util import calc_hash

if TYPE_CHECKING:
    from collections.abc import Callable, Iterator, Sequence

    from streamlit.proto.WidgetStates_pb2 import WidgetStates
    from streamlit.source_util import PageHash, PageInfo

TMP_DIR = tempfile.TemporaryDirectory()


class AppTest:
    """
    A simulated Streamlit app to check the correctness of displayed\
    elements and outputs.

    An instance of ``AppTest`` simulates a running Streamlit app. This class
    provides methods to set up, manipulate, and inspect the app contents via
    API instead of a browser UI. It can be used to write automated tests of an
    app in various scenarios. These can then be run using a tool like pytest.

    ``AppTest`` can be initialized by one of three class methods:

    * |st.testing.v1.AppTest.from_file|_ (recommended)
    * |st.testing.v1.AppTest.from_string|_
    * |st.testing.v1.AppTest.from_function|_

    Once initialized, Session State and widget values can be updated and the
    script can be run. Unlike an actual live-running Streamlit app, you need to
    call ``AppTest.run()`` explicitly to re-run the app after changing a widget
    value. Switching pages also requires an explicit, follow-up call to
    ``AppTest.run()``.

    ``AppTest`` enables developers to build tests on their app as-is, in the
    familiar Python test format, without major refactoring or abstracting out
    logic to be tested separately from the UI. Tests can run quickly with very
    low overhead. A typical pattern is to build a suite of tests for an app
    that ensure consistent functionality as the app evolves, and run the tests
    locally and/or in a CI environment like GitHub Actions.

    .. note::
        ``AppTest`` renders one page at a time. For a multipage app, initialize
        ``AppTest`` with the app's entrypoint script, which is the same file
        you would pass to ``streamlit run``. This applies to apps that use
        ``st.navigation`` or a ``pages/`` directory. To test another
        file-based page, call ``AppTest.switch_page()`` followed by
        ``AppTest.run()``. Passing a page directly to ``AppTest.from_file()``
        makes that page the main script and changes how relative page paths
        are resolved.

    .. |st.testing.v1.AppTest.from_file| replace:: ``st.testing.v1.AppTest.from_file``
    .. _st.testing.v1.AppTest.from_file: #apptestfrom_file
    .. |st.testing.v1.AppTest.from_string| replace:: ``st.testing.v1.AppTest.from_string``
    .. _st.testing.v1.AppTest.from_string: #apptestfrom_string
    .. |st.testing.v1.AppTest.from_function| replace:: ``st.testing.v1.AppTest.from_function``
    .. _st.testing.v1.AppTest.from_function: #apptestfrom_function

    Attributes
    ----------
    secrets: dict[str, Any]
        Dictionary of secrets to be used by the simulated app. Use dict-like
        syntax to set secret values for the simulated app.

    session_state: SafeSessionState
        Session State for the simulated app. SafeSessionState object supports
        read and write operations as usual for Streamlit apps.

    query_params: dict[str, Any]
        Dictionary of query parameters to be used by the simulated app. Use
        dict-like syntax to set ``query_params`` values for the simulated app.
    """

    def __init__(
        self,
        script_path: str | Path,
        *,
        default_timeout: float,
        args: tuple[Any, ...] | None = None,
        kwargs: dict[str, Any] | None = None,
    ) -> None:
        self._script_path = str(script_path)
        self.default_timeout = default_timeout
        session_state = SessionState()
        session_state[TESTING_KEY] = {}
        self.session_state = SafeSessionState(session_state, lambda: None)
        self.query_params: dict[str, Any] = {}
        self.secrets: dict[str, Any] = {}
        self.args = args
        self.kwargs = kwargs
        self._page_hash = ""
        # Pages registered by the most recent run, used to resolve switch_page()
        # against st.navigation hashes (which follow url_path, not filename).
        self._registered_pages: dict[PageHash, PageInfo] = {}
        # Cache the discovered component manager so installed CCv2 components are
        # only scanned once per AppTest instance instead of on every rerun.
        self._bidi_component_manager: BidiComponentManager | None = None

        tree = ElementTree()
        tree._runner = self
        self._tree = tree

    @classmethod
    def from_string(cls, script: str, *, default_timeout: float = 3) -> AppTest:
        """
        Create an instance of ``AppTest`` to simulate an app page defined\
        within a string.

        This is useful for testing short scripts that fit comfortably as an
        inline string in the test itself, without having to create a separate
        file for it. The script must be executable on its own and so must
        contain all necessary imports.

        Parameters
        ----------
        script: str
            The string contents of the script to be run.

        default_timeout: float
            Default time in seconds before a script run is timed out. Can be
            overridden for individual ``.run()`` calls.

        Returns
        -------
        AppTest
            A simulated Streamlit app for testing. The simulated app can be
            executed via ``.run()``.

        """
        return cls._from_string(script, default_timeout=default_timeout)

    @classmethod
    def _from_string(
        cls,
        script: str,
        *,
        default_timeout: float = 3,
        args: tuple[Any, ...] | None = None,
        kwargs: dict[str, Any] | None = None,
    ) -> AppTest:
        script_name = calc_hash(bytes(script, "utf-8"))

        path = Path(TMP_DIR.name, script_name)
        aligned_script = textwrap.dedent(script)
        path.write_text(aligned_script, encoding="utf-8")
        return AppTest(
            str(path), default_timeout=default_timeout, args=args, kwargs=kwargs
        )

    @classmethod
    def from_function(
        cls,
        script: Callable[..., Any],
        *,
        default_timeout: float = 3,
        args: tuple[Any, ...] | None = None,
        kwargs: dict[str, Any] | None = None,
    ) -> AppTest:
        """
        Create an instance of ``AppTest`` to simulate an app page defined\
        within a function.

        This is similar to ``AppTest.from_string()``, but more convenient to
        write with IDE assistance. The script must be executable on its own and
        so must contain all necessary imports.

        Parameters
        ----------
        script: Callable
            A function whose body will be used as a script. Must be runnable
            in isolation, so it must include any necessary imports.

        default_timeout: float
            Default time in seconds before a script run is timed out. Can be
            overridden for individual ``.run()`` calls.

        args: tuple
            An optional tuple of args to pass to the script function.

        kwargs: dict
            An optional dict of kwargs to pass to the script function.

        Returns
        -------
        AppTest
            A simulated Streamlit app for testing. The simulated app can be
            executed via ``.run()``.

        """
        source_lines, _ = inspect.getsourcelines(script)
        source = textwrap.dedent("".join(source_lines))
        module = (
            source
            + f"\n{script.__name__ if hasattr(script, '__name__') else 'script'}(*__args, **__kwargs)"
        )
        return cls._from_string(
            module, default_timeout=default_timeout, args=args, kwargs=kwargs
        )

    @classmethod
    def from_file(
        cls, script_path: str | Path, *, default_timeout: float = 3
    ) -> AppTest:
        """
        Create an ``AppTest`` for an app entrypoint defined in a file.

        This option is most convenient for CI workflows and testing of
        published apps. The script must be executable on its own and so must
        contain all necessary imports. For a multipage app, pass the main
        script that you would supply to ``streamlit run``. To test a
        file-based child page, initialize from the main script and use
        ``AppTest.switch_page()``.

        Parameters
        ----------
        script_path: str | Path
            Path to the app's entrypoint script. An absolute path is used as
            given. A relative path is resolved against the Python file that
            calls ``AppTest.from_file()``.

        default_timeout: float
            Default time in seconds before a script run is timed out. Can be
            overridden for individual ``.run()`` calls.

        Returns
        -------
        AppTest
            A simulated Streamlit app for testing. The simulated app can be
            executed via ``.run()``.

        Raises
        ------
        FileNotFoundError
            If ``script_path`` does not point to an existing file.

        Examples
        --------
        Initialize a multipage app from its entrypoint, then switch to a page:

        >>> at = AppTest.from_file("app.py").run()
        >>> at.switch_page("pages/settings.py").run()
        """
        script_path = Path(script_path)
        if script_path.is_absolute():
            path = script_path
        else:
            stack = traceback.StackSummary.extract(traceback.walk_stack(None))
            caller_file = Path(stack[1].filename)
            path = caller_file.parent / script_path

        if not path.is_file():
            raise FileNotFoundError(
                f"AppTest script not found at {path.resolve()}. Relative paths are "
                "resolved against the file that calls AppTest.from_file()."
            )

        return AppTest(path, default_timeout=default_timeout)

    def _run(
        self,
        widget_state: WidgetStates | None = None,
        timeout: float | None = None,
    ) -> AppTest:
        """Run the script, and parse the output messages for querying
        and interaction.

        Timeout is in seconds, or None to use the default timeout of the runner.
        """
        # Have to import the streamlit module itself so replacing st.secrets
        # is visible to other modules.
        import streamlit as st

        if timeout is None:
            timeout = self.default_timeout

        # setup
        mock_runtime = MagicMock(spec=Runtime)
        mock_runtime.media_file_mgr = MediaFileManager(
            MemoryMediaFileStorage("/mock/media")
        )
        mock_runtime.dataframe_source_mgr = DataframeSourceManager()
        mock_runtime.cache_storage_manager = MemoryCacheStorageManager()
        if self._bidi_component_manager is None:
            bidi_component_manager = BidiComponentManager()
            bidi_component_manager.discover_and_register_components(
                start_file_watching=False
            )
            self._bidi_component_manager = bidi_component_manager
        mock_runtime.bidi_component_registry = self._bidi_component_manager
        Runtime._instance = mock_runtime
        script_cache = ScriptCache()
        # Reset to ensure st.navigation works correctly regardless of prior test state.
        PagesManager.uses_pages_directory = None
        pages_manager = PagesManager(
            self._script_path, script_cache, setup_watcher=False
        )

        saved_secrets: Secrets = st.secrets
        # Only modify global secrets stuff if we have been given secrets
        if self.secrets:
            new_secrets = Secrets()
            new_secrets._secrets = self.secrets
            st.secrets = new_secrets

        script_runner = LocalScriptRunner(
            self._script_path,
            self.session_state,
            pages_manager,
            args=self.args,
            kwargs=self.kwargs,
        )

        # Register any files from FileUploader widgets with the file manager
        self._register_uploaded_files(script_runner)

        with patch_config_options({"global.appTest": True}):
            self._tree = script_runner.run(
                widget_state, self.query_params, timeout, self._page_hash
            )
            self._tree._runner = self
            # A failed run that never reaches st.navigation leaves a
            # main-page-only fallback. Keep the last navigation registry in
            # that case so switch_page() does not silently hash the filename.
            # A successful run that no longer calls st.navigation must drop
            # the stale map.
            new_pages = pages_manager.get_pages()
            if (
                any("url_pathname" in info for info in new_pages.values())
                or self.session_state[SCRIPT_RUN_WITHOUT_ERRORS_KEY]
            ):
                self._registered_pages = new_pages
        # Last event is SHUTDOWN, so the corresponding data includes query string
        query_string = script_runner.event_data[-1]["client_state"].query_string
        self.query_params = parse.parse_qs(query_string)

        if self.secrets:
            if st.secrets._secrets is not None:
                self.secrets = dict(st.secrets._secrets)
            st.secrets = saved_secrets
        Runtime._instance = None

        return self

    def _register_uploaded_files(self, script_runner: LocalScriptRunner) -> None:
        """Register files from FileUploader widgets with the file manager."""
        from streamlit.runtime.uploaded_file_manager import UploadedFileRec

        for widget in self._tree.file_uploader:
            for (
                file_id,
                filename,
                content,
                mime_type,
            ) in widget._get_files_to_register():
                file_rec = UploadedFileRec(
                    file_id=file_id,
                    name=filename,
                    type=mime_type,
                    data=content,
                )
                script_runner.register_file(file_rec)

    def run(self, *, timeout: float | None = None) -> AppTest:
        """Run the script from the current state.

        This is equivalent to manually rerunning the app or the rerun that
        occurs upon user interaction. ``AppTest.run()`` must be manually called
        after updating a widget value or switching pages as script reruns do
        not occur automatically as they do for live-running Streamlit apps.

        Parameters
        ----------
        timeout : float or None
            The maximum number of seconds to run the script. If ``timeout`` is
            ``None`` (default), Streamlit uses the default timeout set for the
            instance of ``AppTest``.

        Returns
        -------
        AppTest
            self

        """
        return self._tree.run(timeout=timeout)

    def switch_page(self, page_path: str) -> AppTest:
        """Switch to a file-based page relative to the app's main script.

        This method does not automatically rerun the app. Use a follow-up call
        to ``AppTest.run()`` to obtain the elements on the selected page. The
        main script supplied to ``AppTest.from_file()`` remains the path root
        after switching pages.

        Call ``run()`` at least once before switching pages. Before the first
        run, Streamlit identifies the page by its filename, which does not
        match a page that ``st.navigation`` registers with a custom
        ``url_path``. If page registration depends on Session State, call
        ``run()`` after updating the state and before switching pages.

        Parameters
        ----------
        page_path: str
            Path of the page to switch to. The path must be relative to the
            main script's location (e.g. ``"pages/my_page.py"``).

        Returns
        -------
        AppTest
            self

        Raises
        ------
        ValueError
            If ``page_path`` does not point to a file relative to the main
            script, or if ``st.navigation`` is active and the file is not a
            registered page.

        Examples
        --------
        >>> at = AppTest.from_file("app.py").run()
        >>> at.switch_page("pages/settings.py").run()

        """
        main_dir = Path(self._script_path).parent
        full_page_path = main_dir / page_path
        if not full_page_path.is_file():
            raise ValueError(
                f"Could not find page {page_path!r} relative to the main script "
                f"{self._script_path!r}. Resolved page path: "
                f"{str(full_page_path.resolve())!r}."
            )
        page_path_str = str(full_page_path.resolve())
        self._page_hash = self._resolve_page_hash(page_path_str)
        return self

    def _resolve_page_hash(self, page_path_str: str) -> str:
        """Map a page file to the script hash a real session would use.

        ``st.Page`` hashes ``url_path``, which may differ from the filename.
        After the first run, match the resolved script path against pages
        registered by ``st.navigation`` or a ``pages/`` directory.
        """
        _, page_name = page_icon_and_name(Path(page_path_str))
        filename_hash = calc_hash(page_name)
        target = Path(page_path_str).resolve()

        path_matches = [
            page_hash
            for page_hash, info in self._registered_pages.items()
            if (script_path := info.get("script_path"))
            and Path(str(script_path)).resolve() == target
        ]
        if path_matches:
            # A file can be registered under multiple URL paths. Prefer the
            # page whose URL matches the filename, then registration order.
            return next(
                (
                    page_hash
                    for page_hash in path_matches
                    if self._registered_pages[page_hash].get("url_pathname")
                    == page_name
                ),
                path_matches[0],
            )

        # st.navigation registers url_pathname even for callable-only pages.
        # Do not fall back to a filename-slug hash: that can collide with a
        # callable page or a custom url_path and silently open the wrong page
        # (https://github.com/streamlit/streamlit/issues/16611).
        has_navigation_registry = any(
            "url_pathname" in info for info in self._registered_pages.values()
        )
        if has_navigation_registry:
            known_pages = [
                str(info["script_path"])
                if info.get("script_path")
                else str(info.get("url_pathname") or info.get("page_name") or page_hash)
                for page_hash, info in self._registered_pages.items()
            ]
            raise ValueError(
                f"Could not find a navigation page for {page_path_str!r}. "
                "AppTest.switch_page() matches registered script paths. "
                "If page registration depends on Session State, call "
                "AppTest.run() after updating the state and before switching. "
                f"Known pages: {known_pages}."
            )
        return filename_hash

    @property
    def main(self) -> Block:
        """Sequence of elements within the main body of the app.

        Returns
        -------
        Block
            A container of elements. Block can be queried for elements in the
            same manner as ``AppTest``. For example, ``Block.checkbox`` will
            return all ``st.checkbox`` within the associated container.
        """
        return self._tree.main

    @property
    def sidebar(self) -> Block:
        """Sequence of all elements within ``st.sidebar``.

        Returns
        -------
        Block
            A container of elements. Block can be queried for elements in the
            same manner as ``AppTest``. For example, ``Block.checkbox`` will
            return all ``st.checkbox`` within the associated container.
        """
        return self._tree.sidebar

    @property
    def button(self) -> WidgetList[Button]:
        """Sequence of all ``st.button`` and ``st.form_submit_button`` widgets.

        Returns
        -------
        WidgetList of Button
            Sequence of all ``st.button`` and ``st.form_submit_button``
            widgets. Individual widgets can be accessed from a WidgetList by
            index (order on the page) or key. For example, ``at.button[0]`` for
            the first widget or ``at.button(key="my_key")`` for a widget with a
            given key.
        """
        return self._tree.button

    @property
    def button_group(self) -> WidgetList[ButtonGroup[Any]]:
        """Sequence of all ``st.pills`` and ``st.segmented_control`` widgets.

        Returns
        -------
        WidgetList of ButtonGroup
            Sequence of all ``st.pills`` and ``st.segmented_control`` widgets.
            Individual widgets can be accessed from a WidgetList by index
            (order on the page) or key. For example, ``at.button_group[0]`` for
            the first widget or ``at.button_group(key="my_key")`` for a widget
            with a given key.
        """
        return self._tree.button_group

    @property
    def pills(self) -> WidgetList[ButtonGroup[Any]]:
        """Sequence of all ``st.pills`` widgets.

        Returns
        -------
        WidgetList of ButtonGroup
            Sequence of all ``st.pills`` widgets (filtered by style).
            Individual widgets can be accessed from a WidgetList by index
            (order on the page) or key. For example, ``at.pills[0]`` for
            the first widget or ``at.pills(key="my_key")`` for a widget
            with a given key.
        """
        return self._tree.pills

    @property
    def segmented_control(self) -> WidgetList[ButtonGroup[Any]]:
        """Sequence of all ``st.segmented_control`` widgets.

        Returns
        -------
        WidgetList of ButtonGroup
            Sequence of all ``st.segmented_control`` widgets (filtered by style).
            Individual widgets can be accessed from a WidgetList by index
            (order on the page) or key. For example, ``at.segmented_control[0]``
            for the first widget or ``at.segmented_control(key="my_key")``
            for a widget with a given key.
        """
        return self._tree.segmented_control

    @property
    def caption(self) -> ElementList[Caption]:
        """Sequence of all ``st.caption`` elements.

        Returns
        -------
        ElementList of Caption
            Sequence of all ``st.caption`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.caption[0]`` for the first element. Caption is an
            extension of the Element class.
        """
        return self._tree.caption

    @property
    def chat_input(self) -> WidgetList[ChatInput]:
        """Sequence of all ``st.chat_input`` widgets.

        Returns
        -------
        WidgetList of ChatInput
            Sequence of all ``st.chat_input`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.chat_input[0]`` for the first widget or
            ``at.chat_input(key="my_key")`` for a widget with a given key.
        """
        return self._tree.chat_input

    @property
    def chat_message(self) -> Sequence[ChatMessage]:
        """Sequence of all ``st.chat_message`` elements.

        Returns
        -------
        Sequence of ChatMessage
            Sequence of all ``st.chat_message`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.chat_message[0]`` for the first element.  ChatMessage
            is an extension of the Block class.
        """
        return self._tree.chat_message

    @property
    def checkbox(self) -> WidgetList[Checkbox]:
        """Sequence of all ``st.checkbox`` widgets.

        Returns
        -------
        WidgetList of Checkbox
            Sequence of all ``st.checkbox`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.checkbox[0]`` for the first widget or
            ``at.checkbox(key="my_key")`` for a widget with a given key.
        """
        return self._tree.checkbox

    @property
    def code(self) -> ElementList[Code]:
        """Sequence of all ``st.code`` elements.

        Returns
        -------
        ElementList of Code
            Sequence of all ``st.code`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.code[0]`` for the first element. Code is an
            extension of the Element class.
        """
        return self._tree.code

    @property
    def color_picker(self) -> WidgetList[ColorPicker]:
        """Sequence of all ``st.color_picker`` widgets.

        Returns
        -------
        WidgetList of ColorPicker
            Sequence of all ``st.color_picker`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.color_picker[0]`` for the first widget or
            ``at.color_picker(key="my_key")`` for a widget with a given key.
        """
        return self._tree.color_picker

    @property
    def columns(self) -> Sequence[Column]:
        """Sequence of all columns within ``st.columns`` elements.

        Each column within a single ``st.columns`` will be returned as a
        separate Column in the Sequence.

        Returns
        -------
        Sequence of Column
            Sequence of all columns within ``st.columns`` elements. Individual
            columns can be accessed from an ElementList by index (order on the
            page). For example, ``at.columns[0]`` for the first column. Column
            is an extension of the Block class.
        """
        return self._tree.columns

    @property
    def container(self) -> BlockList:
        """Sequence of all ``st.container`` blocks, including horizontal containers.

        The implicit row that ``st.columns`` creates is not included.

        Returns
        -------
        BlockList
            Individual blocks can be accessed by index or key. For example,
            ``at.container[0]`` or ``at.container(key="filters")``.
        """
        return self._tree.container

    @property
    def dataframe(self) -> ElementList[Dataframe]:
        """Sequence of all ``st.dataframe`` elements.

        Returns
        -------
        ElementList of Dataframe
            Sequence of all ``st.dataframe`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.dataframe[0]`` for the first element. Dataframe is an
            extension of the Element class.
        """
        return self._tree.dataframe

    @property
    def date_input(self) -> WidgetList[DateInput]:
        """Sequence of all ``st.date_input`` widgets.

        Returns
        -------
        WidgetList of DateInput
            Sequence of all ``st.date_input`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.date_input[0]`` for the first widget or
            ``at.date_input(key="my_key")`` for a widget with a given key.
        """
        return self._tree.date_input

    @property
    def datetime_input(self) -> WidgetList[DateTimeInput]:
        """Sequence of all ``st.datetime_input`` widgets.

        Returns
        -------
        WidgetList of DateTimeInput
            Sequence of all ``st.datetime_input`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.datetime_input[0]`` for the first widget or
            ``at.datetime_input(key="my_key")`` for a widget with a given key.
        """
        return self._tree.datetime_input

    @property
    def divider(self) -> ElementList[Divider]:
        """Sequence of all ``st.divider`` elements.

        Returns
        -------
        ElementList of Divider
            Sequence of all ``st.divider`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.divider[0]`` for the first element. Divider is an
            extension of the Element class.
        """
        return self._tree.divider

    @property
    def download_button(self) -> WidgetList[DownloadButton]:
        """Sequence of all ``st.download_button`` widgets.

        Returns
        -------
        WidgetList of DownloadButton
            Sequence of all ``st.download_button`` widgets. Individual widgets
            can be accessed from a WidgetList by index (order on the page) or
            key. For example, ``at.download_button[0]`` for the first widget or
            ``at.download_button(key="my_key")`` for a widget with a given key.
        """
        return self._tree.download_button

    @property
    def error(self) -> ElementList[Error]:
        """Sequence of all ``st.error`` elements.

        Returns
        -------
        ElementList of Error
            Sequence of all ``st.error`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.error[0]`` for the first element. Error is an
            extension of the Element class.
        """
        return self._tree.error

    @property
    def exception(self) -> ElementList[Exception]:
        """Sequence of all ``st.exception`` elements.

        Returns
        -------
        ElementList of Exception
            Sequence of all ``st.exception`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.exception[0]`` for the first element. Exception is an
            extension of the Element class.
        """
        return self._tree.exception

    @property
    def feedback(self) -> WidgetList[Feedback]:
        """Sequence of all ``st.feedback`` widgets.

        Returns
        -------
        WidgetList of Feedback
            Sequence of all ``st.feedback`` widgets. Individual widgets can be
            accessed from a WidgetList by index (order on the page) or key. For
            example, ``at.feedback[0]`` for the first widget or
            ``at.feedback(key="my_key")`` to access by key.
        """
        return self._tree.feedback

    @property
    def file_uploader(self) -> WidgetList[FileUploader]:
        """Sequence of all ``st.file_uploader`` widgets.

        Returns
        -------
        WidgetList of FileUploader
            Sequence of all ``st.file_uploader`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.file_uploader[0]`` for the first widget or
            ``at.file_uploader(key="my_key")`` for a widget with a given key.
        """
        return self._tree.file_uploader

    @property
    def expander(self) -> Sequence[Expander]:
        """Sequence of all ``st.expander`` elements.

        Returns
        -------
        Sequence of Expandable
            Sequence of all ``st.expander`` elements. Individual elements can be
            accessed from a Sequence by index (order on the page). For
            example, ``at.expander[0]`` for the first element. Expandable is an
            extension of the Block class.
        """
        return self._tree.expander

    @property
    def header(self) -> ElementList[Header]:
        """Sequence of all ``st.header`` elements.

        Returns
        -------
        ElementList of Header
            Sequence of all ``st.header`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.header[0]`` for the first element. Header is an
            extension of the Element class.
        """
        return self._tree.header

    @property
    def image(self) -> ElementList[Image]:
        """Sequence of all ``st.image`` elements.

        Returns
        -------
        ElementList of Image
            Sequence of all ``st.image`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.image[0]`` for the first element. Image is an
            extension of the Element class.
        """
        return self._tree.image

    @property
    def info(self) -> ElementList[Info]:
        """Sequence of all ``st.info`` elements.

        Returns
        -------
        ElementList of Info
            Sequence of all ``st.info`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.info[0]`` for the first element. Info is an
            extension of the Element class.
        """
        return self._tree.info

    @property
    def json(self) -> ElementList[Json]:
        """Sequence of all ``st.json`` elements.

        Returns
        -------
        ElementList of Json
            Sequence of all ``st.json`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.json[0]`` for the first element. Json is an
            extension of the Element class.
        """
        return self._tree.json

    @property
    def latex(self) -> ElementList[Latex]:
        """Sequence of all ``st.latex`` elements.

        Returns
        -------
        ElementList of Latex
            Sequence of all ``st.latex`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.latex[0]`` for the first element. Latex is an
            extension of the Element class.
        """
        return self._tree.latex

    @property
    def markdown(self) -> ElementList[Markdown]:
        """Sequence of all ``st.markdown`` elements.

        Returns
        -------
        ElementList of Markdown
            Sequence of all ``st.markdown`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.markdown[0]`` for the first element. Markdown is an
            extension of the Element class.
        """
        return self._tree.markdown

    @property
    def metric(self) -> ElementList[Metric]:
        """Sequence of all ``st.metric`` elements.

        Returns
        -------
        ElementList of Metric
            Sequence of all ``st.metric`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.metric[0]`` for the first element. Metric is an
            extension of the Element class.
        """
        return self._tree.metric

    @property
    def menu_button(self) -> WidgetList[MenuButton[Any]]:
        """Sequence of all ``st.menu_button`` widgets.

        Returns
        -------
        WidgetList of MenuButton
            Sequence of all ``st.menu_button`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.menu_button[0]`` for the first widget or
            ``at.menu_button(key="my_key")`` for a widget with a given key.
        """
        return self._tree.menu_button

    @property
    def multiselect(self) -> WidgetList[Multiselect[Any]]:
        """Sequence of all ``st.multiselect`` widgets.

        Returns
        -------
        WidgetList of Multiselect
            Sequence of all ``st.multiselect`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.multiselect[0]`` for the first widget or
            ``at.multiselect(key="my_key")`` for a widget with a given key.
        """
        return self._tree.multiselect

    @property
    def number_input(self) -> WidgetList[NumberInput]:
        """Sequence of all ``st.number_input`` widgets.

        Returns
        -------
        WidgetList of NumberInput
            Sequence of all ``st.number_input`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.number_input[0]`` for the first widget or
            ``at.number_input(key="my_key")`` for a widget with a given key.
        """
        return self._tree.number_input

    @property
    def radio(self) -> WidgetList[Radio[Any]]:
        """Sequence of all ``st.radio`` widgets.

        Returns
        -------
        WidgetList of Radio
            Sequence of all ``st.radio`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.radio[0]`` for the first widget or
            ``at.radio(key="my_key")`` for a widget with a given key.
        """
        return self._tree.radio

    @property
    def select_slider(self) -> WidgetList[SelectSlider[Any]]:
        """Sequence of all ``st.select_slider`` widgets.

        Returns
        -------
        WidgetList of SelectSlider
            Sequence of all ``st.select_slider`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.select_slider[0]`` for the first widget or
            ``at.select_slider(key="my_key")`` for a widget with a given key.
        """
        return self._tree.select_slider

    @property
    def selectbox(self) -> WidgetList[Selectbox[Any]]:
        """Sequence of all ``st.selectbox`` widgets.

        Returns
        -------
        WidgetList of Selectbox
            Sequence of all ``st.selectbox`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.selectbox[0]`` for the first widget or
            ``at.selectbox(key="my_key")`` for a widget with a given key.
        """
        return self._tree.selectbox

    @property
    def slider(self) -> WidgetList[Slider[Any]]:
        """Sequence of all ``st.slider`` widgets.

        Returns
        -------
        WidgetList of Slider
            Sequence of all ``st.slider`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.slider[0]`` for the first widget or
            ``at.slider(key="my_key")`` for a widget with a given key.
        """
        return self._tree.slider

    @property
    def subheader(self) -> ElementList[Subheader]:
        """Sequence of all ``st.subheader`` elements.

        Returns
        -------
        ElementList of Subheader
            Sequence of all ``st.subheader`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.subheader[0]`` for the first element. Subheader is an
            extension of the Element class.
        """
        return self._tree.subheader

    @property
    def success(self) -> ElementList[Success]:
        """Sequence of all ``st.success`` elements.

        Returns
        -------
        ElementList of Success
            Sequence of all ``st.success`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.success[0]`` for the first element. Success is an
            extension of the Element class.
        """
        return self._tree.success

    @property
    def status(self) -> Sequence[Status]:
        """Sequence of all ``st.status`` elements.

        Returns
        -------
        Sequence of Status
            Sequence of all ``st.status`` elements. Individual elements can be
            accessed from a Sequence by index (order on the page). For
            example, ``at.status[0]`` for the first element. Status is an
            extension of the Block class.
        """
        return self._tree.status

    @property
    def table(self) -> ElementList[Table]:
        """Sequence of all ``st.table`` elements.

        Returns
        -------
        ElementList of Table
            Sequence of all ``st.table`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.table[0]`` for the first element. Table is an
            extension of the Element class.
        """
        return self._tree.table

    @property
    def tabs(self) -> Sequence[Tab]:
        """Sequence of all tabs within ``st.tabs`` elements.

        Each tab within a single ``st.tabs`` will be returned as a separate Tab
        in the Sequence. Additionally, the tab labels are forwarded to each
        Tab element as a property. For example, ``st.tabs("A","B")`` will
        yield two Tab objects, with ``Tab.label`` returning "A" and "B",
        respectively.

        Returns
        -------
        Sequence of Tab
            Sequence of all tabs within ``st.tabs`` elements. Individual
            tabs can be accessed from an ElementList by index (order on the
            page). For example, ``at.tabs[0]`` for the first tab. Tab is an
            extension of the Block class.
        """
        return self._tree.tabs

    @property
    def text(self) -> ElementList[Text]:
        """Sequence of all ``st.text`` elements.

        Returns
        -------
        ElementList of Text
            Sequence of all ``st.text`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.text[0]`` for the first element. Text is an
            extension of the Element class.
        """
        return self._tree.text

    @property
    def text_area(self) -> WidgetList[TextArea]:
        """Sequence of all ``st.text_area`` widgets.

        Returns
        -------
        WidgetList of TextArea
            Sequence of all ``st.text_area`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.text_area[0]`` for the first widget or
            ``at.text_area(key="my_key")`` for a widget with a given key.
        """
        return self._tree.text_area

    @property
    def text_input(self) -> WidgetList[TextInput]:
        """Sequence of all ``st.text_input`` widgets.

        Returns
        -------
        WidgetList of TextInput
            Sequence of all ``st.text_input`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.text_input[0]`` for the first widget or
            ``at.text_input(key="my_key")`` for a widget with a given key.
        """
        return self._tree.text_input

    @property
    def time_input(self) -> WidgetList[TimeInput]:
        """Sequence of all ``st.time_input`` widgets.

        Returns
        -------
        WidgetList of TimeInput
            Sequence of all ``st.time_input`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.time_input[0]`` for the first widget or
            ``at.time_input(key="my_key")`` for a widget with a given key.
        """
        return self._tree.time_input

    @property
    def title(self) -> ElementList[Title]:
        """Sequence of all ``st.title`` elements.

        Returns
        -------
        ElementList of Title
            Sequence of all ``st.title`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.title[0]`` for the first element. Title is an
            extension of the Element class.
        """
        return self._tree.title

    @property
    def toast(self) -> ElementList[Toast]:
        """Sequence of all ``st.toast`` elements.

        Returns
        -------
        ElementList of Toast
            Sequence of all ``st.toast`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.toast[0]`` for the first element. Toast is an
            extension of the Element class.
        """
        return self._tree.toast

    @property
    def toggle(self) -> WidgetList[Toggle]:
        """Sequence of all ``st.toggle`` widgets.

        Returns
        -------
        WidgetList of Toggle
            Sequence of all ``st.toggle`` widgets. Individual widgets can
            be accessed from a WidgetList by index (order on the page) or key.
            For example, ``at.toggle[0]`` for the first widget or
            ``at.toggle(key="my_key")`` for a widget with a given key.
        """
        return self._tree.toggle

    @property
    def warning(self) -> ElementList[Warning]:
        """Sequence of all ``st.warning`` elements.

        Returns
        -------
        ElementList of Warning
            Sequence of all ``st.warning`` elements. Individual elements can be
            accessed from an ElementList by index (order on the page). For
            example, ``at.warning[0]`` for the first element. Warning is an
            extension of the Element class.
        """
        return self._tree.warning

    def __len__(self) -> int:
        return len(self._tree)

    def __iter__(self) -> Iterator[Node]:
        yield from self._tree

    def __getitem__(self, idx: int) -> Node:
        return self._tree[idx]

    def get(self, element_type: str) -> Sequence[Node]:
        """Get elements or widgets of the specified type.

        This method returns the collection of all elements or widgets of
        the specified type on the current page. Retrieve a specific element by
        using its index (order on page) or key lookup.

        Parameters
        ----------
        element_type: str
            An element attribute of ``AppTest``. For example, "button",
            "caption", or "chat_input".

        Returns
        -------
        Sequence of Elements
            Sequence of elements of the given type. Individual elements can
            be accessed from a Sequence by index (order on the page). When
            getting and ``element_type`` that is a widget, individual widgets
            can be accessed by key. For example, ``at.get("text")[0]`` for the
            first ``st.text`` element or ``at.get("slider")(key="my_key")`` for
            the ``st.slider`` widget with a given key.
        """
        return self._tree.get(element_type)

    def get_by_key(self, key: str) -> Node:
        """Return the element, widget, or container with the given key.

        Use this method when the key is unique across the app and the element
        type does not matter. To disambiguate a key reused across types, use a
        typed collection such as ``at.text_input(key="x")``.

        Parameters
        ----------
        key : str
            The user-provided ``key`` of the element or container.

        Returns
        -------
        Element or Block
            The matching node.

        Raises
        ------
        KeyError
            If no current node has this key.
        AppTestError
            If more than one current node has this key.
        """
        return self._tree.get_by_key(key)

    def __repr__(self) -> str:
        return repr_(self)
