Skip to content

Commit 2f7f130

Browse files
committed
First pass at Playwright testing
1 parent 811efb8 commit 2f7f130

File tree

4 files changed

+330
-0
lines changed

4 files changed

+330
-0
lines changed

shiny/playwright/controller/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
InputDateRange,
3333
InputNumeric,
3434
InputPassword,
35+
InputSubmitTextarea,
3536
InputText,
3637
InputTextArea,
3738
)
@@ -83,6 +84,7 @@
8384
"InputSelectize",
8485
"InputSlider",
8586
"InputSliderRange",
87+
"InputSubmitTextarea",
8688
"InputSwitch",
8789
"InputTaskButton",
8890
"InputText",

shiny/playwright/controller/_input_fields.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,3 +966,110 @@ def expect_autoclose(
966966
# TODO-future; Composable expectations
967967
self.date_start.expect_autoclose(value, timeout=timeout)
968968
self.date_end.expect_autoclose(value, timeout=timeout)
969+
970+
971+
class InputSubmitTextarea(
972+
_SetTextM,
973+
WidthContainerStyleM,
974+
_ExpectTextInputValueM,
975+
_ExpectPlaceholderAttrM,
976+
UiWithLabel,
977+
):
978+
"""Controller for :func:`shiny.ui.input_submit_textarea`."""
979+
980+
loc_button: Locator
981+
"""Playwright `Locator` for the submit button."""
982+
983+
def __init__(self, page: Page, id: str) -> None:
984+
"""
985+
Initializes the input submit textarea.
986+
987+
Parameters
988+
----------
989+
page
990+
The page where the input submit textarea is located.
991+
id
992+
The id of the input submit textarea.
993+
"""
994+
super().__init__(
995+
page,
996+
id=id,
997+
loc=f"textarea#{id}.form-control",
998+
)
999+
self.loc_button = self.loc_container.locator(".bslib-submit-textarea-btn")
1000+
1001+
def set(self, value: str, *, submit: bool = False, timeout: Timeout = None) -> None:
1002+
"""
1003+
Sets the text value in the textarea.
1004+
1005+
Parameters
1006+
----------
1007+
value
1008+
The text to set.
1009+
submit
1010+
Whether to click the submit button after setting the text. Defaults to `False`.
1011+
timeout
1012+
The maximum time to wait for the text to be set. Defaults to `None`.
1013+
"""
1014+
set_text(self.loc, value, timeout=timeout)
1015+
if submit:
1016+
self.loc_button.click(timeout=timeout)
1017+
1018+
def submit(self, *, timeout: Timeout = None) -> None:
1019+
"""
1020+
Clicks the submit button.
1021+
1022+
Parameters
1023+
----------
1024+
timeout
1025+
The maximum time to wait for the click. Defaults to `None`.
1026+
"""
1027+
self.loc_button.click(timeout=timeout)
1028+
1029+
def expect_rows(self, value: AttrValue, *, timeout: Timeout = None) -> None:
1030+
"""
1031+
Expect the `rows` attribute of the input submit textarea to have a specific value.
1032+
1033+
Parameters
1034+
----------
1035+
value
1036+
The expected value of the `rows` attribute.
1037+
timeout
1038+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1039+
"""
1040+
_expect_attribute_to_have_value(self.loc, "rows", value=value, timeout=timeout)
1041+
1042+
def expect_data_needs_modifier(
1043+
self, value: bool, *, timeout: Timeout = None
1044+
) -> None:
1045+
"""
1046+
Expect the `data-needs-modifier` attribute to be present or absent.
1047+
1048+
Parameters
1049+
----------
1050+
value
1051+
If `True`, expects the attribute to be present. If `False`, expects it to be absent.
1052+
timeout
1053+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1054+
"""
1055+
_expect_attribute_to_have_value(
1056+
self.loc,
1057+
"data-needs-modifier",
1058+
value="" if value else None,
1059+
timeout=timeout,
1060+
)
1061+
1062+
def expect_button_label(
1063+
self, value: PatternOrStr, *, timeout: Timeout = None
1064+
) -> None:
1065+
"""
1066+
Expect the submit button to have a specific label.
1067+
1068+
Parameters
1069+
----------
1070+
value
1071+
The expected label text.
1072+
timeout
1073+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1074+
"""
1075+
playwright_expect(self.loc_button).to_contain_text(value, timeout=timeout)
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""
2+
Test app for input_submit_textarea
3+
"""
4+
5+
from shiny import App, Inputs, Outputs, Session, reactive, render, ui
6+
7+
app_ui = ui.page_fluid(
8+
ui.h3("Basic submit textarea"),
9+
ui.input_submit_textarea(
10+
"basic",
11+
label="Enter text",
12+
placeholder="Type something here...",
13+
value="Initial value",
14+
rows=3,
15+
width="400px",
16+
),
17+
ui.output_code("basic_value", placeholder=True),
18+
ui.hr(),
19+
ui.h3("Submit textarea with enter key (no modifier)"),
20+
ui.input_submit_textarea(
21+
"no_modifier",
22+
label="Press Enter to submit",
23+
placeholder="Press Enter to submit",
24+
rows=2,
25+
submit_key="enter",
26+
),
27+
ui.output_code("no_modifier_value", placeholder=True),
28+
ui.hr(),
29+
ui.h3("Submit textarea with custom button"),
30+
ui.input_submit_textarea(
31+
"custom_button",
32+
label="Custom button",
33+
placeholder="Type and click submit",
34+
button=ui.input_task_button("custom_submit", "Send", class_="btn-success"),
35+
rows=1,
36+
),
37+
ui.output_code("custom_button_value", placeholder=True),
38+
ui.hr(),
39+
ui.h3("Update controls"),
40+
ui.input_action_button("update_value", "Set to 'Updated value'"),
41+
ui.input_action_button("update_placeholder", "Update placeholder"),
42+
ui.input_action_button("submit_programmatic", "Submit programmatically"),
43+
)
44+
45+
46+
def server(input: Inputs, output: Outputs, session: Session):
47+
48+
@render.text
49+
def basic_value():
50+
if "basic" in input:
51+
return f"Submitted: {input.basic()}"
52+
else:
53+
return "No value submitted yet"
54+
55+
@render.text
56+
def no_modifier_value():
57+
if "no_modifier" in input:
58+
return f"Submitted: {input.no_modifier()}"
59+
else:
60+
return "No value submitted yet"
61+
62+
@render.text
63+
def custom_button_value():
64+
if "custom_button" in input:
65+
return f"Submitted: {input.custom_button()}"
66+
else:
67+
return "No value submitted yet"
68+
69+
@reactive.effect
70+
@reactive.event(input.update_value)
71+
def _():
72+
ui.update_submit_textarea("basic", value="Updated value")
73+
74+
@reactive.effect
75+
@reactive.event(input.update_placeholder)
76+
def _():
77+
ui.update_submit_textarea("basic", placeholder="New placeholder text")
78+
79+
@reactive.effect
80+
@reactive.event(input.submit_programmatic)
81+
def _():
82+
ui.update_submit_textarea(
83+
"basic", value="Programmatically submitted", submit=True
84+
)
85+
86+
87+
app = App(app_ui, server)
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
from playwright.sync_api import Page
2+
3+
from shiny.playwright import controller
4+
from shiny.run import ShinyAppProc
5+
6+
7+
def test_input_submit_textarea_initial_state(page: Page, local_app: ShinyAppProc):
8+
page.goto(local_app.url)
9+
10+
basic = controller.InputSubmitTextarea(page, "basic")
11+
basic.expect_label("Enter text")
12+
basic.expect_placeholder("Type something here...")
13+
basic.expect_value("Initial value")
14+
basic.expect_rows("3")
15+
basic.expect_width("400px")
16+
basic.expect_data_needs_modifier(True)
17+
basic.expect_button_label("Submit")
18+
19+
value_output = controller.OutputCode(page, "basic_value")
20+
value_output.expect_value("No value submitted yet")
21+
22+
23+
def test_input_submit_textarea_submit_with_button(page: Page, local_app: ShinyAppProc):
24+
page.goto(local_app.url)
25+
26+
basic = controller.InputSubmitTextarea(page, "basic")
27+
value_output = controller.OutputCode(page, "basic_value")
28+
value_output.expect_value("No value submitted yet")
29+
30+
basic.submit()
31+
value_output.expect_value("Submitted: Initial value")
32+
33+
basic.set("New text content")
34+
basic.expect_value("New text content")
35+
value_output.expect_value("Submitted: Initial value")
36+
37+
basic.submit()
38+
basic.expect_value("")
39+
value_output.expect_value("Submitted: New text content")
40+
41+
42+
def test_input_submit_textarea_set_and_submit(page: Page, local_app: ShinyAppProc):
43+
page.goto(local_app.url)
44+
45+
basic = controller.InputSubmitTextarea(page, "basic")
46+
value_output = controller.OutputCode(page, "basic_value")
47+
48+
# Set and submit in one call
49+
basic.set("One-step submission", submit=True)
50+
basic.expect_value("")
51+
value_output.expect_value("Submitted: One-step submission")
52+
53+
54+
def test_input_submit_textarea_no_modifier_key(page: Page, local_app: ShinyAppProc):
55+
page.goto(local_app.url)
56+
57+
no_modifier = controller.InputSubmitTextarea(page, "no_modifier")
58+
value_output = controller.OutputCode(page, "no_modifier_value")
59+
no_modifier.expect_data_needs_modifier(False)
60+
61+
no_modifier.set("Enter key test")
62+
no_modifier.submit()
63+
value_output.expect_value("Submitted: Enter key test")
64+
65+
66+
def test_input_submit_textarea_custom_button(page: Page, local_app: ShinyAppProc):
67+
page.goto(local_app.url)
68+
69+
custom = controller.InputSubmitTextarea(page, "custom_button")
70+
value_output = controller.OutputCode(page, "custom_button_value")
71+
custom.expect_button_label("Send")
72+
73+
custom.set("Custom button test", submit=True)
74+
value_output.expect_value("Submitted: Custom button test")
75+
76+
77+
def test_input_submit_textarea_update_value(page: Page, local_app: ShinyAppProc):
78+
"""Test updating value from server"""
79+
page.goto(local_app.url)
80+
81+
basic = controller.InputSubmitTextarea(page, "basic")
82+
value_output = controller.OutputCode(page, "basic_value")
83+
update_btn = controller.InputActionButton(page, "update_value")
84+
85+
update_btn.click()
86+
basic.expect_value("Updated value")
87+
value_output.expect_value("No value submitted yet")
88+
89+
basic.submit()
90+
value_output.expect_value("Submitted: Updated value")
91+
92+
93+
def test_input_submit_textarea_update_placeholder(page: Page, local_app: ShinyAppProc):
94+
page.goto(local_app.url)
95+
96+
basic = controller.InputSubmitTextarea(page, "basic")
97+
update_btn = controller.InputActionButton(page, "update_placeholder")
98+
basic.expect_placeholder("Type something here...")
99+
100+
update_btn.click()
101+
basic.expect_placeholder("New placeholder text")
102+
103+
104+
def test_input_submit_textarea_programmatic_submit(page: Page, local_app: ShinyAppProc):
105+
page.goto(local_app.url)
106+
107+
basic = controller.InputSubmitTextarea(page, "basic")
108+
value_output = controller.OutputCode(page, "basic_value")
109+
submit_btn = controller.InputActionButton(page, "submit_programmatic")
110+
111+
basic.set("Programmatically submitted")
112+
submit_btn.click()
113+
value_output.expect_value("Submitted: Programmatically submitted")
114+
115+
116+
def test_input_submit_textarea_multiple_submissions(
117+
page: Page, local_app: ShinyAppProc
118+
):
119+
page.goto(local_app.url)
120+
121+
basic = controller.InputSubmitTextarea(page, "basic")
122+
value_output = controller.OutputCode(page, "basic_value")
123+
124+
# First submission
125+
basic.set("First", submit=True)
126+
value_output.expect_value("Submitted: First")
127+
128+
# Second submission
129+
basic.set("Second", submit=True)
130+
value_output.expect_value("Submitted: Second")
131+
132+
# Third submission
133+
basic.set("Third", submit=True)
134+
value_output.expect_value("Submitted: Third")

0 commit comments

Comments
 (0)