diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index a20a1a9a5..ad355f70b 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -187,7 +187,6 @@ jobs: - name: Install node.js uses: actions/setup-node@v4 with: - node-version: "18" cache: npm cache-dependency-path: examples/brownian/shinymediapipe/package-lock.json - name: Install node.js package @@ -206,6 +205,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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c2b84243..3d4b61bc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Makefile b/Makefile index ca216e7dd..215ef4cb4 100644 --- a/Makefile +++ b/Makefile @@ -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 @@ -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 diff --git a/pyrightconfig.json b/pyrightconfig.json index 23f58eacb..236aed7fc 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -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", diff --git a/shiny/playwright/controller/_base.py b/shiny/playwright/controller/_base.py index bd4433ab7..acde32d97 100644 --- a/shiny/playwright/controller/_base.py +++ b/shiny/playwright/controller/_base.py @@ -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, + ) ###################################################### diff --git a/shiny/playwright/controller/_input_buttons.py b/shiny/playwright/controller/_input_buttons.py index 37433a80a..287053185 100644 --- a/shiny/playwright/controller/_input_buttons.py +++ b/shiny/playwright/controller/_input_buttons.py @@ -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): diff --git a/tests/playwright/shiny/tests_for_ai_generated_apps/accordion/app-core.py b/tests/playwright/ai_generated_apps/accordion/app-core.py similarity index 100% rename from tests/playwright/shiny/tests_for_ai_generated_apps/accordion/app-core.py rename to tests/playwright/ai_generated_apps/accordion/app-core.py diff --git a/tests/playwright/shiny/tests_for_ai_generated_apps/accordion/app-express.py b/tests/playwright/ai_generated_apps/accordion/app-express.py similarity index 100% rename from tests/playwright/shiny/tests_for_ai_generated_apps/accordion/app-express.py rename to tests/playwright/ai_generated_apps/accordion/app-express.py diff --git a/tests/playwright/shiny/tests_for_ai_generated_apps/accordion/test_accordion_core_express.py b/tests/playwright/ai_generated_apps/accordion/test_accordion_core_express.py similarity index 100% rename from tests/playwright/shiny/tests_for_ai_generated_apps/accordion/test_accordion_core_express.py rename to tests/playwright/ai_generated_apps/accordion/test_accordion_core_express.py diff --git a/tests/playwright/ai_generated_apps/accordion_panel/app-core.py b/tests/playwright/ai_generated_apps/accordion_panel/app-core.py new file mode 100644 index 000000000..1ef556042 --- /dev/null +++ b/tests/playwright/ai_generated_apps/accordion_panel/app-core.py @@ -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) diff --git a/tests/playwright/ai_generated_apps/accordion_panel/app-express.py b/tests/playwright/ai_generated_apps/accordion_panel/app-express.py new file mode 100644 index 000000000..e4f72674d --- /dev/null +++ b/tests/playwright/ai_generated_apps/accordion_panel/app-express.py @@ -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( + '' + ) +) + +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()}" diff --git a/tests/playwright/ai_generated_apps/accordion_panel/test_accordion_panel_core_express.py b/tests/playwright/ai_generated_apps/accordion_panel/test_accordion_panel_core_express.py new file mode 100644 index 000000000..30d34eba9 --- /dev/null +++ b/tests/playwright/ai_generated_apps/accordion_panel/test_accordion_panel_core_express.py @@ -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')" + ) diff --git a/tests/playwright/ai_generated_apps/card/app-core.py b/tests/playwright/ai_generated_apps/card/app-core.py new file mode 100644 index 000000000..c84a329fd --- /dev/null +++ b/tests/playwright/ai_generated_apps/card/app-core.py @@ -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) diff --git a/tests/playwright/ai_generated_apps/card/app-express.py b/tests/playwright/ai_generated_apps/card/app-express.py new file mode 100644 index 000000000..6a30cdd9a --- /dev/null +++ b/tests/playwright/ai_generated_apps/card/app-express.py @@ -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" diff --git a/tests/playwright/ai_generated_apps/card/test_card_core_express.py b/tests/playwright/ai_generated_apps/card/test_card_core_express.py new file mode 100644 index 000000000..7dad28078 --- /dev/null +++ b/tests/playwright/ai_generated_apps/card/test_card_core_express.py @@ -0,0 +1,29 @@ +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_cards(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + # Test demo_card + demo_card = controller.Card(page, "demo_card") + demo_card.expect_header("Card DemoThis demonstrates all card parameters") + demo_card.expect_height("300px") + demo_card.expect_footer("Card Footer") + demo_card.expect_full_screen_available(True) + + # Test dynamic_card + dynamic_card = controller.Card(page, "dynamic_card") + dynamic_card.expect_header("Dynamic Content Demo") + dynamic_card.expect_height("200px") + dynamic_card.expect_full_screen_available(True) + + # Test dynamic content in dynamic_card + dynamic_content = controller.OutputText(page, "dynamic_content") + dynamic_content.expect_value( + "This card shows how to include dynamic content using render functions" + ) diff --git a/tests/playwright/ai_generated_apps/card_footer/app-core.py b/tests/playwright/ai_generated_apps/card_footer/app-core.py new file mode 100644 index 000000000..572ac84cd --- /dev/null +++ b/tests/playwright/ai_generated_apps/card_footer/app-core.py @@ -0,0 +1,52 @@ +from shiny import App, render, ui + +app_ui = ui.page_fillable( + # First card with basic footer + ui.card( + ui.card_header("Basic Card with Header"), + "This is a basic card with header and footer", + ui.card_footer("Footer content", class_="bg-light"), + full_screen=True, + height="300px", + id="card1", + ), + # Second card with complex footer + ui.card( + ui.card_header("Card with Complex Footer"), + "This card shows different types of content in footer", + ui.card_footer( + ui.HTML("Bold text"), + ui.tags.span(" | ", style="margin: 0 10px;"), + ui.tags.em("Emphasized text"), + ui.tags.span(" | ", style="margin: 0 10px;"), + ui.tags.span("Regular text", style="color: green;"), + ), + full_screen=True, + height="600px", + id="card2", + ), + # Third card with interactive footer + ui.card( + ui.card_header("Card with Interactive Footer"), + "This card has interactive elements in its footer", + ui.card_footer( + ui.input_action_button("btn", "Click Me", class_="btn-primary"), + ui.output_text("click_count"), + class_="d-flex justify-content-between align-items-center", + ), + full_screen=True, + height="400px", + id="card3", + ), +) + + +def server(input, output, session): + @render.text + def click_count(): + if not input.btn(): + return "No clicks yet" + return f"Clicked {input.btn()} times" + + +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/card_footer/app-express.py b/tests/playwright/ai_generated_apps/card_footer/app-express.py new file mode 100644 index 000000000..22761bafc --- /dev/null +++ b/tests/playwright/ai_generated_apps/card_footer/app-express.py @@ -0,0 +1,36 @@ +from shiny.express import input, render, ui + +# Set page options including title +ui.page_opts(title="Card Footer Demo", fillable=True) + +# Create a card with footer to demonstrate basic parameters +with ui.card(full_screen=True, height="300px", id="card1"): + ui.card_header("Basic Card with Header") + "This is a basic card with header and footer" + ui.card_footer("Footer content", class_="bg-light") + +# Create another card to show different content types in footer +with ui.card(full_screen=True, height="600px", id="card2"): + ui.card_header("Card with Complex Footer") + "This card shows different types of content in footer" + + with ui.card_footer(): + ui.HTML("Bold text") + ui.tags.span(" | ", style="margin: 0 10px;") + ui.tags.em("Emphasized text") + ui.tags.span(" | ", style="margin: 0 10px;") + ui.tags.span("Regular text", style="color: green;") + +# Add a third card with interactive elements in footer +with ui.card(full_screen=True, height="400px", id="card3"): + ui.card_header("Card with Interactive Footer") + "This card has interactive elements in its footer" + + with ui.card_footer(class_="d-flex justify-content-between align-items-center"): + ui.input_action_button("btn", "Click Me", class_="btn-primary") + + @render.text + def click_count(): + if not input.btn(): + return "No clicks yet" + return f"Clicked {input.btn()} times" diff --git a/tests/playwright/ai_generated_apps/card_footer/test_card_footer_core_express.py b/tests/playwright/ai_generated_apps/card_footer/test_card_footer_core_express.py new file mode 100644 index 000000000..211000fa3 --- /dev/null +++ b/tests/playwright/ai_generated_apps/card_footer/test_card_footer_core_express.py @@ -0,0 +1,47 @@ +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_cards_with_footer(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test card1 + card1 = controller.Card(page, "card1") + card1.expect_header("Basic Card with Header") + card1.expect_height("300px") + card1.expect_footer("Footer content") + card1.expect_full_screen_available(True) + + # Test card2 + card2 = controller.Card(page, "card2") + card2.expect_header("Card with Complex Footer") + card2.expect_height("600px") + card2.expect_footer("Bold text | Emphasized text | Regular text") + card2.expect_full_screen_available(True) + + # Test card3 + card3 = controller.Card(page, "card3") + card3.expect_header("Card with Interactive Footer") + card3.expect_height("400px") + card3.expect_full_screen_available(True) + + # Test the button and its interaction + btn = controller.InputActionButton(page, "btn") + btn.expect_label("Click Me") + + # Test initial state + output_text = controller.OutputText(page, "click_count") + output_text.expect_value("No clicks yet") + + # Test after clicking + btn.click() + output_text.expect_value("Clicked 1 times") + + # Test multiple clicks + btn.click() + output_text.expect_value("Clicked 2 times") diff --git a/tests/playwright/ai_generated_apps/input_action_button/app-core.py b/tests/playwright/ai_generated_apps/input_action_button/app-core.py new file mode 100644 index 000000000..84e26dec3 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_action_button/app-core.py @@ -0,0 +1,58 @@ +from shiny import App, render, ui + +# Create the UI +app_ui = ui.page_fluid( + # Add Font Awesome CSS in the head + ui.tags.head( + ui.tags.link( + rel="stylesheet", + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css", + ) + ), + # Main layout + ui.layout_column_wrap( + ui.card( + ui.card_header("Action Button Examples"), + # Basic button with width parameter + ui.input_action_button(id="btn1", label="Basic Button", width="200px"), + ui.br(), # Add spacing + # Button with icon and disabled state + ui.input_action_button( + id="btn2", + label="Disabled Button with Icon", + icon=ui.tags.i(class_="fa-solid fa-shield-halved"), + disabled=True, + ), + ui.br(), # Add spacing + # Button with custom class and style attributes + ui.input_action_button( + id="btn3", + label="Styled Button", + class_="btn-success", + style="margin-top: 20px;", + ), + ), + # Card for displaying results + ui.card( + ui.card_header("Click Counts"), + ui.output_text("click_counts"), + ), + width="100%", + ), +) + + +# Define the server +def server(input, output, session): + @output + @render.text + def click_counts(): + return ( + f"Button 1 clicks: {input.btn1() or 0}\n" + f"Button 2 clicks: {input.btn2() or 0}\n" + f"Button 3 clicks: {input.btn3() or 0}" + ) + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_action_button/app-express.py b/tests/playwright/ai_generated_apps/input_action_button/app-express.py new file mode 100644 index 000000000..d65be4267 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_action_button/app-express.py @@ -0,0 +1,48 @@ +from shiny.express import input, render, ui + +# Add Font Awesome CSS for icons - this needs to be before any UI elements +ui.head_content( + ui.HTML( + '' + ) +) + +# Create a layout with some spacing +with ui.layout_column_wrap(width="100%"): + with ui.card(): + ui.card_header("Action Button Examples") + + # Basic button with width parameter + ui.input_action_button(id="btn1", label="Basic Button", width="200px") + + ui.br() # Add some spacing + + # Button with icon and disabled state + ui.input_action_button( + id="btn2", + label="Disabled Button with Icon", + icon=ui.tags.i(class_="fa-solid fa-shield-halved"), + disabled=True, + ) + + ui.br() # Add some spacing + + # Button with custom class and style attributes + ui.input_action_button( + id="btn3", + label="Styled Button", + class_="btn-success", + style="margin-top: 20px;", + ) + + # Create another card for displaying results + with ui.card(): + ui.card_header("Click Counts") + + @render.text + def click_counts(): + return ( + f"Button 1 clicks: {input.btn1() or 0}\n" + f"Button 2 clicks: {input.btn2() or 0}\n" + f"Button 3 clicks: {input.btn3() or 0}" + ) diff --git a/tests/playwright/ai_generated_apps/input_action_button/test_input_action_button_core_express.py b/tests/playwright/ai_generated_apps/input_action_button/test_input_action_button_core_express.py new file mode 100644 index 000000000..74e9c4027 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_action_button/test_input_action_button_core_express.py @@ -0,0 +1,44 @@ +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_action_buttons(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test basic button (btn1) + btn1 = controller.InputActionButton(page, "btn1") + btn1.expect_label("Basic Button") + btn1.expect_width("200px") + + # Test button with icon (btn2) + btn2 = controller.InputActionButton(page, "btn2") + btn2.expect_label("Disabled Button with Icon") + + # Test styled button (btn3) + btn3 = controller.InputActionButton(page, "btn3") + btn3.expect_label("Styled Button") + + # Test initial click counts + click_counts = controller.OutputText(page, "click_counts") + click_counts.expect_value( + "Button 1 clicks: 0\nButton 2 clicks: 0\nButton 3 clicks: 0" + ) + + # Test clicking btn1 and verify count updates + btn1.click() + click_counts.expect_value( + "Button 1 clicks: 1\nButton 2 clicks: 0\nButton 3 clicks: 0" + ) + + # Test clicking btn3 and verify count updates + btn3.click() + click_counts.expect_value( + "Button 1 clicks: 1\nButton 2 clicks: 0\nButton 3 clicks: 1" + ) + + # btn2 is disabled, so we don't test clicking it diff --git a/tests/playwright/ai_generated_apps/input_action_link/app-core.py b/tests/playwright/ai_generated_apps/input_action_link/app-core.py new file mode 100644 index 000000000..aaa54a556 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_action_link/app-core.py @@ -0,0 +1,62 @@ +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", + ) + ), + # Create layout with columns + ui.layout_column_wrap( + # First card + ui.card( + ui.card_header("Action Link Demo"), + # Create an action link with an icon + ui.input_action_link( + id="demo_link", + label="Click Me!", + icon=ui.tags.i(class_="fa-solid fa-shield-halved"), + ), + ui.output_text("link_clicks"), + full_screen=True, + height="300px", + id="card1", + ), + # Second card + ui.card( + ui.card_header("Click History"), + ui.output_text("click_history"), + full_screen=True, + height="300px", + id="card2", + ), + width=1 / 2, + ), +) + + +# Define the server +def server(input, output, session): + @output + @render.text + def link_clicks(): + count = input.demo_link() or 0 + return f"The link has been clicked {count} times" + + @output + @render.text + def click_history(): + count = input.demo_link() or 0 + if count == 0: + return "No clicks yet!" + elif count == 1: + return "First click recorded!" + else: + return f"You've clicked {count} times. Keep going!" + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_action_link/app-express.py b/tests/playwright/ai_generated_apps/input_action_link/app-express.py new file mode 100644 index 000000000..a3bb67e83 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_action_link/app-express.py @@ -0,0 +1,43 @@ +from shiny.express import input, render, ui + +# Add Font Awesome CSS in the head section +ui.head_content( + ui.HTML( + '' + ) +) + +# Set page title +ui.page_opts(fillable=True) + +with ui.layout_column_wrap(width=1 / 2): + # Create a card to hold the action link + with ui.card(full_screen=True, height="300px", id="card1"): + ui.card_header("Action Link Demo") + + # Create an action link with an icon + ui.input_action_link( + id="demo_link", + label="Click Me!", + icon=ui.tags.i(class_="fa-solid fa-shield-halved"), + ) + + # Display the click count + @render.text + def link_clicks(): + count = input.demo_link() or 0 + return f"The link has been clicked {count} times" + + # Create another card to show reactive behavior + with ui.card(full_screen=True, height="300px", id="card2"): + ui.card_header("Click History") + + @render.text + def click_history(): + count = input.demo_link() or 0 + if count == 0: + return "No clicks yet!" + elif count == 1: + return "First click recorded!" + else: + return f"You've clicked {count} times. Keep going!" diff --git a/tests/playwright/ai_generated_apps/input_action_link/test_input_action_link_core_express.py b/tests/playwright/ai_generated_apps/input_action_link/test_input_action_link_core_express.py new file mode 100644 index 000000000..bba4557b8 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_action_link/test_input_action_link_core_express.py @@ -0,0 +1,44 @@ +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_action_link_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test first card + card1 = controller.Card(page, "card1") + card1.expect_header("Action Link Demo") + card1.expect_height("300px") + card1.expect_full_screen_available(True) + + # Test second card + card2 = controller.Card(page, "card2") + card2.expect_header("Click History") + card2.expect_height("300px") + card2.expect_full_screen_available(True) + + # Test action link + demo_link = controller.InputActionLink(page, "demo_link") + demo_link.expect_label("Click Me!") + + # Test initial states of outputs + output_text = controller.OutputText(page, "link_clicks") + output_text.expect_value("The link has been clicked 0 times") + + history_text = controller.OutputText(page, "click_history") + history_text.expect_value("No clicks yet!") + + # Test interaction - first click + demo_link.click() + output_text.expect_value("The link has been clicked 1 times") + history_text.expect_value("First click recorded!") + + # Test interaction - second click + demo_link.click() + output_text.expect_value("The link has been clicked 2 times") + history_text.expect_value("You've clicked 2 times. Keep going!") diff --git a/tests/playwright/ai_generated_apps/input_checkbox/app-core.py b/tests/playwright/ai_generated_apps/input_checkbox/app-core.py new file mode 100644 index 000000000..e75e6e118 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_checkbox/app-core.py @@ -0,0 +1,40 @@ +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fluid( + # Card containing all elements + ui.card( + ui.card_header("Checkbox Demo"), + # Basic checkbox with default value (False) + ui.input_checkbox(id="basic", label="Basic checkbox"), + # Checkbox with initial value set to True + ui.input_checkbox( + id="preset_value", label="Checkbox with preset value", value=True + ), + # Checkbox with custom width + ui.input_checkbox( + id="custom_width", + label="Checkbox with custom width (300px)", + value=False, + width="300px", + ), + # Output UI for checkbox values + ui.output_ui("checkbox_values"), + ) +) + + +# Define the server +def server(input, output, session): + @output + @render.ui + def checkbox_values(): + return ui.tags.div( + ui.tags.p(f"Basic checkbox value: {input.basic()}"), + ui.tags.p(f"Preset value checkbox: {input.preset_value()}"), + ui.tags.p(f"Custom width checkbox: {input.custom_width()}"), + ) + + +# Create the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_checkbox/app-express.py b/tests/playwright/ai_generated_apps/input_checkbox/app-express.py new file mode 100644 index 000000000..7e5a0bc61 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_checkbox/app-express.py @@ -0,0 +1,27 @@ +from shiny.express import input, render, ui + +with ui.card(): + ui.card_header("Checkbox Demo") + + # Basic checkbox with default value (False) + ui.input_checkbox(id="basic", label="Basic checkbox") + + # Checkbox with initial value set to True + ui.input_checkbox(id="preset_value", label="Checkbox with preset value", value=True) + + # Checkbox with custom width + ui.input_checkbox( + id="custom_width", + label="Checkbox with custom width (300px)", + value=False, + width="300px", + ) + + # Display the values of all checkboxes + @render.ui + def checkbox_values(): + return ui.tags.div( + ui.tags.p(f"Basic checkbox value: {input.basic()}"), + ui.tags.p(f"Preset value checkbox: {input.preset_value()}"), + ui.tags.p(f"Custom width checkbox: {input.custom_width()}"), + ) diff --git a/tests/playwright/ai_generated_apps/input_checkbox/test_input_checkbox_core_express.py b/tests/playwright/ai_generated_apps/input_checkbox/test_input_checkbox_core_express.py new file mode 100644 index 000000000..18b68ce4f --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_checkbox/test_input_checkbox_core_express.py @@ -0,0 +1,37 @@ +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_checkbox_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test basic checkbox + basic_checkbox = controller.InputCheckbox(page, "basic") + basic_checkbox.expect_label("Basic checkbox") + basic_checkbox.expect_checked(False) # Default value should be False + + # Test checkbox with preset value + preset_checkbox = controller.InputCheckbox(page, "preset_value") + preset_checkbox.expect_label("Checkbox with preset value") + preset_checkbox.expect_checked(True) # Should be initially True + + # Test checkbox with custom width + custom_width_checkbox = controller.InputCheckbox(page, "custom_width") + custom_width_checkbox.expect_label("Checkbox with custom width (300px)") + custom_width_checkbox.expect_checked(False) # Should be initially False + custom_width_checkbox.expect_width("300px") + + # Test interactivity by changing values + basic_checkbox.set(True) + basic_checkbox.expect_checked(True) + + preset_checkbox.set(False) + preset_checkbox.expect_checked(False) + + custom_width_checkbox.set(True) + custom_width_checkbox.expect_checked(True) diff --git a/tests/playwright/ai_generated_apps/input_checkbox_group/app-core.py b/tests/playwright/ai_generated_apps/input_checkbox_group/app-core.py new file mode 100644 index 000000000..298f65316 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_checkbox_group/app-core.py @@ -0,0 +1,39 @@ +from shiny import App, render, ui + +# Create sample choices with HTML formatting for demonstration +choices = { + "red": ui.span("Red", style="color: #FF0000;"), + "green": ui.span("Green", style="color: #00AA00;"), + "blue": ui.span("Blue", style="color: #0000AA;"), +} + +app_ui = ui.page_fluid( + ui.card( + ui.card_header("Color Selection Demo"), + # Using input_checkbox_group with all its parameters + ui.input_checkbox_group( + id="colors", # Required: unique identifier + label="Choose colors", # Required: label text + choices=choices, # Required: choices as dict with HTML formatting + selected=["red", "blue"], # Optional: pre-selected values + inline=True, # Optional: display choices inline + width="300px", # Optional: CSS width + ), + # Add some spacing + ui.hr(), + # Output for selected values + ui.output_text("selected_colors"), + ) +) + + +def server(input, output, session): + @output + @render.text + def selected_colors(): + if input.colors(): + return f"You selected: {', '.join(input.colors())}" + return "No colors selected" + + +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_checkbox_group/app-express.py b/tests/playwright/ai_generated_apps/input_checkbox_group/app-express.py new file mode 100644 index 000000000..a51840354 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_checkbox_group/app-express.py @@ -0,0 +1,31 @@ +from shiny.express import input, render, ui + +# Create sample choices with HTML formatting for demonstration +choices = { + "red": ui.span("Red", style="color: #FF0000;"), + "green": ui.span("Green", style="color: #00AA00;"), + "blue": ui.span("Blue", style="color: #0000AA;"), +} + +with ui.card(): + ui.card_header("Color Selection Demo") + + # Using input_checkbox_group with all its parameters + ui.input_checkbox_group( + id="colors", # Required: unique identifier + label="Choose colors", # Required: label text + choices=choices, # Required: choices as dict with HTML formatting + selected=["red", "blue"], # Optional: pre-selected values + inline=True, # Optional: display choices inline + width="300px", # Optional: CSS width + ) + + # Add some spacing + ui.hr() + + # Simple output to show selected values + @render.text + def selected_colors(): + if input.colors(): + return f"You selected: {', '.join(input.colors())}" + return "No colors selected" diff --git a/tests/playwright/ai_generated_apps/input_checkbox_group/test_input_checkbox_group_core_express.py b/tests/playwright/ai_generated_apps/input_checkbox_group/test_input_checkbox_group_core_express.py new file mode 100644 index 000000000..afcf9403b --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_checkbox_group/test_input_checkbox_group_core_express.py @@ -0,0 +1,35 @@ +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_checkbox_group_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Get the checkbox group controller + colors = controller.InputCheckboxGroup(page, "colors") + selected_output = controller.OutputText(page, "selected_colors") + + # Test initial state + colors.expect_label("Choose colors") + colors.expect_choices(["red", "green", "blue"]) + colors.expect_selected(["red", "blue"]) # Check pre-selected values + colors.expect_inline(True) + colors.expect_width("300px") + selected_output.expect_value("You selected: red, blue") + + # Test selecting different combinations + colors.set(["green"]) + selected_output.expect_value("You selected: green") + + # Test selecting multiple values + colors.set(["red", "green", "blue"]) + selected_output.expect_value("You selected: red, green, blue") + + # Test deselecting all values + colors.set([]) + selected_output.expect_value("No colors selected") diff --git a/tests/playwright/ai_generated_apps/input_dark_mode/app-core.py b/tests/playwright/ai_generated_apps/input_dark_mode/app-core.py new file mode 100644 index 000000000..8cb4a95b4 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_dark_mode/app-core.py @@ -0,0 +1,41 @@ +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fillable( + # Add Font Awesome CSS in head + ui.tags.head( + ui.tags.link( + rel="stylesheet", + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css", + ) + ), + # Create layout with sidebar + ui.layout_sidebar( + # Sidebar contents + ui.sidebar( + ui.input_dark_mode(id="mode", mode="light"), + ), + # Main panel contents + ui.card( + ui.card_header("Dark Mode Example"), + ui.output_text("current_mode"), + ui.p("This is a demonstration of dark mode functionality."), + ui.div( + ui.tags.i(class_="fa-solid fa-moon", style="font-size: 2rem;"), + ui.span(" Dark Mode Toggle Example", class_="ms-2"), + class_="mt-3", + ), + ), + ), +) + + +# Define the server +def server(input, output, session): + @render.text + def current_mode(): + return f"Current mode: {input.mode()}" + + +# Create the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_dark_mode/app-express.py b/tests/playwright/ai_generated_apps/input_dark_mode/app-express.py new file mode 100644 index 000000000..f09e22482 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_dark_mode/app-express.py @@ -0,0 +1,28 @@ +from shiny.express import input, render, ui + +# Add Font Awesome CSS +ui.head_content( + ui.HTML( + '' + ) +) + +# Set page options +ui.page_opts(fillable=True) + +with ui.layout_sidebar(): + with ui.sidebar(): + ui.input_dark_mode(id="mode", mode="light") + + with ui.card(): + ui.card_header("Dark Mode Example") + + @render.text + def current_mode(): + return f"Current mode: {input.mode()}" + + ui.p("This is a demonstration of dark mode functionality.") + + with ui.div(class_="mt-3"): + ui.tags.i(class_="fa-solid fa-moon", style="font-size: 2rem;") + ui.span(" Dark Mode Toggle Example", class_="ms-2") diff --git a/tests/playwright/ai_generated_apps/input_dark_mode/test_input_dark_mode_core_express.py b/tests/playwright/ai_generated_apps/input_dark_mode/test_input_dark_mode_core_express.py new file mode 100644 index 000000000..3f7436c5c --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_dark_mode/test_input_dark_mode_core_express.py @@ -0,0 +1,18 @@ +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_dark_mode_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test dark mode input + dark_mode = controller.InputDarkMode(page, "mode") + dark_mode.expect_mode("light") # Default mode is light + dark_mode.click() # Toggle dark mode + dark_mode.expect_mode("dark") + dark_mode.expect_page_mode("dark") # Check if page mode is updated diff --git a/tests/playwright/ai_generated_apps/input_date/app-core.py b/tests/playwright/ai_generated_apps/input_date/app-core.py new file mode 100644 index 000000000..8df9cf0ad --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_date/app-core.py @@ -0,0 +1,139 @@ +from datetime import date + +from shiny import App, render, ui + +app_ui = ui.page_fluid( + ui.h2("Date Input Parameters Demo"), + # Basic date input example + ui.div( + ui.input_date("date1", "Default date input:", value="2024-01-01"), + ui.output_text("selected_date1"), + ), + ui.br(), + # Date input with minimum and maximum date constraints + ui.div( + ui.input_date( + "date2", + "Date input with min/max:", + value=date(2024, 1, 1), # Using a date object instead of string + min="2024-01-01", + max="2024-12-31", + ), + ui.output_text("selected_date2"), + ), + ui.br(), + # Date input with custom date format + ui.div( + ui.input_date( + "date3", + "Custom format (mm/dd/yy):", + value="2024-01-01", + format="mm/dd/yy", # Changes how date is displayed + ), + ui.output_text("selected_date3"), + ), + ui.br(), + # Date input that opens to decade view instead of default month view + ui.div( + ui.input_date( + "date4", "Start in decade view:", value="2024-01-01", startview="decade" + ), + ui.output_text("selected_date4"), + ), + ui.br(), + # Date input with week starting on Monday (1) instead of Sunday (0) + ui.div( + ui.input_date( + "date5", "Week starts on Monday:", value="2024-01-01", weekstart=1 + ), + ui.output_text("selected_date5"), + ), + ui.br(), + # Date input with German language localization + ui.div( + ui.input_date("date6", "German language:", value="2024-01-01", language="de"), + ui.output_text("selected_date6"), + ), + ui.br(), + # Date input with custom width + ui.div( + ui.input_date("date7", "Custom width:", value="2024-01-01", width="400px"), + ui.output_text("selected_date7"), + ), + # Date input where calendar doesn't auto-close after selection + ui.div( + ui.input_date( + "date8", "Autoclose disabled:", value="2024-01-01", autoclose=False + ), + ui.output_text("selected_date8"), + ), + ui.br(), + # Date input with specific dates disabled/unavailable for selection + ui.div( + ui.input_date( + "date9", + "Specific dates disabled:", + value="2024-01-01", + datesdisabled=["2024-01-15", "2024-01-16", "2024-01-17"], + ), + ui.output_text("selected_date9"), + ), + ui.br(), + # Date input with weekend days disabled/unavailable for selection + ui.div( + ui.input_date( + "date10", + "Weekends disabled:", + value="2024-01-01", + daysofweekdisabled=[0, 6], # 0 = Sunday, 6 = Saturday + ), + ui.output_text("selected_date10"), + ), +) + + +def server(input, output, session): + # Server functions to display the selected date for each input + @render.text + def selected_date1(): + return f"Selected date: {input.date1()}" + + @render.text + def selected_date2(): + return f"Selected date: {input.date2()}" + + @render.text + def selected_date3(): + return f"Selected date: {input.date3()}" + + @render.text + def selected_date4(): + return f"Selected date: {input.date4()}" + + @render.text + def selected_date5(): + return f"Selected date: {input.date5()}" + + @render.text + def selected_date6(): + return f"Selected date: {input.date6()}" + + @render.text + def selected_date7(): + return f"Selected date: {input.date7()}" + + @render.text + def selected_date8(): + return f"Selected date: {input.date8()}" + + @render.text + def selected_date9(): + return f"Selected date: {input.date9()}" + + @render.text + def selected_date10(): + return f"Selected date: {input.date10()}" + + +# Create and define the Shiny app with UI and server components +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_date/app-express.py b/tests/playwright/ai_generated_apps/input_date/app-express.py new file mode 100644 index 000000000..591a6d045 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_date/app-express.py @@ -0,0 +1,130 @@ +from datetime import date + +from shiny.express import input, render, ui + +ui.page_opts(title="Date Input Parameters Demo", full_width=True) + +# Basic date input +ui.input_date("date1", "Default date input:", value="2024-01-01") + + +@render.text +def selected_date1(): + return f"Selected date: {input.date1()}" + + +ui.br() + +# Date input with min and max dates +ui.input_date( + "date2", + "Date input with min/max:", + value=date(2024, 1, 1), + min="2024-01-01", + max="2024-12-31", +) + + +@render.text +def selected_date2(): + return f"Selected date: {input.date2()}" + + +ui.br() + +# Date input with custom format +ui.input_date( + "date3", "Custom format (mm/dd/yy):", value="2024-01-01", format="mm/dd/yy" +) + + +@render.text +def selected_date3(): + return f"Selected date: {input.date3()}" + + +ui.br() + +# Date input with decade view +ui.input_date("date4", "Start in decade view:", value="2024-01-01", startview="decade") + + +@render.text +def selected_date4(): + return f"Selected date: {input.date4()}" + + +ui.br() + +# Date input with week starting on Monday +ui.input_date("date5", "Week starts on Monday:", value="2024-01-01", weekstart=1) + + +@render.text +def selected_date5(): + return f"Selected date: {input.date5()}" + + +ui.br() + +# Date input with German language +ui.input_date("date6", "German language:", value="2024-01-01", language="de") + + +@render.text +def selected_date6(): + return f"Selected date: {input.date6()}" + + +ui.br() + +# Date input with custom width +ui.input_date("date7", "Custom width:", value="2024-01-01", width="400px") + + +@render.text +def selected_date7(): + return f"Selected date: {input.date7()}" + + +ui.br() + +# Date input with autoclose disabled +ui.input_date("date8", "Autoclose disabled:", value="2024-01-01", autoclose=False) + + +@render.text +def selected_date8(): + return f"Selected date: {input.date8()}" + + +ui.br() + +# Date input with specific dates disabled +ui.input_date( + "date9", + "Specific dates disabled:", + value="2024-01-01", + datesdisabled=["2024-01-15", "2024-01-16", "2024-01-17"], +) + + +@render.text +def selected_date9(): + return f"Selected date: {input.date9()}" + + +ui.br() + +# Date input with specific days of week disabled +ui.input_date( + "date10", + "Weekends disabled:", + value="2024-01-01", + daysofweekdisabled=[0, 6], # 0 = Sunday, 6 = Saturday +) + + +@render.text +def selected_date10(): + return f"Selected date: {input.date10()}" diff --git a/tests/playwright/ai_generated_apps/input_date/test_input_date_core_express.py b/tests/playwright/ai_generated_apps/input_date/test_input_date_core_express.py new file mode 100644 index 000000000..86966c92e --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_date/test_input_date_core_express.py @@ -0,0 +1,78 @@ +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_date_inputs(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test basic date input + date1 = controller.InputDate(page, "date1") + date1.expect_label("Default date input:") + date1.expect_value("2024-01-01") + + # Test date input with min/max range + date2 = controller.InputDate(page, "date2") + date2.expect_label("Date input with min/max:") + date2.expect_value("2024-01-01") + date2.expect_min_date("2024-01-01") + date2.expect_max_date("2024-12-31") + + # Test date input with custom format + date3 = controller.InputDate(page, "date3") + date3.expect_label("Custom format (mm/dd/yy):") + date3.expect_format("mm/dd/yy") + date3.expect_value("01/01/24") + + # Test date input with decade view + date4 = controller.InputDate(page, "date4") + date4.expect_label("Start in decade view:") + date4.expect_value("2024-01-01") + date4.expect_startview("decade") + + # Test date input with Monday start + date5 = controller.InputDate(page, "date5") + date5.expect_label("Week starts on Monday:") + date5.expect_value("2024-01-01") + date5.expect_weekstart("1") + + # Test date input with German language + date6 = controller.InputDate(page, "date6") + date6.expect_label("German language:") + date6.expect_value("2024-01-01") + date6.expect_language("de") + + # Test date input with custom width + date7 = controller.InputDate(page, "date7") + date7.expect_label("Custom width:") + date7.expect_value("2024-01-01") + date7.expect_width("400px") + + # Test date input without autoclose + date8 = controller.InputDate(page, "date8") + date8.expect_label("Autoclose disabled:") + date8.expect_value("2024-01-01") + date8.expect_autoclose("false") + + # Test date input with disabled dates + date9 = controller.InputDate(page, "date9") + date9.expect_label("Specific dates disabled:") + date9.expect_value("2024-01-01") + date9.expect_datesdisabled(["2024-01-15", "2024-01-16", "2024-01-17"]) + + # Test date input with disabled days of week + date10 = controller.InputDate(page, "date10") + date10.expect_label("Weekends disabled:") + date10.expect_value("2024-01-01") + date10.expect_daysofweekdisabled([0, 6]) + + # Test setting new values + date1.set("2024-02-01") + date1.expect_value("2024-02-01") + + date2.set("2024-06-15") + date2.expect_value("2024-06-15") diff --git a/tests/playwright/ai_generated_apps/input_date_range/app-core.py b/tests/playwright/ai_generated_apps/input_date_range/app-core.py new file mode 100644 index 000000000..99ee33cf2 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_date_range/app-core.py @@ -0,0 +1,76 @@ +from datetime import date + +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fluid( + # Add descriptive text + ui.h2("Date Range Input Example"), + ui.markdown( + """ + This example demonstrates a date range input with various customization options: + - Custom date format (mm/dd/yyyy) + - Restricted date range (2020-2025) + - Week starting on Monday + - Custom separator + """ + ), + # Layout wrapper + ui.layout_column_wrap( + # Date range input with all parameters + ui.input_date_range( + id="date_range", + label="Select Date Range", + start=date(2023, 1, 1), # Initial start date + end=date(2023, 12, 31), # Initial end date + min=date(2020, 1, 1), # Minimum allowed date + max=date(2025, 12, 31), # Maximum allowed date + format="mm/dd/yyyy", # Display format + startview="decade", # Initial view when opened + weekstart=1, # Week starts on Monday (0=Sunday, 1=Monday) + language="en", # Language for month/day names + separator=" → ", # Custom separator between dates + width="100%", # Width of the input + autoclose=True, # Close the calendar when a date is selected + ), + # Card to display selected range + ui.card( + ui.card_header("Selected Date Range"), + ui.output_text("selected_range"), + ), + ), + # Additional date info output + ui.output_ui("date_info"), +) + + +# Define the server +def server(input, output, session): + @render.text + def selected_range(): + date_range = input.date_range() + if date_range is None: + return "No dates selected" + + start, end = date_range + return f"Start date: {start}\nEnd date: {end}" + + @render.ui + def date_info(): + date_range = input.date_range() + if date_range is None: + return ui.p("Please select a date range", class_="text-muted") + + start, end = date_range + days = (end - start).days + + return ui.div( + ui.p(f"Number of days selected: {days}"), + ui.p(f"Start day of week: {start.strftime('%A')}"), + ui.p(f"End day of week: {end.strftime('%A')}"), + class_="mt-3", + ) + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_date_range/app-express.py b/tests/playwright/ai_generated_apps/input_date_range/app-express.py new file mode 100644 index 000000000..bbca2566b --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_date_range/app-express.py @@ -0,0 +1,64 @@ +from datetime import date + +from shiny.express import input, render, ui + +# Add some descriptive text +ui.h2("Date Range Input Example") +ui.markdown( + """ +This example demonstrates a date range input with various customization options: +- Custom date format (mm/dd/yyyy) +- Restricted date range (2020-2025) +- Week starting on Monday +- Custom separator +""" +) + +with ui.layout_column_wrap(): + # Create a date range input with all possible parameters + ui.input_date_range( + id="date_range", + label="Select Date Range", + start=date(2023, 1, 1), # Initial start date + end=date(2023, 12, 31), # Initial end date + min=date(2020, 1, 1), # Minimum allowed date + max=date(2025, 12, 31), # Maximum allowed date + format="mm/dd/yyyy", # Display format + startview="decade", # Initial view when opened + weekstart=1, # Week starts on Monday (0=Sunday, 1=Monday) + language="en", # Language for month/day names + separator=" → ", # Custom separator between dates + width="100%", # Width of the input + autoclose=True, # Close the calendar when a date is selected + ) + + # Add a card to display the selected range + with ui.card(): + ui.card_header("Selected Date Range") + + @render.text + def selected_range(): + date_range = input.date_range() + if date_range is None: + return "No dates selected" + + start, end = date_range + return f"Start date: {start}\nEnd date: {end}" + + +# Add some additional information about the selection +@render.ui +def date_info(): + date_range = input.date_range() + if date_range is None: + return ui.p("Please select a date range", class_="text-muted") + + start, end = date_range + days = (end - start).days + + return ui.div( + ui.p(f"Number of days selected: {days}"), + ui.p(f"Start day of week: {start.strftime('%A')}"), + ui.p(f"End day of week: {end.strftime('%A')}"), + class_="mt-3", + ) diff --git a/tests/playwright/ai_generated_apps/input_date_range/test_input_date_range_core_express.py b/tests/playwright/ai_generated_apps/input_date_range/test_input_date_range_core_express.py new file mode 100644 index 000000000..dbf081bf3 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_date_range/test_input_date_range_core_express.py @@ -0,0 +1,47 @@ +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_date_range_input(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Initialize the date range input controller + date_range = controller.InputDateRange(page, "date_range") + + # Test selected_range output + selected_range = controller.OutputText(page, "selected_range") + selected_range.expect_value("Start date: 2023-01-01\nEnd date: 2023-12-31") + + # Test initial properties + date_range.expect_label("Select Date Range") + date_range.expect_value(("01/01/2023", "12/31/2023")) # Test initial values + date_range.expect_min_date("2020-01-01") # Test minimum date + date_range.expect_max_date("2025-12-31") # Test maximum date + date_range.expect_format("mm/dd/yyyy") # Test date format + date_range.expect_startview("decade") # Test start view + date_range.expect_weekstart("1") # Test week start (Monday) + date_range.expect_language("en") # Test language setting + date_range.expect_separator(" → ") # Test separator + date_range.expect_width("100%") # Test width + date_range.expect_autoclose("true") # Test autoclose + + # Test setting new values + date_range.set(("2023-06-01", "2023-06-30")) + date_range.expect_value(("2023-06-01", "2023-06-30")) + + # Test setting only start date + date_range.set(("2023-07-01", None)) + date_range.expect_value(("2023-07-01", "2023-06-30")) + + # Test setting only end date + date_range.set((None, "2023-07-31")) + date_range.expect_value(("2023-07-01", "2023-07-31")) + + # Test setting dates at the boundaries + date_range.set(("2020-01-01", "2025-12-31")) # Min and max dates + date_range.expect_value(("2020-01-01", "2025-12-31")) diff --git a/tests/playwright/ai_generated_apps/input_file/app-core.py b/tests/playwright/ai_generated_apps/input_file/app-core.py new file mode 100644 index 000000000..ce6c9cd5d --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_file/app-core.py @@ -0,0 +1,80 @@ +import pandas as pd + +from shiny import App, reactive, render, ui +from shiny.types import FileInfo + +# Define the UI +app_ui = ui.page_fillable( + # Main card containing file input and table + ui.card( + ui.card_header("File Upload Demo"), + # File input with all possible parameters + ui.input_file( + id="file1", + label="Upload File", + multiple=True, + accept=[".csv", ".txt", "text/plain", "application/pdf", "image/*"], + button_label="Choose Files...", + placeholder="Multiple files can be selected", + width="400px", + capture="user", + ), + # Output for the data grid + ui.output_data_frame("file_table"), + # Instructions card + ui.card( + ui.markdown( + """ + ### Instructions: + 1. Click 'Choose Files...' to select one or more files + 2. Supported file types: CSV, TXT, PDF, and images + 3. File information will appear in the table below + 4. Use filters to search through uploaded files + """ + ) + ), + ) +) + + +# Define the server +def server(input, output, session): + # Reactive calculation for parsing files + @reactive.calc + def parse_files(): + files: list[FileInfo] | None = input.file1() + if not files: + return pd.DataFrame() + + # Create a DataFrame with file information + file_info = [] + for file in files: + file_info.append( + { + "Name": file["name"], + "Size (bytes)": file["size"], + "Type": file["type"], + } + ) + return pd.DataFrame(file_info) + + # Render the data grid + @output + @render.data_frame + def file_table(): + df = parse_files() + if df.empty: + return render.DataGrid( + pd.DataFrame(columns=["Name", "Size (bytes)", "Type"]), + height="300px", + width="100%", + filters=True, + selection_mode="row", + ) + return render.DataGrid( + df, height="300px", width="100%", filters=True, selection_mode="row" + ) + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_file/app-express.py b/tests/playwright/ai_generated_apps/input_file/app-express.py new file mode 100644 index 000000000..eafb7f756 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_file/app-express.py @@ -0,0 +1,68 @@ +import pandas as pd + +from shiny import reactive +from shiny.express import input, render, ui +from shiny.types import FileInfo + +# Set page options +ui.page_opts(title="File Input Demo", fillable=True) + +with ui.card(): + ui.card_header("File Upload Demo") + + # File input with all possible parameters + ui.input_file( + id="file1", + label="Upload File", + multiple=True, + accept=[".csv", ".txt", "text/plain", "application/pdf", "image/*"], + button_label="Choose Files...", + placeholder="Multiple files can be selected", + width="400px", + capture="user", + ) + + @reactive.calc + def parse_files(): + files: list[FileInfo] | None = input.file1() + if not files: + return pd.DataFrame() + + # Create a DataFrame with file information + file_info = [] + for file in files: + file_info.append( + { + "Name": file["name"], + "Size (bytes)": file["size"], + "Type": file["type"], + } + ) + return pd.DataFrame(file_info) + + @render.data_frame + def file_table(): + df = parse_files() + if df.empty: + return render.DataGrid( + pd.DataFrame(columns=["Name", "Size (bytes)", "Type"]), + height="300px", + width="100%", + filters=True, + selection_mode="row", + ) + return render.DataGrid( + df, height="300px", width="100%", filters=True, selection_mode="row" + ) + + # Add some instructions + with ui.card(): + ui.markdown( + """ + ### Instructions: + 1. Click 'Choose Files...' to select one or more files + 2. Supported file types: CSV, TXT, PDF, and images + 3. File information will appear in the table below + 4. Use filters to search through uploaded files + """ + ) diff --git a/tests/playwright/ai_generated_apps/input_file/test_input_file_core_express.py b/tests/playwright/ai_generated_apps/input_file/test_input_file_core_express.py new file mode 100644 index 000000000..45da52e04 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_file/test_input_file_core_express.py @@ -0,0 +1,58 @@ +from playwright.sync_api import FilePayload, 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_file_input_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test file input component + file_input = controller.InputFile(page, "file1") + + # Test initial state + file_input.expect_label("Upload File") + file_input.expect_multiple(True) + file_input.expect_accept( + [".csv", ".txt", "text/plain", "application/pdf", "image/*"] + ) + file_input.expect_button_label("Choose Files...") + file_input.expect_width("400px") + file_input.expect_capture("user") + + # Test data frame output initial state + file_table = controller.OutputDataFrame(page, "file_table") + file_table.expect_column_labels(["Name", "Size (bytes)", "Type"]) + file_table.expect_nrow(0) + file_table.expect_ncol(3) + + # Note: The file content is a CSV with a header and some data + # Simulate uploading a CSV file + file_info: FilePayload = { + "name": "users.csv", + "mimeType": "text/csv", + "buffer": b',user_id,name,email\n1,Alice,alice@example.com\n2,"Bob, Los Angeles", bob\n', + } + + file_input.set(file_info) + file_input.expect_complete() + + # After upload, check if table is updated + file_table.expect_nrow(1) # Should show one row for the uploaded file + file_table.expect_ncol(3) # Should still have 3 columns + + # simulate uploading multiple files + file_info2 = file_info.copy() + file_info2["name"] = "users2.csv" + file_info2["buffer"] = ( + b",user_id,name,email\n3,Charlie,charlie@example.com\n4,Dave,dave@example.com\n" + ) + file_input.set([file_info, file_info2]) + file_input.expect_complete() + + # After uploading multiple files, check if table is updated + file_table.expect_nrow(2) # Should show two rows for the uploaded files + file_table.expect_ncol(3) # Should still have 3 columns diff --git a/tests/playwright/ai_generated_apps/input_numeric/app-core.py b/tests/playwright/ai_generated_apps/input_numeric/app-core.py new file mode 100644 index 000000000..3471a8b65 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_numeric/app-core.py @@ -0,0 +1,62 @@ +from shiny import App, render, ui + +app_ui = ui.page_fluid( + ui.layout_column_wrap( + ui.card( + ui.card_header("Basic Numeric Input"), + ui.input_numeric(id="basic", label="Basic numeric input", value=10), + ui.output_text("basic_value"), + ), + ui.card( + ui.card_header("With Min/Max"), + ui.input_numeric( + id="with_min_max", + label="With min and max values", + value=5, + min=0, + max=10, + ), + ui.output_text("minmax_value"), + ), + ui.card( + ui.card_header("With Step Size"), + ui.input_numeric( + id="with_step", label="With step size", value=0, min=0, max=100, step=5 + ), + ui.output_text("step_value"), + ), + ui.card( + ui.card_header("With Custom Width"), + ui.input_numeric( + id="with_width", label="With custom width", value=42, width="200px" + ), + ui.output_text("width_value"), + ), + width=1 / 2, + ) +) + + +def server(input, output, session): + @output + @render.text + def basic_value(): + return f"Current value: {input.basic()}" + + @output + @render.text + def minmax_value(): + return f"Current value: {input.with_min_max()}" + + @output + @render.text + def step_value(): + return f"Current value: {input.with_step()}" + + @output + @render.text + def width_value(): + return f"Current value: {input.with_width()}" + + +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_numeric/app-express.py b/tests/playwright/ai_generated_apps/input_numeric/app-express.py new file mode 100644 index 000000000..7c3877994 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_numeric/app-express.py @@ -0,0 +1,43 @@ +from shiny.express import input, render, ui + +# Set page title +ui.page_opts(full_width=True) + +with ui.layout_column_wrap(width=1 / 2): + with ui.card(): + ui.card_header("Basic Numeric Input") + ui.input_numeric(id="basic", label="Basic numeric input", value=10) + + @render.text + def basic_value(): + return f"Current value: {input.basic()}" + + with ui.card(): + ui.card_header("With Min/Max") + ui.input_numeric( + id="with_min_max", label="With min and max values", value=5, min=0, max=10 + ) + + @render.text + def minmax_value(): + return f"Current value: {input.with_min_max()}" + + with ui.card(): + ui.card_header("With Step Size") + ui.input_numeric( + id="with_step", label="With step size", value=0, min=0, max=100, step=5 + ) + + @render.text + def step_value(): + return f"Current value: {input.with_step()}" + + with ui.card(): + ui.card_header("With Custom Width") + ui.input_numeric( + id="with_width", label="With custom width", value=42, width="200px" + ) + + @render.text + def width_value(): + return f"Current value: {input.with_width()}" diff --git a/tests/playwright/ai_generated_apps/input_numeric/test_input_numeric_core_express.py b/tests/playwright/ai_generated_apps/input_numeric/test_input_numeric_core_express.py new file mode 100644 index 000000000..82a97d6a8 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_numeric/test_input_numeric_core_express.py @@ -0,0 +1,53 @@ +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_numeric_inputs(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test basic numeric input + basic = controller.InputNumeric(page, "basic") + basic_output = controller.OutputText(page, "basic_value") + + basic.expect_label("Basic numeric input") + basic.expect_value("10") + basic_output.expect_value("Current value: 10") + + # Test with new value + basic.set("20") + basic_output.expect_value("Current value: 20") + + # Test numeric input with min/max + with_min_max = controller.InputNumeric(page, "with_min_max") + minmax_output = controller.OutputText(page, "minmax_value") + + with_min_max.expect_label("With min and max values") + with_min_max.expect_value("5") + with_min_max.expect_min("0") + with_min_max.expect_max("10") + minmax_output.expect_value("Current value: 5") + + # Test with step size + with_step = controller.InputNumeric(page, "with_step") + step_output = controller.OutputText(page, "step_value") + + with_step.expect_label("With step size") + with_step.expect_value("0") + with_step.expect_min("0") + with_step.expect_max("100") + with_step.expect_step("5") + step_output.expect_value("Current value: 0") + + # Test with custom width + with_width = controller.InputNumeric(page, "with_width") + width_output = controller.OutputText(page, "width_value") + + with_width.expect_label("With custom width") + with_width.expect_value("42") + with_width.expect_width("200px") + width_output.expect_value("Current value: 42") diff --git a/tests/playwright/ai_generated_apps/input_password/app-core.py b/tests/playwright/ai_generated_apps/input_password/app-core.py new file mode 100644 index 000000000..e36f5c381 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_password/app-core.py @@ -0,0 +1,46 @@ +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fluid( + # Card container + ui.card( + ui.card_header("Password Input Example"), + # Create password input + ui.input_password( + id="pwd", + label="Enter Password", + value="default123", + width="300px", + placeholder="Type your password here", + ), + # Output for password length + ui.output_text("password_length"), + # Output for masked password + ui.output_text("password_masked"), + ) +) + + +# Define the server +def server(input, output, session): + # Show current input length + @output + @render.text + def password_length(): + pwd = input.pwd() + if not pwd: + return "No password entered" + return f"Password length: {len(pwd)} characters" + + # Show masked password + @output + @render.text + def password_masked(): + pwd = input.pwd() + if not pwd: + return "" + return f"Masked password: {'*' * len(pwd)}" + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_password/app-express.py b/tests/playwright/ai_generated_apps/input_password/app-express.py new file mode 100644 index 000000000..335224daa --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_password/app-express.py @@ -0,0 +1,32 @@ +from shiny.express import input, render, ui + +# Set page title +ui.page_opts(full_width=True) + +with ui.card(): + ui.card_header("Password Input Example") + + # Create password input + ui.input_password( + id="pwd", + label="Enter Password", + value="default123", + width="300px", + placeholder="Type your password here", + ) + + # Show current input length + @render.text + def password_length(): + pwd = input.pwd() + if not pwd: + return "No password entered" + return f"Password length: {len(pwd)} characters" + + # Show masked password + @render.text + def password_masked(): + pwd = input.pwd() + if not pwd: + return "" + return f"Masked password: {'*' * len(pwd)}" diff --git a/tests/playwright/ai_generated_apps/input_password/test_input_password_core_express.py b/tests/playwright/ai_generated_apps/input_password/test_input_password_core_express.py new file mode 100644 index 000000000..3abf1e531 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_password/test_input_password_core_express.py @@ -0,0 +1,32 @@ +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_password_input(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test password input + pwd = controller.InputPassword(page, "pwd") + pwd.expect_label("Enter Password") + pwd.expect_value("default123") + pwd.expect_width("300px") + pwd.expect_placeholder("Type your password here") + + # Test password length output + pwd_length = controller.OutputText(page, "password_length") + pwd_length.expect_value("Password length: 10 characters") + + # Test masked password output + pwd_masked = controller.OutputText(page, "password_masked") + pwd_masked.expect_value("Masked password: **********") + + # Test setting new password + pwd.set("newpass12") + pwd.expect_value("newpass12") + pwd_length.expect_value("Password length: 9 characters") + pwd_masked.expect_value("Masked password: *********") diff --git a/tests/playwright/ai_generated_apps/input_radio_buttons/app-core.py b/tests/playwright/ai_generated_apps/input_radio_buttons/app-core.py new file mode 100644 index 000000000..1c939acc6 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_radio_buttons/app-core.py @@ -0,0 +1,40 @@ +from shiny import App, render, ui + +# Create sample choices with HTML content +choices = { + "choice1": ui.span("Choice 1", style="color: red;"), + "choice2": ui.span("Choice 2", style="color: blue;"), + "choice3": ui.span("Choice 3", style="color: green;"), +} + +# Define the UI +app_ui = ui.page_fluid( + ui.card( + ui.card_header("Radio Buttons Example"), + # Create radio buttons with all possible parameters + ui.input_radio_buttons( + id="radio_demo", # Required: unique identifier + label="Demo Radio Group", # Required: label text + choices=choices, # Required: choices as dict with HTML content + selected="choice1", # Optional: initial selected value + inline=True, # Optional: display buttons inline + width="300px", # Optional: CSS width + ), + # Add some spacing + ui.br(), + # Output for displaying selection + ui.output_text("selection"), + ) +) + + +# Define the server +def server(input, output, session): + @output + @render.text + def selection(): + return f"You selected: {input.radio_demo()}" + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_radio_buttons/app-express.py b/tests/playwright/ai_generated_apps/input_radio_buttons/app-express.py new file mode 100644 index 000000000..9ab37afe8 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_radio_buttons/app-express.py @@ -0,0 +1,33 @@ +from shiny.express import input, render, ui + +# Set page title +ui.page_opts(full_width=True) + +# Create sample choices with HTML content +choices = { + "choice1": ui.span("Choice 1", style="color: red;"), + "choice2": ui.span("Choice 2", style="color: blue;"), + "choice3": ui.span("Choice 3", style="color: green;"), +} + +# Create a card to contain the radio buttons and output +with ui.card(): + ui.card_header("Radio Buttons Example") + + # Create radio buttons with all possible parameters + ui.input_radio_buttons( + id="radio_demo", # Required: unique identifier + label="Demo Radio Group", # Required: label text + choices=choices, # Required: choices as dict with HTML content + selected="choice1", # Optional: initial selected value + inline=True, # Optional: display buttons inline + width="300px", # Optional: CSS width + ) + + # Add some spacing + ui.br() + + # Display the current selection + @render.text + def selection(): + return f"You selected: {input.radio_demo()}" diff --git a/tests/playwright/ai_generated_apps/input_radio_buttons/test_input_radio_buttons_core_express.py b/tests/playwright/ai_generated_apps/input_radio_buttons/test_input_radio_buttons_core_express.py new file mode 100644 index 000000000..0972fcf7e --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_radio_buttons/test_input_radio_buttons_core_express.py @@ -0,0 +1,34 @@ +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_radio_buttons_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test radio buttons input + radio_buttons = controller.InputRadioButtons(page, "radio_demo") + selection_output = controller.OutputText(page, "selection") + + # Test initial state + radio_buttons.expect_label("Demo Radio Group") + radio_buttons.expect_choices(["choice1", "choice2", "choice3"]) + radio_buttons.expect_selected("choice1") # Test default selection + radio_buttons.expect_inline(True) + radio_buttons.expect_width("300px") + selection_output.expect_value("You selected: choice1") + + # Test changing selections + radio_buttons.set("choice2") + selection_output.expect_value("You selected: choice2") + + radio_buttons.set("choice3") + selection_output.expect_value("You selected: choice3") + + # Test going back to first choice + radio_buttons.set("choice1") + selection_output.expect_value("You selected: choice1") diff --git a/tests/playwright/ai_generated_apps/input_select/app-core.py b/tests/playwright/ai_generated_apps/input_select/app-core.py new file mode 100644 index 000000000..e0d8ac126 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_select/app-core.py @@ -0,0 +1,112 @@ +from shiny import App, render, ui + +# Sample data for different types of choices +simple_choices = ["A", "B", "C", "D"] +dict_choices = {"a": "Option A", "b": "Option B", "c": "Option C"} +grouped_choices = { + "Group 1": {"g1a": "Group 1 - A", "g1b": "Group 1 - B"}, + "Group 2": {"g2a": "Group 2 - A", "g2b": "Group 2 - B"}, +} + +app_ui = ui.page_fillable( + ui.layout_column_wrap( + # Basic select with simple choices + ui.card( + ui.card_header("Basic Select"), + ui.input_select( + id="select1", + label="Basic select (simple list)", + choices=simple_choices, + selected="A", + ), + ui.output_text("selected_value1"), + ), + # Select with dictionary choices + ui.card( + ui.card_header("Dictionary Choices"), + ui.input_select( + id="select2", + label="Select with dictionary choices", + choices=dict_choices, + selected="a", + ), + ui.output_text("selected_value2"), + ), + # Select with grouped choices + ui.card( + ui.card_header("Grouped Choices"), + ui.input_select( + id="select3", + label="Select with grouped choices", + choices=grouped_choices, + selected="g1a", + ), + ui.output_text("selected_value3"), + ), + # Multiple select + ui.card( + ui.card_header("Multiple Select"), + ui.input_select( + id="select4", + label="Multiple select", + choices=simple_choices, + selected=["A", "B"], + multiple=True, + ), + ui.output_text("selected_value4"), + ), + # Select with custom width + ui.card( + ui.card_header("Custom Width"), + ui.input_select( + id="select5", + label="Select with custom width", + choices=simple_choices, + width="200px", + ), + ui.output_text("selected_value5"), + ), + # Select with size parameter + ui.card( + ui.card_header("Box Style"), + ui.input_select( + id="select6", + label="Select with size parameter", + choices=simple_choices, + size="4", # Shows 4 items at once + ), + ui.output_text("selected_value6"), + ), + width="400px", + ), +) + + +def server(input, output, session): + + @render.text + def selected_value1(): + return f"Selected: {input.select1()}" + + @render.text + def selected_value2(): + return f"Selected: {input.select2()}" + + @render.text + def selected_value3(): + return f"Selected: {input.select3()}" + + @render.text + def selected_value4(): + return f"Selected: {input.select4()}" + + @render.text + def selected_value5(): + return f"Selected: {input.select5()}" + + @render.text + def selected_value6(): + return f"Selected: {input.select6()}" + + +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_select/app-express.py b/tests/playwright/ai_generated_apps/input_select/app-express.py new file mode 100644 index 000000000..cabf7575e --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_select/app-express.py @@ -0,0 +1,98 @@ +from shiny.express import input, render, ui + +# Sample data for different types of choices +simple_choices = ["A", "B", "C", "D"] +dict_choices = {"a": "Option A", "b": "Option B", "c": "Option C"} +grouped_choices = { + "Group 1": {"g1a": "Group 1 - A", "g1b": "Group 1 - B"}, + "Group 2": {"g2a": "Group 2 - A", "g2b": "Group 2 - B"}, +} + +# Page options +ui.page_opts(fillable=True) + +with ui.layout_column_wrap(width="400px"): + # Basic select with simple choices + with ui.card(): + ui.card_header("Basic Select") + ui.input_select( + id="select1", + label="Basic select (simple list)", + choices=simple_choices, + selected="A", + ) + + @render.text + def selected_value1(): + return f"Selected: {input.select1()}" + + # Select with dictionary choices + with ui.card(): + ui.card_header("Dictionary Choices") + ui.input_select( + id="select2", + label="Select with dictionary choices", + choices=dict_choices, + selected="a", + ) + + @render.text + def selected_value2(): + return f"Selected: {input.select2()}" + + # Select with grouped choices + with ui.card(): + ui.card_header("Grouped Choices") + ui.input_select( + id="select3", + label="Select with grouped choices", + choices=grouped_choices, + selected="g1a", + ) + + @render.text + def selected_value3(): + return f"Selected: {input.select3()}" + + # Multiple select + with ui.card(): + ui.card_header("Multiple Select") + ui.input_select( + id="select4", + label="Multiple select", + choices=simple_choices, + selected=["A", "B"], + multiple=True, + ) + + @render.text + def selected_value4(): + return f"Selected: {input.select4()}" + + # Select with custom width + with ui.card(): + ui.card_header("Custom Width") + ui.input_select( + id="select5", + label="Select with custom width", + choices=simple_choices, + width="200px", + ) + + @render.text + def selected_value5(): + return f"Selected: {input.select5()}" + + # Select with size parameter + with ui.card(): + ui.card_header("Box Style") + ui.input_select( + id="select6", + label="Select with size parameter", + choices=simple_choices, + size="4", # Shows 4 items at once + ) + + @render.text + def selected_value6(): + return f"Selected: {input.select6()}" diff --git a/tests/playwright/ai_generated_apps/input_select/test_input_select_core_express.py b/tests/playwright/ai_generated_apps/input_select/test_input_select_core_express.py new file mode 100644 index 000000000..8b0da4f9f --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_select/test_input_select_core_express.py @@ -0,0 +1,70 @@ +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_input_select_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test basic select with simple choices + select1 = controller.InputSelect(page, "select1") + select1.expect_label("Basic select (simple list)") + select1.expect_choices(["A", "B", "C", "D"]) + select1.expect_selected("A") # Test initial selection + select1.expect_multiple(False) + select1_value = controller.OutputText(page, "selected_value1") + select1_value.expect_value("Selected: A") + + # Test select with dictionary choices + select2 = controller.InputSelect(page, "select2") + select2.expect_label("Select with dictionary choices") + select2.expect_choices(["a", "b", "c"]) + select2.expect_choice_labels(["Option A", "Option B", "Option C"]) + select2.expect_selected("a") # Test initial selection + select2.expect_multiple(False) + select2_value = controller.OutputText(page, "selected_value2") + select2_value.expect_value("Selected: a") + + # Test select with grouped choices + select3 = controller.InputSelect(page, "select3") + select3.expect_label("Select with grouped choices") + select3.expect_choices(["g1a", "g1b", "g2a", "g2b"]) + select3.expect_choice_labels( + ["Group 1 - A", "Group 1 - B", "Group 2 - A", "Group 2 - B"] + ) + select3.expect_choice_groups(["Group 1", "Group 2"]) + select3.expect_selected("g1a") # Test initial selection + select3.expect_multiple(False) + select3_value = controller.OutputText(page, "selected_value3") + select3_value.expect_value("Selected: g1a") + + # Test multiple select + select4 = controller.InputSelect(page, "select4") + select4.expect_label("Multiple select") + select4.expect_choices(["A", "B", "C", "D"]) + select4.expect_selected(["A", "B"]) # Test initial multiple selection + select4.expect_multiple(True) + select4_value = controller.OutputText(page, "selected_value4") + select4_value.expect_value("Selected: ('A', 'B')") + + # Test select with custom width + select5 = controller.InputSelect(page, "select5") + select5.expect_label("Select with custom width") + select5.expect_choices(["A", "B", "C", "D"]) + select5.expect_width("200px") + select5.expect_multiple(False) + select5_value = controller.OutputText(page, "selected_value5") + select5_value.expect_value("Selected: A") + + # Test select with size parameter + select6 = controller.InputSelect(page, "select6") + select6.expect_label("Select with size parameter") + select6.expect_choices(["A", "B", "C", "D"]) + select6.expect_size("4") + select6.expect_multiple(False) + select6_value = controller.OutputText(page, "selected_value6") + select6_value.expect_value("Selected: A") diff --git a/tests/playwright/ai_generated_apps/input_slider/app-core.py b/tests/playwright/ai_generated_apps/input_slider/app-core.py new file mode 100644 index 000000000..f67be899a --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_slider/app-core.py @@ -0,0 +1,162 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +from shiny import App, reactive, render, ui + +# Define a consistent timezone +TIMEZONE = ZoneInfo("UTC") + +app_ui = ui.page_fluid( + ui.panel_title("Slider Parameters Demo"), + ui.layout_column_wrap( + # Numeric Slider - basic parameters + ui.card( + ui.card_header("Basic Numeric Slider"), + ui.input_slider("slider1", "Min, max, value", min=0, max=100, value=50), + ui.output_text("value1"), + ), + # Numeric Slider with step + ui.card( + ui.card_header("Step Parameter"), + ui.input_slider( + "slider2", "Step size = 10", min=0, max=100, value=50, step=10 + ), + ui.output_text("value2"), + ), + # Range Slider + ui.card( + ui.card_header("Range Slider"), + ui.input_slider( + "slider3", "Select a range", min=0, max=100, value=(30, 70) + ), + ui.output_text("value3"), + ), + # Date Slider + ui.card( + ui.card_header("Date Slider"), + ui.input_slider( + "slider4", + "Select a date", + min=datetime(2023, 1, 1, 0, 0, tzinfo=TIMEZONE), + max=datetime(2023, 12, 31, 0, 0, tzinfo=TIMEZONE), + value=datetime(2023, 6, 15, 12, 30, tzinfo=TIMEZONE), + time_format="%Y-%m-%d", + ), + ui.output_text("value4"), + ), + # Animated Slider + ui.card( + ui.card_header("Animated Slider"), + ui.input_slider( + "slider5", "With animation", min=0, max=100, value=50, animate=True + ), + ui.output_text("value5"), + ), + # Slider with custom formatting + ui.card( + ui.card_header("Custom Formatting"), + ui.input_slider( + "slider6", + "With prefix and suffix", + min=0, + max=100, + value=50, + pre="$", + post="%", + sep=",", + ), + ui.output_text("value6"), + ), + # Slider with ticks + ui.card( + ui.card_header("Ticks Display"), + ui.input_slider( + "slider7", "With tick marks", min=0, max=100, value=50, ticks=True + ), + ui.output_text("value7"), + ), + # Date Range Slider with drag_range + ui.card( + ui.card_header("Date Range"), + ui.input_slider( + "slider9", + "Draggable range", + min=datetime(2023, 1, 1, 0, 0, tzinfo=TIMEZONE), + max=datetime(2023, 12, 31, 0, 0, tzinfo=TIMEZONE), + value=( + datetime(2023, 3, 1, 0, 0, tzinfo=TIMEZONE), + datetime(2023, 9, 30, 0, 0, tzinfo=TIMEZONE), + ), + time_format="%Y-%m-%d", + timezone="UTC", + drag_range=True, + ), + ui.output_text("value9"), + ), + # Datetime slider + ui.card( + ui.card_header("Datetime Slider"), + ui.input_slider( + "slider10", + "With time format", + min=datetime(2023, 1, 1, 0, 0, tzinfo=TIMEZONE), + max=datetime(2023, 12, 31, 23, 59, tzinfo=TIMEZONE), + value=datetime(2023, 6, 15, 12, 30, tzinfo=TIMEZONE), + time_format="%Y-%m-%d %H:%M", + timezone="UTC", + ), + ui.output_text("value10"), + ), + width="400px", + ), +) + + +def server(input, output, session): + @render.text + @reactive.event(input.slider1) + def value1(): + return f"Value: {input.slider1()}" + + @render.text + @reactive.event(input.slider2) + def value2(): + return f"Value: {input.slider2()}" + + @render.text + @reactive.event(input.slider3) + def value3(): + return f"Value: {input.slider3()}" + + @render.text + @reactive.event(input.slider4) + def value4(): + return f"Value: {input.slider4()}" + + @render.text + @reactive.event(input.slider5) + def value5(): + return f"Value: {input.slider5()}" + + @render.text + @reactive.event(input.slider6) + def value6(): + return f"Value: {input.slider6()}" + + @render.text + @reactive.event(input.slider7) + def value7(): + return f"Value: {input.slider7()}" + + @render.text + @reactive.event(input.slider9) + def value9(): + return f"Value: {input.slider9()}" + + @render.text + @reactive.event(input.slider10) + def value10(): + return f"Value: {input.slider10()}" + + +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_slider/app-express.py b/tests/playwright/ai_generated_apps/input_slider/app-express.py new file mode 100644 index 000000000..9bf518a0f --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_slider/app-express.py @@ -0,0 +1,132 @@ +from datetime import datetime +from zoneinfo import ZoneInfo + +from shiny.express import input, render, ui + +# Define a consistent timezone +TIMEZONE = ZoneInfo("UTC") + +ui.page_opts(title="Slider Parameters Demo", full_width=True) + +with ui.layout_column_wrap(width="400px"): + # Numeric Slider - basic parameters + with ui.card(): + ui.card_header("Basic Numeric Slider") + ui.input_slider("slider1", "Min, max, value", min=0, max=100, value=50) + + @render.text + def value1(): + return f"Value: {input.slider1()}" + + # Numeric Slider with step + with ui.card(): + ui.card_header("Step Parameter") + ui.input_slider("slider2", "Step size = 10", min=0, max=100, value=50, step=10) + + @render.text + def value2(): + return f"Value: {input.slider2()}" + + # Range Slider + with ui.card(): + ui.card_header("Range Slider") + ui.input_slider("slider3", "Select a range", min=0, max=100, value=(30, 70)) + + @render.text + def value3(): + return f"Value: {input.slider3()}" + + # Date Slider + with ui.card(): + ui.card_header("Date Slider") + ui.input_slider( + "slider4", + "Select a date", + min=datetime(2023, 1, 1, 0, 0, tzinfo=TIMEZONE), + max=datetime(2023, 12, 31, 0, 0, tzinfo=TIMEZONE), + value=datetime(2023, 6, 15, 12, 30, tzinfo=TIMEZONE), + time_format="%Y-%m-%d", + timezone="UTC", + ) + + @render.text + def value4(): + return f"Value: {input.slider4()}" + + # Animated Slider + with ui.card(): + ui.card_header("Animated Slider") + ui.input_slider( + "slider5", "With animation", min=0, max=100, value=50, animate=True + ) + + @render.text + def value5(): + return f"Value: {input.slider5()}" + + # Slider with custom formatting + with ui.card(): + ui.card_header("Custom Formatting") + ui.input_slider( + "slider6", + "With prefix and suffix", + min=0, + max=100, + value=50, + pre="$", + post="%", + sep=",", + ) + + @render.text + def value6(): + return f"Value: {input.slider6()}" + + # Slider with ticks + with ui.card(): + ui.card_header("Ticks Display") + ui.input_slider( + "slider7", "With tick marks", min=0, max=100, value=50, ticks=True + ) + + @render.text + def value7(): + return f"Value: {input.slider7()}" + + # Date Range Slider with drag_range + with ui.card(): + ui.card_header("Date Range") + ui.input_slider( + "slider9", + "Draggable range", + min=datetime(2023, 1, 1, 0, 0, tzinfo=TIMEZONE), + max=datetime(2023, 12, 31, 0, 0, tzinfo=TIMEZONE), + value=( + datetime(2023, 3, 1, 0, 0, tzinfo=TIMEZONE), + datetime(2023, 9, 30, 0, 0, tzinfo=TIMEZONE), + ), + time_format="%Y-%m-%d", + timezone="UTC", + drag_range=True, + ) + + @render.text + def value9(): + return f"Value: {input.slider9()}" + + # Datetime slider + with ui.card(): + ui.card_header("Datetime Slider") + ui.input_slider( + "slider10", + "With time format", + min=datetime(2023, 1, 1, 0, 0, tzinfo=TIMEZONE), + max=datetime(2023, 12, 31, 23, 59, tzinfo=TIMEZONE), + value=datetime(2023, 6, 15, 12, 30, tzinfo=TIMEZONE), + time_format="%Y-%m-%d %H:%M", + timezone="UTC", + ) + + @render.text + def value10(): + return f"Value: {input.slider10()}" diff --git a/tests/playwright/ai_generated_apps/input_slider/test_input_slider_core_express.py b/tests/playwright/ai_generated_apps/input_slider/test_input_slider_core_express.py new file mode 100644 index 000000000..e39b1ef98 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_slider/test_input_slider_core_express.py @@ -0,0 +1,102 @@ +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_slider_parameters(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test basic numeric slider + slider1 = controller.InputSlider(page, "slider1") + value1 = controller.OutputText(page, "value1") + value1.expect_value("Value: 50") + slider1.expect_label("Min, max, value") + slider1.expect_min("0") + slider1.expect_max("100") + slider1.expect_value("50") + + # Test slider with step + slider2 = controller.InputSlider(page, "slider2") + value2 = controller.OutputText(page, "value2") + value2.expect_value("Value: 50") + slider2.expect_label("Step size = 10") + slider2.expect_min("0") + slider2.expect_max("100") + slider2.expect_value("50") + slider2.expect_step("10") + + # Test range slider + slider3 = controller.InputSliderRange(page, "slider3") + value3 = controller.OutputText(page, "value3") + value3.expect_value("Value: (30, 70)") + slider3.expect_label("Select a range") + slider3.expect_min("0") + slider3.expect_max("100") + slider3.expect_value(("30", "70")) + + # Test date slider + slider4 = controller.InputSlider(page, "slider4") + value4 = controller.OutputText(page, "value4") + value4.expect_value("Value: 2023-06-15 12:30:00") + slider4.expect_label("Select a date") + slider4.expect_min("1672531200000.0") # 2023-01-01 + slider4.expect_max("1703980800000.0") # 2023-12-31 + slider4.expect_value("2023-06-15") + + # Test animated slider + slider5 = controller.InputSlider(page, "slider5") + value5 = controller.OutputText(page, "value5") + value5.expect_value("Value: 50") + slider5.expect_label("With animation") + slider5.expect_min("0") + slider5.expect_max("100") + slider5.expect_value("50") + + # Test formatted slider + slider6 = controller.InputSlider(page, "slider6") + value6 = controller.OutputText(page, "value6") + value6.expect_value("Value: 50") + slider6.expect_label("With prefix and suffix") + slider6.expect_min("0") + slider6.expect_max("100") + slider6.expect_value("$50%") + slider6.expect_pre("$") + slider6.expect_post("%") + slider6.expect_sep(",") + + # Test slider with ticks + slider7 = controller.InputSlider(page, "slider7") + value7 = controller.OutputText(page, "value7") + value7.expect_value("Value: 50") + slider7.expect_label("With tick marks") + slider7.expect_min("0") + slider7.expect_max("100") + slider7.expect_value("50") + slider7.expect_ticks("true") + + # Test date range slider + slider9 = controller.InputSliderRange(page, "slider9") + value9 = controller.OutputText(page, "value9") + value9.expect_value( + "Value: (datetime.datetime(2023, 3, 1, 0, 0), datetime.datetime(2023, 9, 30, 0, 0))" + ) + slider9.expect_label("Draggable range") + slider9.expect_min("1672531200000.0") # 2023-01-01 + slider9.expect_max("1703980800000.0") # 2023-12-31 + slider9.expect_value(("2023-03-01", "2023-09-30")) + slider9.expect_drag_range("true") + + # Test datetime slider + slider10 = controller.InputSlider(page, "slider10") + value10 = controller.OutputText(page, "value10") + value10.expect_value("Value: 2023-06-15 12:30:00") + slider10.expect_label("With time format") + slider10.expect_min("1672531200000.0") # 2023-01-01 00:00 + slider10.expect_max("1704067140000.0") # 2023-12-31 23:59 + slider10.expect_value("2023-06-15 12:30") + slider10.expect_time_format("%Y-%m-%d %H:%M") + slider10.expect_timezone("UTC") diff --git a/tests/playwright/ai_generated_apps/input_switch/app-core.py b/tests/playwright/ai_generated_apps/input_switch/app-core.py new file mode 100644 index 000000000..03bdae2fc --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_switch/app-core.py @@ -0,0 +1,35 @@ +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fluid( + # Create a card to contain the switches + ui.card( + ui.card_header("Switch Demo"), + # Basic switch with default parameters + ui.input_switch( + id="switch1", label="Basic switch (default params)", value=False + ), + # Switch with custom width + ui.input_switch( + id="switch2", label="Switch with custom width", value=True, width="300px" + ), + # Output for switch states + ui.output_text("switch_states"), + full_screen=True, + height="300px", + id="card1", + ) +) + + +# Define the server +def server(input, output, session): + @render.text + def switch_states(): + return ( + f"Switch 1 state: {input.switch1()}\n" f"Switch 2 state: {input.switch2()}" + ) + + +# Create the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_switch/app-express.py b/tests/playwright/ai_generated_apps/input_switch/app-express.py new file mode 100644 index 000000000..2bf875fc4 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_switch/app-express.py @@ -0,0 +1,23 @@ +from shiny.express import input, render, ui + +# Set page title +ui.page_opts(full_width=True) + +# Create a card to contain the switches +with ui.card(full_screen=True, height="300px", id="card1"): + ui.card_header("Switch Demo") + + # Basic switch with default parameters + ui.input_switch(id="switch1", label="Basic switch (default params)", value=False) + + # Switch with custom width + ui.input_switch( + id="switch2", label="Switch with custom width", value=True, width="300px" + ) + + # Display switch states + @render.text + def switch_states(): + return ( + f"Switch 1 state: {input.switch1()}\n" f"Switch 2 state: {input.switch2()}" + ) diff --git a/tests/playwright/ai_generated_apps/input_switch/test_input_switch_core_express.py b/tests/playwright/ai_generated_apps/input_switch/test_input_switch_core_express.py new file mode 100644 index 000000000..2c5ca3531 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_switch/test_input_switch_core_express.py @@ -0,0 +1,36 @@ +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_switch_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test card1 + card1 = controller.Card(page, "card1") + card1.expect_header("Switch Demo") + card1.expect_height("300px") + card1.expect_full_screen_available(True) + + # Test switch1 + switch1 = controller.InputSwitch(page, "switch1") + switch1.expect_label("Basic switch (default params)") + switch1.expect_checked(False) # Test initial value + + # Toggle switch1 and verify + switch1.set(True) + switch1.expect_checked(True) + + # Test switch2 + switch2 = controller.InputSwitch(page, "switch2") + switch2.expect_label("Switch with custom width") + switch2.expect_checked(True) # Test initial value + switch2.expect_width("300px") + + # Toggle switch2 and verify + switch2.set(False) + switch2.expect_checked(False) diff --git a/tests/playwright/ai_generated_apps/input_task_button/app-core.py b/tests/playwright/ai_generated_apps/input_task_button/app-core.py new file mode 100644 index 000000000..c99e646bb --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_task_button/app-core.py @@ -0,0 +1,50 @@ +from shiny import App, reactive, 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", + ) + ), + # Create a card to contain our task button and status + ui.card( + ui.card_header("Task Button Demo"), + # Define the task button + ui.input_task_button( + id="task_btn", + label="Run Task", + icon=ui.tags.i(class_="fa-solid fa-play"), + label_busy="Processing...", + icon_busy=ui.tags.i(class_="fa-solid fa-spinner fa-spin"), + class_="btn-primary m-3", + ), + ui.output_text("task_status"), + height="300px", + ), +) + + +# Define the server +def server(input, output, session): + @output + @render.text + def task_status(): + count = input.task_btn() + if count == 0: + return "Task hasn't started yet" + return f"Task has been run {count} times" + + @reactive.effect + @reactive.event(input.task_btn) + def handle_task(): + import time + + # Simulate a long-running task + time.sleep(2) + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_task_button/app-express.py b/tests/playwright/ai_generated_apps/input_task_button/app-express.py new file mode 100644 index 000000000..90f5c69d5 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_task_button/app-express.py @@ -0,0 +1,43 @@ +from shiny import reactive +from shiny.express import input, render, ui + +# Page options for basic styling +ui.page_opts(fillable=True) + +# Add Font Awesome CSS in the head section +ui.head_content( + ui.HTML( + '' + ) +) + +# Create a card to contain our task button and status +with ui.card(height="300px"): + ui.card_header("Task Button Demo") + + # Define the task button + ui.input_task_button( + id="task_btn", + label="Run Task", + icon=ui.tags.i(class_="fa-solid fa-play"), + label_busy="Processing...", + icon_busy=ui.tags.i(class_="fa-solid fa-spinner fa-spin"), + class_="btn-primary m-3", + ) + + @render.text + def task_status(): + count = input.task_btn() + if count == 0: + return "Task hasn't started yet" + return f"Task has been run {count} times" + + +# Effect to handle the task +@reactive.effect +@reactive.event(input.task_btn) +def handle_task(): + import time + + # Simulate a long-running task + time.sleep(2) diff --git a/tests/playwright/ai_generated_apps/input_task_button/test_input_task_button_core_express.py b/tests/playwright/ai_generated_apps/input_task_button/test_input_task_button_core_express.py new file mode 100644 index 000000000..fee1076bc --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_task_button/test_input_task_button_core_express.py @@ -0,0 +1,35 @@ +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_task_button_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Get the task button controller + task_btn = controller.InputTaskButton(page, "task_btn") + task_status = controller.OutputText(page, "task_status") + + # Test initial state + task_btn.expect_label("Run Task") + task_btn.expect_label_busy("Processing...") + task_btn.expect_state("ready") + task_status.expect_value("Task hasn't started yet") + + # Click the button and test busy state + task_btn.click() + task_btn.expect_state("busy") + + # After task completes, verify new state and status + task_btn.expect_state("ready", timeout=3000) # Allow time for the 2-second sleep + task_status.expect_value("Task has been run 1 times") + + # Test a second click + task_btn.click() + task_btn.expect_state("busy") + task_btn.expect_state("ready", timeout=3000) + task_status.expect_value("Task has been run 2 times") diff --git a/tests/playwright/ai_generated_apps/input_text/app-core.py b/tests/playwright/ai_generated_apps/input_text/app-core.py new file mode 100644 index 000000000..aa58431eb --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_text/app-core.py @@ -0,0 +1,34 @@ +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fillable( + # Create a card to contain the input and output + ui.card( + ui.card_header("Text Input Example"), + # Create text input with all possible parameters + ui.input_text( + id="demo_text", # Required: unique identifier + label="Demo Text Input", # Required: label text + value="Initial value", # Optional: starting value + width="300px", # Optional: CSS width + placeholder="Enter text here", # Optional: hint text + autocomplete="off", # Optional: browser autocomplete behavior + spellcheck="true", # Optional: browser spellcheck behavior + ), + # Add some spacing + ui.hr(), + # Output area for the current value + ui.output_text("current_value"), + ) +) + + +# Define the server function +def server(input, output, session): + @render.text + def current_value(): + return f"Current value: {input.demo_text()}" + + +# Create and return the app object +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_text/app-express.py b/tests/playwright/ai_generated_apps/input_text/app-express.py new file mode 100644 index 000000000..81b04b067 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_text/app-express.py @@ -0,0 +1,27 @@ +from shiny.express import input, render, ui + +# Set page options for a clean layout +ui.page_opts(fillable=True) + +# Create a card to contain the input and output +with ui.card(): + ui.card_header("Text Input Example") + + # Create text input with all possible parameters + ui.input_text( + id="demo_text", # Required: unique identifier + label="Demo Text Input", # Required: label text + value="Initial value", # Optional: starting value + width="300px", # Optional: CSS width + placeholder="Enter text here", # Optional: hint text + autocomplete="off", # Optional: browser autocomplete behavior + spellcheck="true", # Optional: browser spellcheck behavior + ) + + # Add some spacing + ui.hr() + + # Display area for the current value + @render.text + def current_value(): + return f"Current value: {input.demo_text()}" diff --git a/tests/playwright/ai_generated_apps/input_text/test_input_text_core_express.py b/tests/playwright/ai_generated_apps/input_text/test_input_text_core_express.py new file mode 100644 index 000000000..a5d6eb0d9 --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_text/test_input_text_core_express.py @@ -0,0 +1,32 @@ +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_text_input_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test text input + text_input = controller.InputText(page, "demo_text") + text_output = controller.OutputText(page, "current_value") + + # Test initial state + text_input.expect_label("Demo Text Input") + text_input.expect_value("Initial value") + text_input.expect_width("300px") + text_input.expect_placeholder("Enter text here") + text_input.expect_autocomplete("off") + text_input.expect_spellcheck("true") + text_output.expect_value("Current value: Initial value") + + # Test setting new value + text_input.set("New test value") + text_output.expect_value("Current value: New test value") + + # Test empty value + text_input.set("") + text_output.expect_value("Current value: ") diff --git a/tests/playwright/ai_generated_apps/input_text_area/app-core.py b/tests/playwright/ai_generated_apps/input_text_area/app-core.py new file mode 100644 index 000000000..bc190882b --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_text_area/app-core.py @@ -0,0 +1,39 @@ +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fluid( + # Page title is set in the page_fluid + ui.tags.head(ui.tags.title("Text Area Demo")), + # Create a text area with all possible parameters + ui.input_text_area( + id="text_input", + label="Enter your text:", + value="This is some default text.\nIt has multiple lines.\nYou can edit it!", + width="500px", + height="200px", + cols=50, + rows=8, + placeholder="Type something here...", + resize="both", + autoresize=True, + spellcheck="true", + ), + # Add some spacing + ui.br(), + ui.br(), + # Add a header for the output + ui.h4("Output:"), + # Add the output text area + ui.output_text("show_text"), +) + + +# Define the server +def server(input, output, session): + @render.text + def show_text(): + return f"You entered:\n{input.text_input()}" + + +# Create the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/input_text_area/app-express.py b/tests/playwright/ai_generated_apps/input_text_area/app-express.py new file mode 100644 index 000000000..0f41661fd --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_text_area/app-express.py @@ -0,0 +1,34 @@ +from shiny.express import input, render, ui + +ui.page_opts(full_width=True) + +ui.tags.head( + ui.tags.title("Text Area Demo"), +) +# Create a text area with all possible parameters +ui.input_text_area( + id="text_input", + label="Enter your text:", + value="This is some default text.\nIt has multiple lines.\nYou can edit it!", + width="500px", + height="200px", + cols=50, + rows=8, + placeholder="Type something here...", + resize="both", + autoresize=True, + spellcheck="true", +) + +# Add some spacing +ui.br() +ui.br() + +# Add a header for the output +ui.h4("Output:") + + +# Display the input value in a pre-formatted text block +@render.text +def show_text(): + return f"You entered:\n{input.text_input()}" diff --git a/tests/playwright/ai_generated_apps/input_text_area/test_input_text_area_core_express.py b/tests/playwright/ai_generated_apps/input_text_area/test_input_text_area_core_express.py new file mode 100644 index 000000000..00839927d --- /dev/null +++ b/tests/playwright/ai_generated_apps/input_text_area/test_input_text_area_core_express.py @@ -0,0 +1,39 @@ +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_text_area_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Get the text area controller + text_area = controller.InputTextArea(page, "text_input") + + # Test initial state + text_area.expect_label("Enter your text:") + text_area.expect_value( + "This is some default text.\nIt has multiple lines.\nYou can edit it!" + ) + text_area.expect_width("500px") + text_area.expect_height("194px") # 200px - padding + text_area.expect_cols("50") + text_area.expect_rows("8") + text_area.expect_placeholder("Type something here...") + text_area.expect_resize("both") + text_area.expect_autoresize(True) + text_area.expect_spellcheck("true") + + # Test output text controller + output_text = controller.OutputText(page, "show_text") + output_text.expect_value( + "You entered:\nThis is some default text.\nIt has multiple lines.\nYou can edit it!" + ) + + # Test setting new value + new_text = "This is new text\nWith new lines" + text_area.set(new_text) + output_text.expect_value(f"You entered:\n{new_text}") diff --git a/tests/playwright/ai_generated_apps/navset_pill/app-core.py b/tests/playwright/ai_generated_apps/navset_pill/app-core.py new file mode 100644 index 000000000..8da59279e --- /dev/null +++ b/tests/playwright/ai_generated_apps/navset_pill/app-core.py @@ -0,0 +1,55 @@ +from shiny import App, render, ui + +# Define the UI +app_ui = ui.page_fillable( + # Create a navset_pill with all possible parameters + ui.navset_pill( + # Panel A + ui.nav_panel( + "A", + "This is content for Panel A", + ui.input_slider("n1", "N1", min=0, max=100, value=20), + ui.output_text("panel_a_text"), + ), + # Panel B + ui.nav_panel( + "B", + "This is content for Panel B", + ui.input_numeric("n2", "N2", value=10), + ui.output_text("panel_b_text"), + ), + # Panel C + ui.nav_panel( + "C", + "This is content for Panel C", + ui.input_text("txt", "Enter text", "Hello"), + ui.output_text("panel_c_text"), + ), + id="pills", + ), + # Show which panel is currently selected + ui.output_text("selected_panel"), +) + + +# Define the server +def server(input, output, session): + @render.text + def panel_a_text(): + return f"Value of n1: {input.n1()}" + + @render.text + def panel_b_text(): + return f"Value of n2: {input.n2()}" + + @render.text + def panel_c_text(): + return f"You entered: {input.txt()}" + + @render.text + def selected_panel(): + return f"Currently selected panel: {input.pills()}" + + +# Create the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/navset_pill/app-express.py b/tests/playwright/ai_generated_apps/navset_pill/app-express.py new file mode 100644 index 000000000..8ef6d6796 --- /dev/null +++ b/tests/playwright/ai_generated_apps/navset_pill/app-express.py @@ -0,0 +1,39 @@ +from shiny.express import input, render, ui + +# Page options for the app +ui.page_opts(fillable=True) + +# Create a navset_pill with all possible parameters +with ui.navset_pill(id="pills"): + # Panel A + with ui.nav_panel("A"): + "This is content for Panel A" + ui.input_slider("n1", "N1", min=0, max=100, value=20) + + @render.text + def panel_a_text(): + return f"Value of n1: {input.n1()}" + + # Panel B + with ui.nav_panel("B"): + "This is content for Panel B" + ui.input_numeric("n2", "N2", value=10) + + @render.text + def panel_b_text(): + return f"Value of n2: {input.n2()}" + + # Panel C + with ui.nav_panel("C"): + "This is content for Panel C" + ui.input_text("txt", "Enter text", "Hello") + + @render.text + def panel_c_text(): + return f"You entered: {input.txt()}" + + +# Show which panel is currently selected +@render.text +def selected_panel(): + return f"Currently selected panel: {input.pills()}" diff --git a/tests/playwright/ai_generated_apps/navset_pill/test_navset_pill_core_express.py b/tests/playwright/ai_generated_apps/navset_pill/test_navset_pill_core_express.py new file mode 100644 index 000000000..d080ca743 --- /dev/null +++ b/tests/playwright/ai_generated_apps/navset_pill/test_navset_pill_core_express.py @@ -0,0 +1,67 @@ +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_navset_pill_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test navset pill + navset = controller.NavsetPill(page, "pills") + selected_panel_text = controller.OutputText(page, "selected_panel") + + # Test initial state + navset.expect_nav_titles(["A", "B", "C"]) + navset.expect_nav_values(["A", "B", "C"]) + navset.expect_value("A") # First panel should be selected by default + selected_panel_text.expect_value("Currently selected panel: A") + + # Test Panel A components + slider = controller.InputSlider(page, "n1") + panel_a_text = controller.OutputText(page, "panel_a_text") + + slider.expect_label("N1") + slider.expect_min("0") + slider.expect_max("100") + slider.expect_value("20") + panel_a_text.expect_value("Value of n1: 20") + + # Change slider value and verify + slider.set("50") + panel_a_text.expect_value("Value of n1: 50") + + # Switch to Panel B and test its components + navset.set("B") + navset.expect_value("B") + selected_panel_text.expect_value("Currently selected panel: B") + + numeric = controller.InputNumeric(page, "n2") + panel_b_text = controller.OutputText(page, "panel_b_text") + + numeric.expect_label("N2") + numeric.expect_value("10") + panel_b_text.expect_value("Value of n2: 10") + + # Change numeric value and verify + numeric.set("25") + panel_b_text.expect_value("Value of n2: 25") + + # Switch to Panel C and test its components + navset.set("C") + navset.expect_value("C") + selected_panel_text.expect_value("Currently selected panel: C") + + text_input = controller.InputText(page, "txt") + panel_c_text = controller.OutputText(page, "panel_c_text") + + text_input.expect_label("Enter text") + text_input.expect_value("Hello") + panel_c_text.expect_value("You entered: Hello") + + # Change text value and verify + text_input.set("Testing") + panel_c_text.expect_value("You entered: Testing") diff --git a/tests/playwright/ai_generated_apps/update_text/app-core.py b/tests/playwright/ai_generated_apps/update_text/app-core.py new file mode 100644 index 000000000..0a2ce7ffe --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_text/app-core.py @@ -0,0 +1,84 @@ +from shiny import App, reactive, render, ui + +# Define the UI +app_ui = ui.page_fillable( + # Layout with two columns + ui.layout_column_wrap( + # First card with text input + ui.card( + ui.card_header("Text Input Demo"), + ui.input_text( + "txt", + "Original Text", + value="Initial value", + placeholder="Type something...", + ), + ui.output_text("current_value"), + ), + # Second card with control buttons + ui.card( + ui.card_header("Control Buttons"), + ui.layout_column_wrap( + ui.input_action_button( + "update_all", "Update All Parameters", class_="btn-primary mb-3" + ), + ui.input_action_button( + "update_label", "Update Label Only", class_="btn-secondary mb-3" + ), + ui.input_action_button( + "update_value", "Update Value Only", class_="btn-success mb-3" + ), + ui.input_action_button( + "update_placeholder", + "Update Placeholder Only", + class_="btn-info mb-3", + ), + ), + ), + width=1 / 2, + ), +) + + +# Define the server +def server(input, output, session): + # Render current value text + @render.text + def current_value(): + return f"Current value: {input.txt()}" + + # Effect for updating all parameters + @reactive.effect + @reactive.event(input.update_all) + def _(): + ui.update_text( + id="txt", + label="Updated Label", + value="Updated Value", + placeholder="Updated Placeholder", + ) + + # Effect for updating label only + @reactive.effect + @reactive.event(input.update_label) + def _(): + ui.update_text(id="txt", label=f"Label Updated {input.update_label()} times") + + # Effect for updating value only + @reactive.effect + @reactive.event(input.update_value) + def _(): + ui.update_text(id="txt", value=f"Value Updated {input.update_value()} times") + + # Effect for updating placeholder only + @reactive.effect + @reactive.event(input.update_placeholder) + def _(): + ui.update_text( + id="txt", + placeholder=f"Placeholder Updated {input.update_placeholder()} times", + ) + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/update_text/app-express.py b/tests/playwright/ai_generated_apps/update_text/app-express.py new file mode 100644 index 000000000..7e2e6eb01 --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_text/app-express.py @@ -0,0 +1,69 @@ +from shiny import reactive +from shiny.express import input, render, ui + +# Page title +ui.page_opts(fillable=True) + +with ui.layout_column_wrap(width=1 / 2): + # Input text that will be updated + with ui.card(): + ui.card_header("Text Input Demo") + ui.input_text( + "txt", + "Original Text", + value="Initial value", + placeholder="Type something...", + ) + + @render.text + def current_value(): + return f"Current value: {input.txt()}" + + # Buttons to trigger different update scenarios + with ui.card(): + ui.card_header("Control Buttons") + with ui.layout_column_wrap(): + ui.input_action_button( + "update_all", "Update All Parameters", class_="btn-primary mb-3" + ) + ui.input_action_button( + "update_label", "Update Label Only", class_="btn-secondary mb-3" + ) + ui.input_action_button( + "update_value", "Update Value Only", class_="btn-success mb-3" + ) + ui.input_action_button( + "update_placeholder", "Update Placeholder Only", class_="btn-info mb-3" + ) + + +# Effects to handle different update scenarios +@reactive.effect +@reactive.event(input.update_all) +def _(): + ui.update_text( + id="txt", + label="Updated Label", + value="Updated Value", + placeholder="Updated Placeholder", + ) + + +@reactive.effect +@reactive.event(input.update_label) +def _(): + ui.update_text(id="txt", label=f"Label Updated {input.update_label()} times") + + +@reactive.effect +@reactive.event(input.update_value) +def _(): + ui.update_text(id="txt", value=f"Value Updated {input.update_value()} times") + + +@reactive.effect +@reactive.event(input.update_placeholder) +def _(): + ui.update_text( + id="txt", placeholder=f"Placeholder Updated {input.update_placeholder()} times" + ) diff --git a/tests/playwright/ai_generated_apps/update_text/test_update_text_core_express.py b/tests/playwright/ai_generated_apps/update_text/test_update_text_core_express.py new file mode 100644 index 000000000..9b6d1526e --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_text/test_update_text_core_express.py @@ -0,0 +1,64 @@ +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_update_text(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Get controllers for the components with IDs + text_input = controller.InputText(page, "txt") + text_output = controller.OutputText(page, "current_value") + update_all_btn = controller.InputActionButton(page, "update_all") + update_label_btn = controller.InputActionButton(page, "update_label") + update_value_btn = controller.InputActionButton(page, "update_value") + update_placeholder_btn = controller.InputActionButton(page, "update_placeholder") + + # Test initial state + text_input.expect_label("Original Text") + text_input.expect_value("Initial value") + text_input.expect_placeholder("Type something...") + text_output.expect_value("Current value: Initial value") + + # Test update all parameters + update_all_btn.click() + text_input.expect_label("Updated Label") + text_input.expect_value("Updated Value") + text_input.expect_placeholder("Updated Placeholder") + text_output.expect_value("Current value: Updated Value") + + # Test update label only + update_label_btn.click() + text_input.expect_label("Label Updated 1 times") + text_output.expect_value( + "Current value: Updated Value" + ) # Value should remain unchanged + + # Test update value only + update_value_btn.click() + text_input.expect_value("Value Updated 1 times") + text_output.expect_value("Current value: Value Updated 1 times") + text_input.expect_label("Label Updated 1 times") # Label should remain unchanged + + # Test update placeholder only + update_placeholder_btn.click() + text_input.expect_placeholder("Placeholder Updated 1 times") + text_output.expect_value( + "Current value: Value Updated 1 times" + ) # Value should remain unchanged + + # Test manual text input + text_input.set("User typed value") + text_output.expect_value("Current value: User typed value") + + # Test multiple updates + update_label_btn.click() + text_input.expect_label("Label Updated 2 times") + update_value_btn.click() + text_input.expect_value("Value Updated 2 times") + update_placeholder_btn.click() + text_input.expect_placeholder("Placeholder Updated 2 times") diff --git a/tests/playwright/ai_generated_apps/update_text_area/app-core.py b/tests/playwright/ai_generated_apps/update_text_area/app-core.py new file mode 100644 index 000000000..edecea96d --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_text_area/app-core.py @@ -0,0 +1,59 @@ +from shiny import App, reactive, render, ui + +app_ui = ui.page_fillable( + # Set page title in the UI + ui.panel_title("Text Area Update Demo"), + # Initial text area with a card wrapper + ui.card( + ui.card_header("Text Area Demo"), + ui.input_text_area( + id="textarea", + label="Sample Text Area", + value="Initial text", + placeholder="Enter your text here", + rows=5, + height="200px", + ), + ), + # Controls for updating text area in a separate card + ui.card( + ui.card_header("Control Panel"), + ui.layout_column_wrap( + ui.input_text("new_label", "New Label", value="Updated Label"), + ui.input_text("new_value", "New Value", value="Updated text content"), + ui.input_text( + "new_placeholder", "New Placeholder", value="Updated placeholder text" + ), + ui.input_action_button("update", "Update Text Area", class_="btn-primary"), + width=1 / 2, + ), + ), + # Display current values for verification + ui.card(ui.card_header("Current Values"), ui.output_text("show_values")), + fillable_mobile=True, +) + + +def server(input, output, session): + # Display current values + @render.text + def show_values(): + return f""" + Current Label: {input.new_label()} + Current Value: {input.new_value()} + Current Placeholder: {input.new_placeholder()} + """ + + # Effect to update the text area when the button is clicked + @reactive.effect + @reactive.event(input.update) + def _(): + ui.update_text_area( + id="textarea", + label=input.new_label(), + value=input.new_value(), + placeholder=input.new_placeholder(), + ) + + +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/update_text_area/app-express.py b/tests/playwright/ai_generated_apps/update_text_area/app-express.py new file mode 100644 index 000000000..4db5c2375 --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_text_area/app-express.py @@ -0,0 +1,51 @@ +from shiny import reactive +from shiny.express import input, render, ui + +ui.page_opts(title="Text Area Update Demo", fillable=True) + +# Initial text area with a card wrapper for better visual organization +with ui.card(): + ui.card_header("Text Area Demo") + ui.input_text_area( + id="textarea", + label="Sample Text Area", + value="Initial text", + placeholder="Enter your text here", + rows=5, + height="200px", + ) + +# Controls for updating text area in a separate card +with ui.card(): + ui.card_header("Control Panel") + with ui.layout_column_wrap(width=1 / 2): + ui.input_text("new_label", "New Label", value="Updated Label") + ui.input_text("new_value", "New Value", value="Updated text content") + ui.input_text( + "new_placeholder", "New Placeholder", value="Updated placeholder text" + ) + ui.input_action_button("update", "Update Text Area", class_="btn-primary") + +# Display current values for verification +with ui.card(): + ui.card_header("Current Values") + + @render.text + def show_values(): + return f""" + Current Label: {input.new_label()} + Current Value: {input.new_value()} + Current Placeholder: {input.new_placeholder()} + """ + + +# Effect to update the text area when the button is clicked +@reactive.effect +@reactive.event(input.update) +def _(): + ui.update_text_area( + id="textarea", + label=input.new_label(), + value=input.new_value(), + placeholder=input.new_placeholder(), + ) diff --git a/tests/playwright/ai_generated_apps/update_text_area/test_update_text_area_core_express.py b/tests/playwright/ai_generated_apps/update_text_area/test_update_text_area_core_express.py new file mode 100644 index 000000000..292d6d37c --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_text_area/test_update_text_area_core_express.py @@ -0,0 +1,65 @@ +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_update_text_area(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Get the text area controller + text_area = controller.InputTextArea(page, "textarea") + + new_label = controller.InputText(page, "new_label") + new_value = controller.InputText(page, "new_value") + new_placeholder = controller.InputText(page, "new_placeholder") + + show_values = controller.OutputText(page, "show_values") + show_values.expect_value( + """ + Current Label: Updated Label + Current Value: Updated text content + Current Placeholder: Updated placeholder text + """ + ) + + # Update text are button + update_button = controller.InputActionButton(page, "update") + + # Test initial state + text_area.expect_label("Sample Text Area") + text_area.expect_value("Initial text") + text_area.expect_placeholder("Enter your text here") + + new_label.expect_label("New Label") + new_label.expect_value("Updated Label") + + new_placeholder.expect_label("New Placeholder") + new_placeholder.expect_value("Updated placeholder text") + + new_value.expect_label("New Value") + new_value.expect_value("Updated text content") + + # Update the text area + new_label.set("New Label") + new_value.set("New text content") + new_placeholder.set("New placeholder text") + update_button.click() + text_area.expect_label("New Label") + text_area.expect_value("New text content") + text_area.expect_placeholder("New placeholder text") + + # Check the output + show_values.expect_value( + """ + Current Label: New Label + Current Value: New text content + Current Placeholder: New placeholder text + """ + ) + text_area.expect_label("New Label") + text_area.expect_value("New text content") + text_area.expect_placeholder("New placeholder text") diff --git a/tests/playwright/ai_generated_apps/update_tooltip/app-core.py b/tests/playwright/ai_generated_apps/update_tooltip/app-core.py new file mode 100644 index 000000000..3f2eece04 --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_tooltip/app-core.py @@ -0,0 +1,92 @@ +from shiny import App, reactive, ui + +# Define the UI +app_ui = ui.page_fillable( + # Add Font Awesome for icons + ui.tags.head( + ui.tags.link( + rel="stylesheet", + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css", + ), + ), + # Create a container card + ui.card( + ui.card_header("Tooltip Demo"), + # Control buttons in a row + ui.layout_column_wrap( + ui.input_action_button( + "btn_show", + "Show tooltip", + class_="btn btn-info", + icon=ui.tags.i(class_="fa-solid fa-eye"), + ), + ui.input_action_button( + "btn_close", + "Close tooltip", + class_="btn btn-warning", + icon=ui.tags.i(class_="fa-solid fa-eye-slash"), + ), + ui.input_action_button( + "btn_update", + "Update tooltip", + class_="btn btn-success", + icon=ui.tags.i(class_="fa-solid fa-sync"), + ), + width=1 / 3, + ), + ui.hr(), + # Center the tooltip button + ui.div( + ui.tooltip( + ui.input_action_button( + "btn_w_tooltip", + "Hover over me!", + class_="btn btn-primary btn-lg", + icon=ui.tags.i(class_="fa-solid fa-info-circle"), + ), + "Initial tooltip message - try the buttons above!", + id="tooltip_id", + placement="right", + ), + class_="d-flex justify-content-center align-items-center", + style="height: 200px;", + ), + ), +) + + +# Define the server +def server(input, output, session): + # Effect to show tooltip + @reactive.effect + @reactive.event(input.btn_show) + def _(): + ui.update_tooltip("tooltip_id", show=True) + + # Effect to close tooltip + @reactive.effect + @reactive.event(input.btn_close) + def _(): + ui.update_tooltip("tooltip_id", show=False) + + # Effect to update tooltip content and show it + @reactive.effect + @reactive.event(input.btn_update) + def _(): + # Create dynamic content based on number of clicks + count = input.btn_update() + content = f"Tooltip updated {count} time{'s' if count > 1 else ''}!" + # Update tooltip with new content and show it + ui.update_tooltip("tooltip_id", content, show=True) + + # Show notification when button with tooltip is clicked + @reactive.effect + @reactive.event(input.btn_w_tooltip) + def _(): + ui.notification_show( + "Button clicked!", duration=2, type="message", close_button=True + ) + + +# Create and return the app +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/update_tooltip/app-express.py b/tests/playwright/ai_generated_apps/update_tooltip/app-express.py new file mode 100644 index 000000000..73a55f3e5 --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_tooltip/app-express.py @@ -0,0 +1,95 @@ +from shiny import reactive +from shiny.express import input, ui + +# Page options +ui.page_opts( + title="Update Tooltip Demo", + fillable=True, + # Add Font Awesome for icons +) + +# Add Font Awesome CSS in the head section first +ui.head_content( + ui.tags.link( + rel="stylesheet", + href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css", + ) +) + +# Create a container card for better organization +with ui.card(): + ui.card_header("Tooltip Demo") + + # Control buttons in a row + with ui.layout_column_wrap(width=1 / 3): + ui.input_action_button( + "btn_show", + "Show tooltip", + class_="btn btn-info", + icon=ui.tags.i(class_="fa-solid fa-eye"), + ) + + ui.input_action_button( + "btn_close", + "Close tooltip", + class_="btn btn-warning", + icon=ui.tags.i(class_="fa-solid fa-eye-slash"), + ) + + ui.input_action_button( + "btn_update", + "Update tooltip", + class_="btn btn-success", + icon=ui.tags.i(class_="fa-solid fa-sync"), + ) + + # Spacer + ui.hr() + + # Center the tooltip button + with ui.div( + class_="d-flex justify-content-center align-items-center", + style="height: 200px;", + ): + with ui.tooltip(id="tooltip_id", placement="right"): + ui.input_action_button( + "btn_w_tooltip", + "Hover over me!", + class_="btn btn-primary btn-lg", + icon=ui.tags.i(class_="fa-solid fa-info-circle"), + ) + "Initial tooltip message - try the buttons above!" + + +# Effect to show tooltip +@reactive.effect +@reactive.event(input.btn_show) +def _(): + ui.update_tooltip("tooltip_id", show=True) + + +# Effect to close tooltip +@reactive.effect +@reactive.event(input.btn_close) +def _(): + ui.update_tooltip("tooltip_id", show=False) + + +# Effect to update tooltip content and show it +@reactive.effect +@reactive.event(input.btn_update) +def _(): + # Create dynamic content based on number of clicks + count = input.btn_update() + content = f"Tooltip updated {count} time{'s' if count > 1 else ''}!" + # Update tooltip with new content and show it + ui.update_tooltip("tooltip_id", content, show=True) + + +# Show notification when button with tooltip is clicked +@reactive.effect +@reactive.event(input.btn_w_tooltip) +def _(): + ui.notification_show( + "Button clicked!", duration=2, type="message", close_button=True + ) diff --git a/tests/playwright/ai_generated_apps/update_tooltip/test_update_tooltip_core_express.py b/tests/playwright/ai_generated_apps/update_tooltip/test_update_tooltip_core_express.py new file mode 100644 index 000000000..1202a7e2b --- /dev/null +++ b/tests/playwright/ai_generated_apps/update_tooltip/test_update_tooltip_core_express.py @@ -0,0 +1,52 @@ +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_tooltip_demo(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Get tooltip controller + tooltip = controller.Tooltip(page, "tooltip_id") + + # Get button controllers + show_btn = controller.InputActionButton(page, "btn_show") + close_btn = controller.InputActionButton(page, "btn_close") + update_btn = controller.InputActionButton(page, "btn_update") + tooltip_btn = controller.InputActionButton(page, "btn_w_tooltip") + + # Test initial button labels + show_btn.expect_label("Show tooltip") + close_btn.expect_label("Close tooltip") + update_btn.expect_label("Update tooltip") + tooltip_btn.expect_label("Hover over me!") + + # Test initial tooltip state and content + tooltip.expect_active(False) + show_btn.click() + tooltip.expect_body("Initial tooltip message - try the buttons above!") + tooltip.expect_placement("right") + + # Test showing tooltip + tooltip.expect_active(True) + + # Test closing tooltip + close_btn.click() + tooltip.expect_active(False) + + # Test updating tooltip content + update_btn.click() + tooltip.expect_active(True) + tooltip.expect_body("Tooltip updated 1 time!") + + close_btn.click() + tooltip.expect_active(False) + + # Click update button again and verify content changes + update_btn.click() + tooltip.expect_active(True) + tooltip.expect_body("Tooltip updated 2 times!") diff --git a/tests/playwright/ai_generated_apps/value_box/app-core.py b/tests/playwright/ai_generated_apps/value_box/app-core.py new file mode 100644 index 000000000..6cc4ce28c --- /dev/null +++ b/tests/playwright/ai_generated_apps/value_box/app-core.py @@ -0,0 +1,120 @@ +import pandas as pd +import plotly.graph_objects as go +import shinywidgets as sw + +from shiny import App, ui + +# Create a custom icon for the showcase using Font Awesome +chart_icon = ui.tags.i(class_="fa-solid fa-chart-simple", style="font-size: 8rem;") +chart_thumbs_up_icon = ui.tags.i( + class_="fa-solid fa-thumbs-up", style="font-size: 5rem;" +) +chart_star_icon = ui.tags.i(class_="fa-solid fa-star", style="font-size: 5rem;") +chart_heart_icon = ui.tags.i(class_="fa-solid fa-heart", style="font-size: 5rem;") +chart_lightbulb_icon = ui.tags.i( + class_="fa-solid fa-lightbulb", style="font-size: 10rem;" +) + +data = pd.DataFrame( + {"Year": range(2018, 2024), "Revenue": [100, 120, 110, 122, 118, 130]} +) + +app_ui = ui.page_fluid( + # Add Font Awesome CSS to the app + ui.head_content( + ui.HTML( + '' + ) + ), + ui.br(), + ui.layout_column_wrap( + # 1. Basic value box with left-center showcase layout (default) + ui.value_box( + "Revenue", + "$5.2M", + "Up 12% from last month", + id="left_center_value_box", + showcase=chart_icon, + theme="primary", + ), + # 2. Value box with top-right showcase layout + ui.value_box( + "Active Users", + "2.4K", + "Daily active users", + id="top_right_value_box", + showcase=chart_thumbs_up_icon, + showcase_layout="top right", + theme="bg-gradient-purple-red", + height="200px", + ), + # 3. Value box with bottom showcase layout + ui.value_box( + "Conversion Rate", + "3.8%", + "Increased by 0.5%", + id="bottom_value_box", + showcase=sw.output_widget("graph"), + showcase_layout="bottom", + theme="text-success", + height="200px", + ), + # 4. Value box with full screen capability and custom theme + ui.value_box( + "Total Sales", + "8,742", + "Year to date performance", + id="full_screen_value_box", + showcase=chart_lightbulb_icon, + full_screen=True, + theme="bg-gradient-orange-red", + height="600px", + min_height="150px", + max_height="300px", + fill=True, + ), + # 5. Value box with custom background color using class_ + ui.value_box( + "Pending Orders", + "156", + "Requires attention", + id="custom_bg_value_box", + showcase=chart_heart_icon, + theme=None, + height="200px", + class_="bg-warning text-dark", + ), + width="400px", + ), +) + + +def server(input, output, session): + @sw.render_plotly + def graph(): + fig = go.Figure() + + # Add line trace + fig.add_trace( + go.Scatter( + x=data["Year"], + y=data["Revenue"], + mode="lines+markers", + line=dict(color="blue", width=2), + marker=dict(size=4), + ) + ) + + # Update layout for sparkline appearance + fig.update_layout( + showlegend=False, + paper_bgcolor="mistyrose", + plot_bgcolor="rgba(0,0,0,0)", + xaxis=dict(showgrid=False, zeroline=False), + yaxis=dict(showgrid=False, zeroline=False), + ) + + return fig + + +app = App(app_ui, server) diff --git a/tests/playwright/ai_generated_apps/value_box/app-express.py b/tests/playwright/ai_generated_apps/value_box/app-express.py new file mode 100644 index 000000000..5f26ed9ca --- /dev/null +++ b/tests/playwright/ai_generated_apps/value_box/app-express.py @@ -0,0 +1,115 @@ +import pandas as pd +import plotly.graph_objects as go +import shinywidgets as sw + +from shiny.express import ui + +ui.page_opts(full_width=True) + +# Create a custom icon for the showcase using Font Awesome +chart_icon = ui.tags.i(class_="fa-solid fa-chart-simple", style="font-size: 8rem;") +chart_thumbs_up_icon = ui.tags.i( + class_="fa-solid fa-thumbs-up", style="font-size: 5rem;" +) +chart_star_icon = ui.tags.i(class_="fa-solid fa-star", style="font-size: 5rem;") +chart_heart_icon = ui.tags.i(class_="fa-solid fa-heart", style="font-size: 5rem;") +chart_lightbulb_icon = ui.tags.i( + class_="fa-solid fa-lightbulb", style="font-size: 10rem;" +) + +data = pd.DataFrame( + {"Year": range(2018, 2024), "Revenue": [100, 120, 110, 122, 118, 130]} +) + +# Add Font Awesome CSS to the app +ui.head_content( + ui.HTML( + '' + ) +) +ui.br() + +with ui.layout_column_wrap(width="400px"): + # 1. Basic value box with left-center showcase layout (default) + with ui.value_box(id="left_center_value_box", showcase=chart_icon, theme="primary"): + "Revenue" + "$5.2M" + "Up 12% from last month" + + # 2. Value box with top-right showcase layout + with ui.value_box( + id="top_right_value_box", + showcase=chart_thumbs_up_icon, + showcase_layout="top right", + theme="bg-gradient-purple-red", + height="200px", + ): + "Active Users" + "2.4K" + "Daily active users" + + # 3. Value box with bottom showcase layout + with ui.value_box( + id="bottom_value_box", + showcase=sw.output_widget("graph"), + showcase_layout="bottom", + theme="text-success", + height="200px", + ): + "Conversion Rate" + "3.8%" + "Increased by 0.5%" + with ui.hold(): + + @sw.render_plotly + def graph(): + fig = go.Figure() + + # Add line trace + fig.add_trace( + go.Scatter( + x=data["Year"], + y=data["Revenue"], + mode="lines+markers", + line=dict(color="blue", width=2), + marker=dict(size=4), + ) + ) + + # Update layout for sparkline appearance + fig.update_layout( + showlegend=False, + paper_bgcolor="mistyrose", + plot_bgcolor="rgba(0,0,0,0)", + xaxis=dict(showgrid=False, zeroline=False), + yaxis=dict(showgrid=False, zeroline=False), + ) + + return fig + + # 4. Value box with full screen capability and custom theme + with ui.value_box( + id="full_screen_value_box", + showcase=chart_lightbulb_icon, + full_screen=True, + theme="bg-gradient-orange-red", + height="600px", + min_height="150px", + max_height="300px", + fill=True, + ): + "Total Sales" + "8,742" + "Year to date performance" + + # 5. Value box with custom background color using class_ + with ui.value_box( + id="custom_bg_value_box", + showcase=chart_heart_icon, + theme=None, + height="200px", + class_="bg-warning text-dark", + ): + "Pending Orders" + "156" + "Requires attention" diff --git a/tests/playwright/ai_generated_apps/value_box/test_value_box_core_express.py b/tests/playwright/ai_generated_apps/value_box/test_value_box_core_express.py new file mode 100644 index 000000000..37ab0b907 --- /dev/null +++ b/tests/playwright/ai_generated_apps/value_box/test_value_box_core_express.py @@ -0,0 +1,41 @@ +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_value_boxes(page: Page, app: ShinyAppProc) -> None: + page.goto(app.url) + + # Test left-center value box + left_center_box = controller.ValueBox(page, "left_center_value_box") + left_center_box.expect_title("Revenue") + left_center_box.expect_value("$5.2M") + + # Test top-right value box + top_right_box = controller.ValueBox(page, "top_right_value_box") + top_right_box.expect_title("Active Users") + top_right_box.expect_value("2.4K") + top_right_box.expect_height("200px") + + # Test bottom value box + bottom_box = controller.ValueBox(page, "bottom_value_box") + bottom_box.expect_title("Conversion Rate") + bottom_box.expect_value("3.8%") + bottom_box.expect_height("200px") + + # Test full screen value box + full_screen_box = controller.ValueBox(page, "full_screen_value_box") + full_screen_box.expect_title("Total Sales") + full_screen_box.expect_value("8,742") + full_screen_box.expect_height("600px") + full_screen_box.expect_full_screen_available(True) + + # Test custom background value box + custom_bg_box = controller.ValueBox(page, "custom_bg_value_box") + custom_bg_box.expect_title("Pending Orders") + custom_bg_box.expect_value("156") + custom_bg_box.expect_height("200px")