Skip to content

Commit 2eb2698

Browse files
committed
Add a basic test
1 parent a682747 commit 2eb2698

File tree

5 files changed

+181
-0
lines changed

5 files changed

+181
-0
lines changed

shiny/playwright/controller/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@
5050
from ._chat import (
5151
Chat,
5252
)
53+
from ._markdown_stream import (
54+
MarkdownStream,
55+
)
5356
from ._navs import (
5457
NavPanel,
5558
NavsetBar,
@@ -104,6 +107,7 @@
104107
"OutputUi",
105108
"ValueBox",
106109
"Card",
110+
"MarkdownStream",
107111
"Chat",
108112
"Accordion",
109113
"AccordionPanel",
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
from __future__ import annotations
2+
3+
from playwright.sync_api import Locator, Page
4+
from playwright.sync_api import expect as playwright_expect
5+
6+
from .._types import PatternOrStr, Timeout
7+
from ._base import UiBase
8+
9+
10+
class MarkdownStream(UiBase):
11+
"""Controller for :func:`shiny.ui.MarkdownStream`."""
12+
13+
loc: Locator
14+
"""
15+
Playwright `Locator` for the markdown stream.
16+
"""
17+
18+
def __init__(self, page: Page, id: str) -> None:
19+
"""
20+
Initializes a new instance of the `MarkdownStream` class.
21+
22+
Parameters
23+
----------
24+
page
25+
Playwright `Page` of the Shiny app.
26+
id
27+
The ID of the chat.
28+
"""
29+
super().__init__(
30+
page,
31+
id=id,
32+
loc=f"#{id}",
33+
)
34+
35+
def expect_content(
36+
self,
37+
value: PatternOrStr,
38+
*,
39+
timeout: Timeout = None,
40+
) -> None:
41+
"""
42+
Expect the content of the markdown stream to match a value.
43+
44+
Parameters
45+
----------
46+
value
47+
The expected value.
48+
timeout
49+
Maximum time in milliseconds to wait for the content to match the value.
50+
"""
51+
playwright_expect(self.loc).to_have_text(value, timeout=timeout)
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# Shiny for Python
2+
3+
[![PyPI Latest Release](https://img.shields.io/pypi/v/shiny.svg)](https://pypi.org/project/shiny/)
4+
[![Build status](https://img.shields.io/github/actions/workflow/status/posit-dev/py-shiny/pytest.yaml?branch=main)](https://img.shields.io/github/actions/workflow/status/posit-dev/py-shiny/pytest.yaml?branch=main)
5+
[![Conda Latest Release](https://anaconda.org/conda-forge/shiny/badges/version.svg)](https://anaconda.org/conda-forge/shiny)
6+
[![Supported Python versions](https://img.shields.io/pypi/pyversions/shiny)](https://pypi.org/project/shiny/)
7+
[![License](https://img.shields.io/github/license/posit-dev/py-shiny)](https://github.com/posit-dev/py-shiny/blob/main/LICENSE)
8+
9+
Shiny for Python is the best way to build fast, beautiful web applications in Python. You can build quickly with Shiny and create simple interactive visualizations and prototype applications in an afternoon. But unlike other frameworks targeted at data scientists, Shiny does not limit your app's growth. Shiny remains extensible enough to power large, mission-critical applications.
10+
11+
To learn more about Shiny see the [Shiny for Python website](https://shiny.posit.co/py/). If you're new to the framework we recommend these resources:
12+
13+
- How [Shiny is different](https://posit.co/blog/why-shiny-for-python/) from Dash and Streamlit.
14+
15+
- How [reactive programming](https://shiny.posit.co/py/docs/reactive-programming.html) can help you build better applications.
16+
17+
- How to [use modules](https://shiny.posit.co/py/docs/workflow-modules.html) to efficiently develop large applications.
18+
19+
- Hosting applications for free on [shinyapps.io](https://shiny.posit.co/py/docs/deploy.html#deploy-to-shinyapps.io-cloud-hosting), [Hugging Face](https://shiny.posit.co/blog/posts/shiny-on-hugging-face/), or [Shinylive](https://shiny.posit.co/py/docs/shinylive.html).
20+
21+
## Join the conversation
22+
23+
If you have questions about Shiny for Python, or want to help us decide what to work on next, [join us on Discord](https://discord.gg/yMGCamUMnS).
24+
25+
## Getting started
26+
27+
To get started with shiny follow the [installation instructions](https://shiny.posit.co/py/docs/install-create-run.html) or just install it from pip.
28+
29+
```sh
30+
pip install shiny
31+
```
32+
33+
To install the latest development version:
34+
35+
```sh
36+
# First install htmltools, then shiny
37+
pip install git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
38+
pip install git+https://github.com/posit-dev/py-shiny.git#egg=shiny
39+
```
40+
41+
You can create and run your first application with `shiny create`, the CLI will ask you which template you would like to use. You can either run the app with the Shiny extension, or call `shiny run app.py --reload --launch-browser`.
42+
43+
## Development
44+
45+
* Shinylive built from the `main` branch: https://posit-dev.github.io/py-shiny/shinylive/py/examples/
46+
* API documentation for the `main` branch:
47+
* https://posit-dev.github.io/py-shiny/docs/api/express/
48+
* https://posit-dev.github.io/py-shiny/docs/api/core/
49+
50+
If you want to do development on Shiny for Python:
51+
52+
```sh
53+
pip install -e ".[dev,test]"
54+
```
55+
56+
Additionally, you can install pre-commit hooks which will automatically reformat and lint the code when you make a commit:
57+
58+
```sh
59+
pre-commit install
60+
61+
# To disable:
62+
# pre-commit uninstall
63+
```
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from pathlib import Path
2+
3+
from shiny.express import ui
4+
5+
readme = Path(__file__).parent / "README.md"
6+
with open(readme, "r") as f:
7+
readme_chunks = f.read().replace("\n", " \n ").split(" ")
8+
9+
10+
# Generate words from the README.md file (with a small delay)
11+
def chunk_generator():
12+
for chunk in readme_chunks:
13+
yield chunk + " "
14+
15+
16+
md = ui.MarkdownStream("shiny-readme")
17+
18+
with ui.card(
19+
height="400px",
20+
class_="mt-3",
21+
):
22+
ui.card_header("Shiny README.md")
23+
md.ui()
24+
25+
md.stream(chunk_generator())
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from playwright.sync_api import Page, expect
2+
from utils.deploy_utils import skip_on_webkit
3+
4+
from shiny.playwright import controller
5+
from shiny.run import ShinyAppProc
6+
7+
8+
async def is_element_scrolled_to_bottom(page: Page, selector: str) -> bool:
9+
return await page.evaluate(
10+
"""(selector) => {
11+
const element = document.querySelector(selector);
12+
if (!element) return false;
13+
14+
// Get the exact scroll values (rounded to handle float values)
15+
const scrollTop = Math.round(element.scrollTop);
16+
const scrollHeight = Math.round(element.scrollHeight);
17+
const clientHeight = Math.round(element.clientHeight);
18+
19+
// Check if we're at the bottom (allowing for 1px difference due to rounding)
20+
return Math.abs((scrollTop + clientHeight) - scrollHeight) <= 1;
21+
}""",
22+
selector,
23+
)
24+
25+
26+
@skip_on_webkit
27+
async def test_validate_stream_basic(page: Page, local_app: ShinyAppProc) -> None:
28+
page.goto(local_app.url)
29+
30+
stream = controller.MarkdownStream(page, "shiny-readme")
31+
32+
expect(stream.loc).to_be_visible(timeout=30 * 1000)
33+
stream.expect_content("pip install shiny")
34+
35+
# Check that the card body container (the parent of the markdown stream) is scrolled
36+
# all the way to the bottom
37+
is_scrolled = await is_element_scrolled_to_bottom(page, ".card-body")
38+
assert is_scrolled, "The card body container should be scrolled to the bottom"

0 commit comments

Comments
 (0)