Skip to content

Commit

Permalink
Page switching in AppTest (#8280)
Browse files Browse the repository at this point in the history
## Describe your changes
Adds page switching to AppTest

## GitHub Issue Link (if applicable)
Enables a fix for #8154

## Testing Plan

App tests added
  • Loading branch information
AnOctopus committed Mar 13, 2024
1 parent 0e2814f commit c61e33b
Show file tree
Hide file tree
Showing 5 changed files with 99 additions and 7 deletions.
39 changes: 33 additions & 6 deletions lib/streamlit/testing/v1/app_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@

import hashlib
import inspect
import pathlib
import tempfile
import textwrap
import traceback
from pathlib import Path
from typing import Any, Callable, Sequence
from unittest.mock import MagicMock
from urllib import parse
Expand Down Expand Up @@ -84,7 +84,7 @@
)
from streamlit.testing.v1.local_script_runner import LocalScriptRunner
from streamlit.testing.v1.util import patch_config_options
from streamlit.util import HASHLIB_KWARGS
from streamlit.util import HASHLIB_KWARGS, calc_md5

TMP_DIR = tempfile.TemporaryDirectory()

Expand Down Expand Up @@ -162,6 +162,7 @@ def __init__(
self.secrets: dict[str, Any] = {}
self.args = args
self.kwargs = kwargs
self._page_hash = ""

tree = ElementTree()
tree._runner = self
Expand Down Expand Up @@ -203,7 +204,7 @@ def _from_string(
hasher = hashlib.md5(bytes(script, "utf-8"), **HASHLIB_KWARGS)
script_name = hasher.hexdigest()

path = pathlib.Path(TMP_DIR.name, script_name)
path = Path(TMP_DIR.name, script_name)
aligned_script = textwrap.dedent(script)
path.write_text(aligned_script)
return AppTest(
Expand Down Expand Up @@ -284,14 +285,14 @@ def from_file(cls, script_path: str, *, default_timeout: float = 3) -> AppTest:
executed via ``.run()``.
"""
if pathlib.Path.is_file(pathlib.Path(script_path)):
if Path.is_file(Path(script_path)):
path = script_path
else:
# TODO: Make this not super fragile
# Attempt to find the test file calling this method, so the
# path can be relative to there.
stack = traceback.StackSummary.extract(traceback.walk_stack(None))
filepath = pathlib.Path(stack[1].filename)
filepath = Path(stack[1].filename)
path = str(filepath.parent / script_path)
return AppTest(path, default_timeout=default_timeout)

Expand Down Expand Up @@ -334,7 +335,9 @@ def _run(
self._script_path, self.session_state, args=self.args, kwargs=self.kwargs
)
with patch_config_options({"global.appTest": True}):
self._tree = script_runner.run(widget_state, self.query_params, timeout)
self._tree = script_runner.run(
widget_state, self.query_params, timeout, self._page_hash
)
self._tree._runner = self
# Last event is SHUTDOWN, so the corresponding data includes query string
query_string = script_runner.event_data[-1]["client_state"].query_string
Expand Down Expand Up @@ -373,6 +376,30 @@ def run(self, *, timeout: float | None = None) -> AppTest:
"""
return self._tree.run(timeout=timeout)

def switch_page(self, page_path: str) -> AppTest:
"""Switch to another page of the app.
Parameters
----------
page_path: str
Path of the page to switch to. The path must be relative to the
location of the main script (e.g. ``"pages/my_page.py"``).
Returns
-------
AppTest
self
"""
main_dir = Path(self._script_path).parent
full_page_path = main_dir / page_path
if not full_page_path.is_file():
raise ValueError(
f"Unable to find script at {page_path}, make sure the page given is relative to the main script."
)
page_path_str = str(full_page_path.resolve())
self._page_hash = calc_md5(page_path_str)
return self

@property
def main(self) -> Block:
"""Sequence of elements within the main body of the app.
Expand Down
7 changes: 6 additions & 1 deletion lib/streamlit/testing/v1/local_script_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def run(
widget_state: WidgetStates | None = None,
query_params=None,
timeout: float = 3,
page_hash: str = "",
) -> ElementTree:
"""Run the script, and parse the output messages for querying
and interaction.
Expand All @@ -109,7 +110,11 @@ def run(
if query_params:
query_string = parse.urlencode(query_params, doseq=True)

rerun_data = RerunData(widget_states=widget_state, query_string=query_string)
rerun_data = RerunData(
widget_states=widget_state,
query_string=query_string,
page_script_hash=page_hash,
)
self.request_rerun(rerun_data)
if not self._script_thread:
self.start()
Expand Down
24 changes: 24 additions & 0 deletions lib/tests/streamlit/testing/app_test_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,3 +234,27 @@ def code():
at = AppTest.from_function(code).run()
# The script run should finish instead of recurring and timing out
at.button[0].click().run()


def test_switch_page():
at = AppTest.from_file("test_data/main.py").run()
assert at.text[0].value == "main page"

at.switch_page("pages/page1.py").run()
assert at.text[0].value == "page 1"

with pytest.raises(ValueError) as e:
# Pages must be relative to main script path
at.switch_page("test_data/pages/page1.py")
assert "relative to the main script path" in str(e.value)


def test_switch_page_widgets():
at = AppTest.from_file("test_data/main.py").run()
at.slider[0].set_value(5).run()
assert at.slider[0].value == 5

at.switch_page("pages/page1.py").run()
assert not at.slider
at.switch_page("main.py").run()
assert at.slider[0].value == 0
19 changes: 19 additions & 0 deletions lib/tests/streamlit/testing/test_data/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# 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.

import streamlit as st

st.text("main page")

st.slider("slider", min_value=0, max_value=10)
17 changes: 17 additions & 0 deletions lib/tests/streamlit/testing/test_data/pages/page1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2024)
#
# 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.

import streamlit as st

st.text("page 1")

0 comments on commit c61e33b

Please sign in to comment.