Skip to content

Commit

Permalink
Add a basic test
Browse files Browse the repository at this point in the history
  • Loading branch information
cpsievert committed Jan 29, 2025
1 parent a682747 commit 2eb2698
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 0 deletions.
4 changes: 4 additions & 0 deletions shiny/playwright/controller/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@
from ._chat import (
Chat,
)
from ._markdown_stream import (
MarkdownStream,
)
from ._navs import (
NavPanel,
NavsetBar,
Expand Down Expand Up @@ -104,6 +107,7 @@
"OutputUi",
"ValueBox",
"Card",
"MarkdownStream",
"Chat",
"Accordion",
"AccordionPanel",
Expand Down
51 changes: 51 additions & 0 deletions shiny/playwright/controller/_markdown_stream.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from __future__ import annotations

from playwright.sync_api import Locator, Page
from playwright.sync_api import expect as playwright_expect

from .._types import PatternOrStr, Timeout
from ._base import UiBase


class MarkdownStream(UiBase):
"""Controller for :func:`shiny.ui.MarkdownStream`."""

loc: Locator
"""
Playwright `Locator` for the markdown stream.
"""

def __init__(self, page: Page, id: str) -> None:
"""
Initializes a new instance of the `MarkdownStream` class.
Parameters
----------
page
Playwright `Page` of the Shiny app.
id
The ID of the chat.
"""
super().__init__(
page,
id=id,
loc=f"#{id}",
)

def expect_content(
self,
value: PatternOrStr,
*,
timeout: Timeout = None,
) -> None:
"""
Expect the content of the markdown stream to match a value.
Parameters
----------
value
The expected value.
timeout
Maximum time in milliseconds to wait for the content to match the value.
"""
playwright_expect(self.loc).to_have_text(value, timeout=timeout)
63 changes: 63 additions & 0 deletions tests/playwright/shiny/components/MarkdownStream/basic/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Shiny for Python

[![PyPI Latest Release](https://img.shields.io/pypi/v/shiny.svg)](https://pypi.org/project/shiny/)
[![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)
[![Conda Latest Release](https://anaconda.org/conda-forge/shiny/badges/version.svg)](https://anaconda.org/conda-forge/shiny)
[![Supported Python versions](https://img.shields.io/pypi/pyversions/shiny)](https://pypi.org/project/shiny/)
[![License](https://img.shields.io/github/license/posit-dev/py-shiny)](https://github.com/posit-dev/py-shiny/blob/main/LICENSE)

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.

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:

- How [Shiny is different](https://posit.co/blog/why-shiny-for-python/) from Dash and Streamlit.

- How [reactive programming](https://shiny.posit.co/py/docs/reactive-programming.html) can help you build better applications.

- How to [use modules](https://shiny.posit.co/py/docs/workflow-modules.html) to efficiently develop large applications.

- 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).

## Join the conversation

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).

## Getting started

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.

```sh
pip install shiny
```

To install the latest development version:

```sh
# First install htmltools, then shiny
pip install git+https://github.com/posit-dev/py-htmltools.git#egg=htmltools
pip install git+https://github.com/posit-dev/py-shiny.git#egg=shiny
```

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`.

## Development

* Shinylive built from the `main` branch: https://posit-dev.github.io/py-shiny/shinylive/py/examples/
* API documentation for the `main` branch:
* https://posit-dev.github.io/py-shiny/docs/api/express/
* https://posit-dev.github.io/py-shiny/docs/api/core/

If you want to do development on Shiny for Python:

```sh
pip install -e ".[dev,test]"
```

Additionally, you can install pre-commit hooks which will automatically reformat and lint the code when you make a commit:

```sh
pre-commit install

# To disable:
# pre-commit uninstall
```
25 changes: 25 additions & 0 deletions tests/playwright/shiny/components/MarkdownStream/basic/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from pathlib import Path

from shiny.express import ui

readme = Path(__file__).parent / "README.md"
with open(readme, "r") as f:
readme_chunks = f.read().replace("\n", " \n ").split(" ")


# Generate words from the README.md file (with a small delay)
def chunk_generator():
for chunk in readme_chunks:
yield chunk + " "


md = ui.MarkdownStream("shiny-readme")

with ui.card(
height="400px",
class_="mt-3",
):
ui.card_header("Shiny README.md")
md.ui()

md.stream(chunk_generator())
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
from playwright.sync_api import Page, expect
from utils.deploy_utils import skip_on_webkit

from shiny.playwright import controller
from shiny.run import ShinyAppProc


async def is_element_scrolled_to_bottom(page: Page, selector: str) -> bool:
return await page.evaluate(
"""(selector) => {
const element = document.querySelector(selector);
if (!element) return false;
// Get the exact scroll values (rounded to handle float values)
const scrollTop = Math.round(element.scrollTop);
const scrollHeight = Math.round(element.scrollHeight);
const clientHeight = Math.round(element.clientHeight);
// Check if we're at the bottom (allowing for 1px difference due to rounding)
return Math.abs((scrollTop + clientHeight) - scrollHeight) <= 1;
}""",
selector,
)


@skip_on_webkit
async def test_validate_stream_basic(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

stream = controller.MarkdownStream(page, "shiny-readme")

expect(stream.loc).to_be_visible(timeout=30 * 1000)
stream.expect_content("pip install shiny")

# Check that the card body container (the parent of the markdown stream) is scrolled
# all the way to the bottom
is_scrolled = await is_element_scrolled_to_bottom(page, ".card-body")
assert is_scrolled, "The card body container should be scrolled to the bottom"

0 comments on commit 2eb2698

Please sign in to comment.