Skip to content

Commit 80018f2

Browse files
committed
Add download button and link kitchen sink tests
Introduces comprehensive kitchen sink apps and Playwright tests for both download_button and download_link Shiny components.
1 parent b6a3b11 commit 80018f2

File tree

5 files changed

+285
-2
lines changed

5 files changed

+285
-2
lines changed

shiny/playwright/controller/_file.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from __future__ import annotations
22

33
from playwright.sync_api import Page
4+
from playwright.sync_api import expect as playwright_expect
45

5-
from ._base import InputActionBase, WidthLocM
6+
from .._types import PatternOrStr, Timeout
7+
from ._base import InputActionBase, WidthLocStlyeM
68

79

8-
class _DownloadMixin(WidthLocM, InputActionBase):
10+
class _DownloadMixin(WidthLocStlyeM, InputActionBase):
911
"""Mixin for download controls."""
1012

1113
def __init__(self, page: Page, id: str, *, loc_suffix: str) -> None:
@@ -15,6 +17,16 @@ def __init__(self, page: Page, id: str, *, loc_suffix: str) -> None:
1517
loc=f"#{id}.shiny-download-link{loc_suffix}",
1618
)
1719

20+
def expect_label(
21+
self,
22+
value: PatternOrStr,
23+
*,
24+
timeout: Timeout = None,
25+
) -> None:
26+
"""Expect the anchor itself to contain the provided label text."""
27+
28+
playwright_expect(self.loc).to_have_text(value, timeout=timeout)
29+
1830

1931
class DownloadLink(_DownloadMixin):
2032
"""
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
from __future__ import annotations
2+
3+
import faicons
4+
5+
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
6+
7+
app_ui = ui.page_fluid(
8+
ui.h2("Download button kitchen sink"),
9+
ui.layout_columns(
10+
ui.card(
11+
ui.h6("Click the button below to increment the counter"),
12+
ui.input_action_button("increment", "Add to counter", class_="mb-2"),
13+
),
14+
ui.card(
15+
ui.h4("Buttons"),
16+
ui.download_button(
17+
"plain_csv",
18+
"Plain CSV",
19+
),
20+
ui.download_button(
21+
"styled_csv",
22+
"Styled CSV",
23+
icon=faicons.icon_svg("file-csv"),
24+
width="560px",
25+
),
26+
),
27+
),
28+
)
29+
30+
31+
def server(input: Inputs, output: Outputs, session: Session) -> None:
32+
inventory_total = 0
33+
plain_total = 0
34+
styled_total = 0
35+
36+
@reactive.effect
37+
@reactive.event(input.increment)
38+
def _() -> None:
39+
nonlocal inventory_total
40+
inventory_total += 1
41+
42+
@render.download(filename=lambda: f"plain-{plain_total + 1}.csv")
43+
async def plain_csv():
44+
nonlocal plain_total
45+
plain_total += 1
46+
current_plain = plain_total
47+
current_inventory = inventory_total
48+
yield "kind,inventory,count\n"
49+
yield f"plain,{current_inventory},{current_plain}\n"
50+
51+
@render.download(
52+
filename=lambda: f"styled-{inventory_total}-{styled_total + 1}.csv"
53+
)
54+
async def styled_csv():
55+
nonlocal styled_total
56+
styled_total += 1
57+
current_styled = styled_total
58+
current_inventory = inventory_total
59+
yield "metric,value\n"
60+
yield f"inventory,{current_inventory}\n"
61+
yield f"download_number,{current_styled}\n"
62+
63+
64+
app = App(app_ui, server)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from playwright.sync_api import Page
6+
7+
from shiny.playwright import controller
8+
from shiny.run import ShinyAppProc
9+
10+
11+
def test_download_button_kitchensink(page: Page, local_app: ShinyAppProc) -> None:
12+
page.goto(local_app.url)
13+
14+
increment = controller.InputActionButton(page, "increment")
15+
plain_button = controller.DownloadButton(page, "plain_csv")
16+
styled_button = controller.DownloadButton(page, "styled_csv")
17+
plain_button.expect_label("Plain CSV")
18+
styled_button.expect_label("Styled CSV")
19+
styled_button.expect_width("560px")
20+
21+
with page.expect_download() as plain_info:
22+
plain_button.click()
23+
plain_download = plain_info.value
24+
plain_path_str = plain_download.path()
25+
assert plain_download.suggested_filename == "plain-1.csv"
26+
assert plain_path_str is not None
27+
assert Path(plain_path_str).read_text() == "kind,inventory,count\nplain,0,1\n"
28+
29+
increment.click()
30+
31+
with page.expect_download() as styled_info:
32+
styled_button.click()
33+
styled_download = styled_info.value
34+
styled_path_str = styled_download.path()
35+
assert styled_path_str is not None
36+
assert styled_download.suggested_filename == "styled-1-1.csv"
37+
assert (
38+
Path(styled_path_str).read_text()
39+
== "metric,value\ninventory,1\ndownload_number,1\n"
40+
)
41+
42+
increment.click()
43+
44+
with page.expect_download() as plain_info_2:
45+
plain_button.click()
46+
plain_download_2 = plain_info_2.value
47+
plain_path_str_2 = plain_download_2.path()
48+
assert plain_path_str_2 is not None
49+
assert plain_download_2.suggested_filename == "plain-2.csv"
50+
assert Path(plain_path_str_2).read_text() == "kind,inventory,count\nplain,2,2\n"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from __future__ import annotations
2+
3+
import faicons
4+
5+
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
6+
7+
8+
def _title(text: str) -> ui.Tag:
9+
return ui.tags.h4(text, class_="mb-3")
10+
11+
12+
def _action_label(text: str) -> ui.Tag:
13+
return ui.tags.span(text, class_="action-label")
14+
15+
16+
def _action_icon(text: str) -> ui.Tag:
17+
return ui.tags.span(text, class_="action-icon")
18+
19+
20+
app_ui = ui.page_fluid(
21+
ui.h2("Download link kitchen sink"),
22+
ui.layout_columns(
23+
ui.card(
24+
_title("Controls"),
25+
ui.input_text("prefix", "Filename prefix", value="report"),
26+
ui.input_checkbox("include_summary", "Include summary footer", value=True),
27+
),
28+
ui.card(
29+
_title("Links"),
30+
ui.download_link(
31+
"plain_link",
32+
_action_label("Plain report"),
33+
),
34+
ui.download_link(
35+
"styled_link",
36+
"Styled report",
37+
icon=faicons.icon_svg("file-arrow-down"),
38+
width="560px",
39+
),
40+
),
41+
),
42+
)
43+
44+
45+
def server(input: Inputs, output: Outputs, session: Session) -> None:
46+
plain_total = 0
47+
styled_total = 0
48+
current_prefix = "report"
49+
summary_enabled = True
50+
51+
@reactive.calc
52+
def prefix_value() -> str:
53+
value = input.prefix()
54+
return value.strip() or "report"
55+
56+
@reactive.calc
57+
def include_summary() -> bool:
58+
return bool(input.include_summary())
59+
60+
def sync_controls() -> None:
61+
nonlocal current_prefix, summary_enabled
62+
with reactive.isolate():
63+
current_prefix = prefix_value()
64+
summary_enabled = include_summary()
65+
66+
sync_controls()
67+
68+
@reactive.effect
69+
def _() -> None:
70+
prefix_value()
71+
include_summary()
72+
sync_controls()
73+
74+
@render.download(filename=lambda: f"{current_prefix}-plain.txt")
75+
async def plain_link():
76+
nonlocal plain_total
77+
plain_total += 1
78+
current_count = plain_total
79+
prefix = current_prefix
80+
include_footer = summary_enabled
81+
yield f"{prefix} plain download #{current_count}\n"
82+
if include_footer:
83+
yield "Summary: plain link\n"
84+
85+
@render.download(filename=lambda: f"{current_prefix}-styled.csv")
86+
async def styled_link():
87+
nonlocal styled_total
88+
styled_total += 1
89+
current_count = styled_total
90+
prefix = current_prefix
91+
include_footer = summary_enabled
92+
yield "metric,value\n"
93+
yield f"prefix,{prefix}\n"
94+
yield f"download_count,{current_count}\n"
95+
if include_footer:
96+
yield "footer,enabled\n"
97+
98+
99+
app = App(app_ui, server)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
from __future__ import annotations
2+
3+
from pathlib import Path
4+
5+
from playwright.sync_api import Page
6+
7+
from shiny.playwright import controller
8+
from shiny.run import ShinyAppProc
9+
10+
11+
def test_download_link_kitchensink(page: Page, local_app: ShinyAppProc) -> None:
12+
page.goto(local_app.url)
13+
14+
prefix_input = controller.InputText(page, "prefix")
15+
summary_toggle = controller.InputCheckbox(page, "include_summary")
16+
plain_link = controller.DownloadLink(page, "plain_link")
17+
styled_link = controller.DownloadLink(page, "styled_link")
18+
19+
plain_link.expect_label("Plain report")
20+
styled_link.expect_label("Styled report")
21+
styled_link.expect_width("560px")
22+
23+
with page.expect_download() as plain_info:
24+
plain_link.click()
25+
plain_download = plain_info.value
26+
plain_path_str = plain_download.path()
27+
assert plain_path_str is not None
28+
assert plain_download.suggested_filename == "report-plain.txt"
29+
plain_content = Path(plain_path_str).read_text()
30+
assert "report plain download #1" in plain_content
31+
assert "Summary: plain link" in plain_content
32+
33+
prefix_input.set("custom")
34+
summary_toggle.set(False)
35+
36+
with page.expect_download() as styled_info:
37+
styled_link.click()
38+
styled_download = styled_info.value
39+
styled_path_str = styled_download.path()
40+
assert styled_path_str is not None
41+
assert styled_download.suggested_filename == "custom-styled.csv"
42+
styled_content = Path(styled_path_str).read_text()
43+
assert "metric,value" in styled_content
44+
assert "prefix,custom" in styled_content
45+
assert "download_count,1" in styled_content
46+
assert "footer,enabled" not in styled_content
47+
48+
summary_toggle.set(True)
49+
50+
with page.expect_download() as plain_info_2:
51+
plain_link.click()
52+
plain_download_2 = plain_info_2.value
53+
plain_path_str_2 = plain_download_2.path()
54+
assert plain_path_str_2 is not None
55+
assert plain_download_2.suggested_filename == "custom-plain.txt"
56+
plain_content_2 = Path(plain_path_str_2).read_text()
57+
assert "custom plain download #2" in plain_content_2
58+
assert "Summary: plain link" in plain_content_2

0 commit comments

Comments
 (0)