Skip to content

Commit aa90a27

Browse files
tests(selectize): Add controllers for selectize (#1519)
Co-authored-by: Barret Schloerke <barret@posit.co>
1 parent dfde632 commit aa90a27

File tree

4 files changed

+370
-49
lines changed

4 files changed

+370
-49
lines changed

shiny/playwright/controller/_controls.py

Lines changed: 181 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,16 +1032,192 @@ def expect_selectize(self, value: bool, *, timeout: Timeout = None) -> None:
10321032
)
10331033

10341034

1035-
class InputSelectize(_InputSelectBase):
1035+
class InputSelectize(
1036+
_UiWithLabel,
1037+
):
10361038
"""Controller for :func:`shiny.ui.input_selectize`."""
10371039

10381040
def __init__(self, page: Page, id: str) -> None:
1039-
super().__init__(
1040-
page,
1041-
id=id,
1042-
select_class=".selectized",
1041+
super().__init__(page, id=id, loc=f"#{id} + .selectize-control")
1042+
self._loc_dropdown = self.loc.locator("> .selectize-dropdown")
1043+
self._loc_events = self.loc.locator("> .selectize-input")
1044+
self._loc_selectize = self._loc_dropdown.locator(
1045+
"> .selectize-dropdown-content"
1046+
)
1047+
self.loc = self.loc_container.locator(f"select#{id}")
1048+
self.loc_choice_groups = self._loc_selectize.locator(
1049+
"> .optgroup > .optgroup-header"
1050+
)
1051+
# Do not use `.option` class as we are not guaranteed to have it.
1052+
# We are only guaranteed to have `data-value` attribute for each _option_
1053+
self.loc_choices = self._loc_selectize.locator("[data-value]")
1054+
self.loc_selected = self.loc_container.locator(f"select#{id} > option")
1055+
1056+
def set(
1057+
self,
1058+
selected: str | list[str],
1059+
*,
1060+
timeout: Timeout = None,
1061+
) -> None:
1062+
"""
1063+
Sets the selected option(s) of the input selectize.
1064+
1065+
Parameters
1066+
----------
1067+
selected
1068+
The value(s) of the selected option(s).
1069+
timeout
1070+
The maximum time to wait for the selection to be set. Defaults to `None`.
1071+
"""
1072+
if isinstance(selected, str):
1073+
selected = [selected]
1074+
self._loc_events.click()
1075+
for value in selected:
1076+
self._loc_selectize.locator(f"[data-value='{value}']").click(
1077+
timeout=timeout
1078+
)
1079+
self._loc_events.press("Escape")
1080+
1081+
def expect_choices(
1082+
self,
1083+
# TODO-future; support patterns?
1084+
choices: ListPatternOrStr,
1085+
*,
1086+
timeout: Timeout = None,
1087+
) -> None:
1088+
"""
1089+
Expect the available options of the input selectize to be an exact match.
1090+
1091+
Parameters
1092+
----------
1093+
choices
1094+
The expected choices of the input select.
1095+
timeout
1096+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1097+
"""
1098+
self._populate_dom()
1099+
# Playwright doesn't like lists of size 0. Instead, check for empty locator
1100+
if len(choices) == 0:
1101+
playwright_expect(self.loc_choices).to_have_count(0, timeout=timeout)
1102+
return
1103+
1104+
_MultipleDomItems.expect_locator_values_in_list(
1105+
page=self.page,
1106+
loc_container=self._loc_selectize,
1107+
el_type=self.page.locator("[data-value]"),
1108+
arr_name="choices",
1109+
arr=choices,
1110+
key="data-value",
1111+
timeout=timeout,
10431112
)
10441113

1114+
def expect_selected(
1115+
self,
1116+
value: ListPatternOrStr,
1117+
*,
1118+
timeout: Timeout = None,
1119+
) -> None:
1120+
"""
1121+
Expect the selected option(s) of the input select to be an exact match.
1122+
1123+
Parameters
1124+
----------
1125+
value
1126+
The expected value(s) of the selected option(s).
1127+
timeout
1128+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1129+
"""
1130+
# Playwright doesn't like lists of size 0
1131+
if isinstance(value, list) and len(value) == 0:
1132+
playwright_expect(self.loc_selected).to_have_count(0, timeout=timeout)
1133+
return
1134+
1135+
_MultipleDomItems.expect_locator_values_in_list(
1136+
page=self.page,
1137+
loc_container=self.loc,
1138+
el_type=self.page.locator("> option"),
1139+
arr_name="value",
1140+
arr=value,
1141+
key="value",
1142+
)
1143+
1144+
def _populate_dom(self, timeout: Timeout = None) -> None:
1145+
"""
1146+
The click and Escape keypress is used to load the DOM elements
1147+
"""
1148+
self._loc_events.click(timeout=timeout)
1149+
expect_to_have_style(self._loc_dropdown, "display", "block", timeout=timeout)
1150+
self.page.locator("body").click(timeout=timeout)
1151+
expect_to_have_style(self._loc_dropdown, "display", "none", timeout=timeout)
1152+
1153+
def expect_choice_groups(
1154+
self,
1155+
choice_groups: ListPatternOrStr,
1156+
*,
1157+
timeout: Timeout = None,
1158+
) -> None:
1159+
"""
1160+
Expect the choice groups of the input select to be an exact match.
1161+
1162+
Parameters
1163+
----------
1164+
choice_groups
1165+
The expected choice groups of the input select.
1166+
timeout
1167+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1168+
"""
1169+
self._populate_dom()
1170+
# Playwright doesn't like lists of size 0. Instead, use `None`
1171+
if len(choice_groups) == 0:
1172+
playwright_expect(self.loc_choice_groups).to_have_count(0, timeout=timeout)
1173+
return
1174+
1175+
playwright_expect(self.loc_choice_groups).to_have_text(
1176+
choice_groups, timeout=timeout
1177+
)
1178+
1179+
def expect_choice_labels(
1180+
self,
1181+
value: ListPatternOrStr,
1182+
*,
1183+
timeout: Timeout = None,
1184+
) -> None:
1185+
"""
1186+
Expect the choice labels of the input selectize to be an exact match.
1187+
1188+
Parameters
1189+
----------
1190+
value
1191+
The expected choice labels of the input select.
1192+
timeout
1193+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1194+
"""
1195+
self._populate_dom()
1196+
# Playwright doesn't like lists of size 0. Instead, use `None`
1197+
if len(value) == 0:
1198+
playwright_expect(self.loc_choices).to_have_count(0, timeout=timeout)
1199+
return
1200+
playwright_expect(self.loc_choices).to_have_text(value, timeout=timeout)
1201+
1202+
# multiple: bool = False,
1203+
def expect_multiple(self, value: bool, *, timeout: Timeout = None) -> None:
1204+
"""
1205+
Expect the input selectize to allow multiple selections.
1206+
1207+
Parameters
1208+
----------
1209+
value
1210+
Whether the input select allows multiple selections.
1211+
timeout
1212+
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
1213+
"""
1214+
if value:
1215+
expect_attribute_to_have_value(
1216+
self.loc, "multiple", "multiple", timeout=timeout
1217+
)
1218+
else:
1219+
expect_attribute_to_have_value(self.loc, "multiple", None, timeout=timeout)
1220+
10451221

10461222
class _InputActionBase(_UiBase):
10471223
def expect_label(
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
from shiny import App, Inputs, Outputs, Session, render, ui
2+
3+
states = {
4+
"East Coast": {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"},
5+
"West Coast": {"WA": "Washington", "OR": "Oregon", "CA": "California"},
6+
"Midwest": {"MN": "Minnesota", "WI": "Wisconsin", "IA": "Iowa"},
7+
}
8+
9+
state_without_groups = {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"}
10+
state_without_keys = ["New York", "New Jersey", "Connecticut"]
11+
12+
app_ui = ui.page_fluid(
13+
ui.input_selectize(
14+
"state1",
15+
"Choose a state:",
16+
states,
17+
multiple=True,
18+
),
19+
ui.input_selectize(
20+
"state2",
21+
"Selectize Options",
22+
states,
23+
multiple=True,
24+
options=(
25+
{
26+
"placeholder": "Enter text",
27+
"render": ui.js_eval(
28+
'{option: function(item, escape) {return "<div><strong>Select " + escape(item.label) + "</strong></div>";}}'
29+
),
30+
"create": True,
31+
}
32+
),
33+
),
34+
ui.input_selectize(
35+
"state3",
36+
"Single Selectize",
37+
state_without_groups,
38+
multiple=False,
39+
options={"plugins": ["clear_button"]},
40+
),
41+
ui.input_selectize(
42+
"state4",
43+
"Simple Selectize",
44+
state_without_keys,
45+
multiple=False,
46+
),
47+
ui.hr(),
48+
ui.output_code("value1"),
49+
ui.output_code("value2"),
50+
ui.output_code("value3"),
51+
ui.output_code("value4"),
52+
)
53+
54+
55+
def server(input: Inputs, output: Outputs, session: Session):
56+
@render.code
57+
def value1():
58+
return str(input.state1())
59+
60+
@render.code
61+
def value2():
62+
return str(input.state2())
63+
64+
@render.code
65+
def value3():
66+
return str(input.state3())
67+
68+
@render.code
69+
def value4():
70+
return str(input.state4())
71+
72+
73+
app = App(app_ui, server)
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
from playwright.sync_api import Page, expect
2+
3+
from shiny.playwright import controller
4+
from shiny.run import ShinyAppProc
5+
6+
7+
def test_input_selectize_kitchen(page: Page, local_app: ShinyAppProc) -> None:
8+
page.goto(local_app.url)
9+
10+
state1 = controller.InputSelectize(page, "state1")
11+
state2 = controller.InputSelectize(page, "state2")
12+
state3 = controller.InputSelectize(page, "state3")
13+
state4 = controller.InputSelectize(page, "state4")
14+
15+
value1 = controller.OutputCode(page, "value1")
16+
value2 = controller.OutputCode(page, "value2")
17+
value3 = controller.OutputCode(page, "value3")
18+
value4 = controller.OutputCode(page, "value4")
19+
20+
# -------------------------
21+
22+
expect(state1.loc_label).to_have_text("Choose a state:")
23+
state1.expect_label("Choose a state:")
24+
25+
state1.expect_choices(["NY", "NJ", "CT", "WA", "OR", "CA", "MN", "WI", "IA"])
26+
state1.expect_choice_labels(
27+
[
28+
"New York",
29+
"New Jersey",
30+
"Connecticut",
31+
"Washington",
32+
"Oregon",
33+
"California",
34+
"Minnesota",
35+
"Wisconsin",
36+
"Iowa",
37+
]
38+
)
39+
state1.expect_choice_groups(["East Coast", "West Coast", "Midwest"])
40+
41+
state1.expect_multiple(True)
42+
43+
state1.set(["IA", "CA"])
44+
45+
state1.expect_selected(["IA", "CA"])
46+
47+
value1.expect_value("('IA', 'CA')")
48+
49+
# -------------------------
50+
51+
state2.expect_label("Selectize Options")
52+
53+
state2.expect_choices(["NY", "NJ", "CT", "WA", "OR", "CA", "MN", "WI", "IA"])
54+
state2.expect_choice_labels(
55+
[
56+
"Select New York",
57+
"Select New Jersey",
58+
"Select Connecticut",
59+
"Select Washington",
60+
"Select Oregon",
61+
"Select California",
62+
"Select Minnesota",
63+
"Select Wisconsin",
64+
"Select Iowa",
65+
]
66+
)
67+
state2.expect_choice_groups(["East Coast", "West Coast", "Midwest"])
68+
69+
state2.expect_multiple(True)
70+
71+
state2.set(["IA", "CA"])
72+
73+
state2.expect_selected(["IA", "CA"])
74+
value2.expect_value("('IA', 'CA')")
75+
76+
# -------------------------
77+
78+
state3.expect_label("Single Selectize")
79+
80+
state3.expect_choices(["NY", "NJ", "CT"])
81+
82+
state3.expect_choice_labels(
83+
[
84+
"New York",
85+
"New Jersey",
86+
"Connecticut",
87+
]
88+
)
89+
90+
state3.expect_multiple(False)
91+
92+
state3.set(["NJ"])
93+
94+
state3.expect_selected(["NJ"])
95+
value3.expect_value("NJ")
96+
97+
# -------------------------
98+
99+
state4.expect_label("Simple Selectize")
100+
101+
state4.expect_choices(["New York", "New Jersey", "Connecticut"])
102+
103+
state4.expect_choice_labels(
104+
[
105+
"New York",
106+
"New Jersey",
107+
"Connecticut",
108+
]
109+
)
110+
111+
state4.expect_multiple(False)
112+
113+
state4.set(["New York"])
114+
115+
state4.expect_selected(["New York"])
116+
value4.expect_value("New York")

0 commit comments

Comments
 (0)