Skip to content
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ jobs:
- name: Install node.js
uses: actions/setup-node@v4
with:
node-version: "18"
node-version: "22"
cache: npm
cache-dependency-path: examples/brownian/shinymediapipe/package-lock.json
- name: Install node.js package
Expand All @@ -206,6 +206,49 @@ jobs:
path: test-results/
retention-days: 5

playwright-ai:
if: github.event_name != 'release'
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.12", "3.11", "3.10", "3.9"]
browser: ["chromium", "firefox", "webkit"]
exclude:
- python-version: ${{ github.event.pull_request.draft && '3.11' }}
- python-version: ${{ github.event.pull_request.draft && '3.10' }}
- python-version: ${{ github.event.pull_request.draft && '3.9' }}
- browser: ${{ github.event.pull_request.draft && 'firefox' }}
- browser: ${{ github.event.pull_request.draft && 'webkit' }}
fail-fast: false

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup py-shiny
uses: ./.github/py-shiny/setup
with:
python-version: ${{ matrix.python-version }}
- name: Determine browsers for testing
uses: ./.github/py-shiny/pytest-browsers
id: browsers
with:
browser: ${{ matrix.browser }}
# If anything other than `true`, it will heavily reduce webkit performance
# Related: https://github.com/microsoft/playwright/issues/18119
disable-playwright-diagnostics: ${{ matrix.browser == 'webkit' || matrix.browser == 'firefox' }}

- name: Run playwright tests for AI generated apps
timeout-minutes: 60
run: |
make playwright-ai SUB_FILE=". --numprocesses 3 ${{ steps.browsers.outputs.playwright-diagnostic-args }}" ${{ steps.browsers.outputs.browsers }}
- uses: actions/upload-artifact@v4
if: failure() && steps.browsers.outputs.has-playwright-diagnostics
with:
name: "playright-ai-${{ runner.os }}-${{ matrix.python-version }}-${{ matrix.browser }}-results"
path: test-results/
retention-days: 5

playwright-deploys-precheck:
if: github.event_name != 'release'
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

* Fixed an issue where the `.update_user_input()` method on `ui.Chat()` isn't working in shinylive. (#1891)

* Fixed an issue with the `.click()` method on InputActionButton controllers in `shiny.playwright.controllers` where the method would not work as expected. (#1886)

## [1.3.0] - 2025-03-03

### New features
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ TEST_FILE:=tests/playwright/$(SUB_FILE)
DEPLOYS_TEST_FILE:=tests/playwright/deploys$(SUB_FILE)
SHINY_TEST_FILE:=tests/playwright/shiny/$(SUB_FILE)
EXAMPLES_TEST_FILE:=tests/playwright/examples/$(SUB_FILE)
AI_TEST_FILE:=tests/playwright/ai_generated_apps/$(SUB_FILE)

install-playwright: FORCE
playwright install --with-deps
Expand Down Expand Up @@ -187,6 +188,10 @@ playwright-deploys: FORCE
playwright-examples: FORCE
$(MAKE) playwright TEST_FILE="$(EXAMPLES_TEST_FILE)"

# end-to-end tests for all AI generated apps
playwright-ai: FORCE
$(MAKE) playwright TEST_FILE="$(AI_TEST_FILE)"

coverage: FORCE ## check combined code coverage (must run e2e last)
pytest --cov-report term-missing --cov=shiny tests/pytest/ $(SHINY_TEST_FILE) $(PYTEST_BROWSERS)
coverage html
Expand Down
2 changes: 1 addition & 1 deletion pyrightconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"docs",
"tests/playwright/deploys/*/app.py",
"shiny/templates",
"tests/playwright/shiny/tests_for_ai_generated_apps"
"tests/playwright/ai_generated_apps",
],
"typeCheckingMode": "strict",
"reportImportCycles": "none",
Expand Down
8 changes: 6 additions & 2 deletions shiny/playwright/controller/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,12 @@ def set_text(


def _expect_multiple(loc: Locator, multiple: bool, timeout: Timeout = None) -> None:
value = "True" if multiple else None
_expect_style_to_have_value(loc, "multiple", value, timeout=timeout)
_expect_attribute_to_have_value(
loc,
"multiple",
value="multiple" if multiple else None,
timeout=timeout,
)


######################################################
Expand Down
2 changes: 1 addition & 1 deletion shiny/playwright/controller/_input_buttons.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def click(self, *, timeout: Timeout = None):
timeout
The maximum time to wait for the input dark mode to be clicked. Defaults to `None`.
"""
self.loc.click(timeout=timeout)
self.loc.locator("button").click(timeout=timeout)
return self

def expect_mode(self, value: str, *, timeout: Timeout = None):
Expand Down
65 changes: 65 additions & 0 deletions tests/playwright/ai_generated_apps/accordion_panel/app-core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from shiny import App, render, ui

# Define the UI
app_ui = ui.page_fillable(
# Add Font Awesome CSS in the head section
ui.tags.head(
ui.tags.link(
rel="stylesheet",
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css",
)
),
# Card containing accordion
ui.card(
ui.accordion(
# Basic Panel
ui.accordion_panel(
"Basic Panel",
ui.markdown("This is a basic panel with just a title parameter"),
),
# Panel with title and value
ui.accordion_panel(
"Panel with Value",
ui.markdown("This panel has both a title and a value parameter"),
value="panel2",
),
# Panel with title, value, and icon
ui.accordion_panel(
"Panel with Icon",
ui.markdown("This panel includes an icon parameter using Font Awesome"),
value="panel3",
icon=ui.tags.i(
class_="fa-solid fa-shield-halved", style="font-size: 1rem;"
),
),
# Panel with title, value, icon, and custom attributes
ui.accordion_panel(
"Panel with Custom Attributes",
ui.markdown(
"This panel demonstrates custom attributes (class and style)"
),
value="panel4",
icon=ui.tags.i(class_="fa-solid fa-star", style="font-size: 1rem;"),
class_="custom-panel",
style="background-color: #f8f9fa;",
),
id="acc",
open=True,
multiple=True,
),
),
# Output for selected panel
ui.output_text("selected_panel"),
)


# Define the server
def server(input, output, session):
@output
@render.text
def selected_panel():
return f"Currently selected panel: {input.acc()}"


# Create the app
app = App(app_ui, server)
46 changes: 46 additions & 0 deletions tests/playwright/ai_generated_apps/accordion_panel/app-express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from shiny.express import input, render, ui

# Add Font Awesome CSS in the head section first
ui.head_content(
ui.HTML(
'<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css">'
)
)

ui.page_opts(fillable=True)

with ui.card():
with ui.accordion(id="acc", open=True, multiple=True):
# Panel with just title
with ui.accordion_panel("Basic Panel"):
ui.markdown("This is a basic panel with just a title parameter")

# Panel with title and value
with ui.accordion_panel("Panel with Value", value="panel2"):
ui.markdown("This panel has both a title and a value parameter")

# Panel with title, value, and icon
with ui.accordion_panel(
"Panel with Icon",
value="panel3",
icon=ui.tags.i(
class_="fa-solid fa-shield-halved", style="font-size: 1rem;"
),
):
ui.markdown("This panel includes an icon parameter using Font Awesome")

# Panel with title, value, icon, and custom attributes
with ui.accordion_panel(
"Panel with Custom Attributes",
value="panel4",
icon=ui.tags.i(class_="fa-solid fa-star", style="font-size: 1rem;"),
class_="custom-panel",
style="background-color: #f8f9fa;",
):
ui.markdown("This panel demonstrates custom attributes (class and style)")


# Show which panel is currently selected
@render.text
def selected_panel():
return f"Currently selected panel: {input.acc()}"
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from playwright.sync_api import Page

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

app = create_app_fixture(["app-core.py", "app-express.py"])


def test_accordion_demo(page: Page, app: ShinyAppProc) -> None:
page.goto(app.url)

# Test accordion with ID "acc"
accordion = controller.Accordion(page, "acc")

# Test that multiple panels can be open
accordion.expect_multiple(True)

# Test that accordion is initially open
panel1 = accordion.accordion_panel("Basic Panel")
panel2 = accordion.accordion_panel("panel2")
panel3 = accordion.accordion_panel("panel3")
panel4 = accordion.accordion_panel("panel4")

# Test initial state
panel1.expect_open(True)
panel2.expect_open(True)
panel3.expect_open(True)
panel4.expect_open(True)

# Test panel labels
panel1.expect_label("Basic Panel")
panel2.expect_label("Panel with Value")
panel3.expect_label("Panel with Icon")
panel4.expect_label("Panel with Custom Attributes")

# Test panel content
panel1.expect_body("This is a basic panel with just a title parameter")
panel2.expect_body("This panel has both a title and a value parameter")
panel3.expect_body("This panel includes an icon parameter using Font Awesome")
panel4.expect_body("This panel demonstrates custom attributes (class and style)")

# Test icons (presence/absence)
panel1.expect_icon(False) # First panel has no icon
panel2.expect_icon(False) # Second panel has no icon
panel3.expect_icon(True) # Third panel has an icon
panel4.expect_icon(True) # Fourth panel has an icon

# Test closing and opening panels
panel1.set(False)
panel1.expect_open(False)
panel1.set(True)
panel1.expect_open(True)

# Test output text that shows selected panel
selected_text = controller.OutputText(page, "selected_panel")
selected_text.expect_value(
"Currently selected panel: ('Basic Panel', 'panel2', 'panel3', 'panel4')"
)
46 changes: 46 additions & 0 deletions tests/playwright/ai_generated_apps/card/app-core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from shiny import App, render, ui

# Define the UI
app_ui = ui.page_fillable(
# Card with all possible parameters
ui.card(
ui.card_header("Card Demo", "This demonstrates all card parameters"),
ui.markdown(
"""
This is the main content of the card.
The card has various parameters set including:

* full_screen=True - allows expanding to full screen
* height='300px' - sets fixed height
* fill=True - allows card to grow/shrink
* class_='my-4' - adds custom CSS classes
"""
),
ui.card_footer("Card Footer", class_="text-muted"),
id="demo_card",
full_screen=True, # Allow card to be expanded to full screen
height="300px", # Set card height
fill=True, # Allow card to grow/shrink to fit container
class_="my-4", # Add custom CSS classes
),
# Another card showing dynamic content
ui.card(
ui.card_header("Dynamic Content Demo"),
ui.output_text("dynamic_content"),
id="dynamic_card",
full_screen=True,
height="200px",
class_="mt-4",
),
)


# Define the server
def server(input, output, session):
@render.text
def dynamic_content():
return "This card shows how to include dynamic content using render functions"


# Create the app
app = App(app_ui, server)
39 changes: 39 additions & 0 deletions tests/playwright/ai_generated_apps/card/app-express.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
from shiny.express import render, ui

# Set page options
ui.page_opts(fillable=True)

# Card with all possible parameters
with ui.card(
id="demo_card",
full_screen=True, # Allow card to be expanded to full screen
height="300px", # Set card height
fill=True, # Allow card to grow/shrink to fit container
class_="my-4", # Add custom CSS classes
):
# Card header
ui.card_header("Card Demo", "This demonstrates all card parameters")

# Card body content
ui.markdown(
"""
This is the main content of the card.
The card has various parameters set including:

* full_screen=True - allows expanding to full screen
* height='300px' - sets fixed height
* fill=True - allows card to grow/shrink
* class_='my-4' - adds custom CSS classes
"""
)

# Card footer
ui.card_footer("Card Footer", class_="text-muted")

# Another card showing dynamic content
with ui.card(id="dynamic_card", full_screen=True, height="200px", class_="mt-4"):
ui.card_header("Dynamic Content Demo")

@render.text
def dynamic_content():
return "This card shows how to include dynamic content using render functions"
Loading
Loading