Skip to content

Commit 36d27e7

Browse files
committed
fix handling the setting of both multiple and single selectize elements
1 parent 09a1b71 commit 36d27e7

File tree

2 files changed

+130
-47
lines changed

2 files changed

+130
-47
lines changed

shiny/playwright/controller/_input_controls.py

Lines changed: 108 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,7 @@ def set(
579579
The timeout for the action. Defaults to `None`.
580580
"""
581581
if not isinstance(selected, str):
582-
raise TypeError("`selected` must be a string")
582+
raise TypeError("`selected=` must be a string")
583583

584584
# Only need to set.
585585
# The Browser will _unset_ the previously selected radio button
@@ -778,10 +778,10 @@ def set(
778778
"""
779779
# Having an arr of size 0 is allowed. Will uncheck everything
780780
if not isinstance(selected, list):
781-
raise TypeError("`selected` must be a list or tuple")
781+
raise TypeError("`selected=` must be a list or tuple")
782782
for item in selected:
783783
if not isinstance(item, str):
784-
raise TypeError("`selected` must be a list of strings")
784+
raise TypeError("`selected=` must be a list of strings")
785785

786786
# Make sure the selected items exist
787787
# Similar to `self.expect_choices(choices = selected)`, but with
@@ -1200,62 +1200,126 @@ def set(
12001200
timeout
12011201
The maximum time to wait for the selection to be set. Defaults to `None`.
12021202
"""
1203-
if isinstance(selected, str):
1204-
selected = [selected]
1205-
# Open the dropdown
1206-
self._loc_events.click()
12071203

1208-
# Sift through the selected items
1209-
# From left to right, we will remove ordered items that are not in the ordered `selected`
1210-
# If any selected items are not in the current selection, we will add them at the end
1211-
i = 0
1212-
while i < self._loc_events.locator("> .item").count():
1213-
item_loc = self._loc_events.locator("> .item").nth(i)
1214-
item_data_value = item_loc.get_attribute("data-value")
1204+
def click_item(data_value: str, error_str: str) -> None:
1205+
"""
1206+
Clicks the item in the dropdown by its `data-value` attribute.
1207+
"""
1208+
if not isinstance(data_value, str):
1209+
raise TypeError(error_str)
1210+
1211+
# Wait for the item to exist
1212+
playwright_expect(
1213+
self._loc_selectize.locator(f"[data-value='{data_value}']")
1214+
).to_have_count(1, timeout=timeout)
1215+
# Click the item
1216+
self._loc_selectize.locator(f"[data-value='{data_value}']").click(
1217+
timeout=timeout
1218+
)
1219+
1220+
# Make sure the selectize exists
1221+
playwright_expect(self._loc_events).to_have_count(1, timeout=timeout)
1222+
1223+
if self.loc.get_attribute("multiple") is None:
1224+
# Single element selectize
1225+
if isinstance(selected, list):
1226+
if len(selected) != 1:
1227+
raise ValueError(
1228+
"Expected a `str` value (or a list of a single `str` value) when setting a single-select input."
1229+
)
1230+
selected = selected[0]
1231+
1232+
# Open the dropdown
1233+
self._loc_events.click(timeout=timeout)
1234+
1235+
try:
1236+
# Click the item (which closes the dropdown)
1237+
click_item(selected, "`selected=` value must be a `str`")
1238+
finally:
1239+
# Be sure to close the dropdown
1240+
# (While this is not necessary on a sucessful `set()`, it is cleaner
1241+
# than a catch all except)
1242+
self._loc_events.press("Escape", timeout=timeout)
1243+
1244+
else:
1245+
# Multiple element selectize
12151246

12161247
def delete_item(item_loc: Locator) -> None:
12171248
"""
12181249
Deletes the item by clicking on it and pressing the Delete key.
12191250
"""
1251+
12201252
item_loc.click()
12211253
self.page.keyboard.press("Delete")
12221254

1223-
# If the item has no data-value, remove it
1224-
if item_data_value is None:
1225-
delete_item(item_loc)
1226-
continue
1227-
1228-
# If there are more items than selected, we need to remove the extra ones
1229-
if i >= len(selected):
1230-
delete_item(item_loc)
1231-
continue
1232-
1233-
selected_data_value = selected[i]
1234-
1235-
# If the item is not the next `selected` value, remove it
1236-
if item_data_value != selected_data_value:
1237-
delete_item(item_loc)
1238-
continue
1239-
1240-
# The item is the next `selected` value
1241-
# Increment the index!
1242-
i += 1
1243-
1244-
# Add the remaining items
1245-
if i < len(selected):
1246-
for data_value in selected[i:]:
1247-
# Click on the item in the dropdown to select it
1248-
self._loc_selectize.locator(f"[data-value='{data_value}']").click(
1249-
timeout=timeout
1255+
if isinstance(selected, str):
1256+
selected = [selected]
1257+
if not isinstance(selected, list):
1258+
raise TypeError(
1259+
"`selected=` must be a single `str` value or a list of `str` values when setting a multiple-select input"
12501260
)
12511261

1252-
# Close the dropdown
1253-
self._loc_events.press("Escape")
1262+
# Open the dropdown
1263+
self._loc_events.click()
1264+
1265+
try:
1266+
# Sift through the selected items
1267+
# From left to right, we will remove ordered items that are not in the
1268+
# ordered `selected`
1269+
# If any selected items are not in the current selection, we will add
1270+
# them at the end
1271+
1272+
# All state transitions examples have an end goal of
1273+
# A,B,C,D,E
1274+
#
1275+
# Items wrapped in `[]` are the item of interest at position `i`
1276+
# Ex: `Z`,i=3 in A,B,C,[Z],E
1277+
1278+
i = 0
1279+
while i < self._loc_events.locator("> .item").count():
1280+
item_loc = self._loc_events.locator("> .item").nth(i)
1281+
item_data_value = item_loc.get_attribute("data-value")
1282+
1283+
# If the item has no data-value, remove it
1284+
# Transition: A,B,C,[?],D,E -> A,B,C,[D],E
1285+
if item_data_value is None:
1286+
delete_item(item_loc)
1287+
continue
1288+
1289+
# If there are more items than selected, remove the extras
1290+
# Transition: A,B,C,D,E,[Z] -> A,B,C,D,E,[]
1291+
if i >= len(selected):
1292+
delete_item(item_loc)
1293+
continue
1294+
1295+
selected_data_value = selected[i]
1296+
1297+
# If the item is not the next `selected` value, remove it
1298+
# Transition: A,B,[Z],C,D,E -> A,B,[C],D,E
1299+
if item_data_value != selected_data_value:
1300+
delete_item(item_loc)
1301+
continue
1302+
1303+
# The item is the next `selected` value
1304+
# Increment the index! (No need to remove it and add it back)
1305+
# A,B,[C],D,E -> A,B,C,[D],E
1306+
i += 1
1307+
1308+
# Add the remaining items
1309+
# A,B,[] -> A,B,C,D,E
1310+
if i < len(selected):
1311+
for data_value in selected[i:]:
1312+
click_item(
1313+
data_value, f"`selected[{i}]=` value must be a `str`"
1314+
)
1315+
1316+
finally:
1317+
# Be sure to close the dropdown
1318+
self._loc_events.press("Escape", timeout=timeout)
12541319
return
12551320

12561321
def expect_choices(
12571322
self,
1258-
# TODO-future; support patterns?
12591323
choices: ListPatternOrStr,
12601324
*,
12611325
timeout: Timeout = None,

tests/playwright/shiny/inputs/input_selectize/test_input_selectize.py

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import pytest
12
from playwright.sync_api import Page, expect
23

34
from shiny.playwright import controller
@@ -40,10 +41,18 @@ def test_input_selectize_kitchen(page: Page, local_app: ShinyAppProc) -> None:
4041

4142
state1.expect_multiple(True)
4243

43-
state1.set(["IA", "CA"])
44+
state1.set("CA")
45+
state1.expect_selected(["CA"])
4446

45-
state1.expect_selected(["IA", "CA"])
47+
with pytest.raises(TypeError) as err:
48+
state1.set({"a": "1"}) # pyright: ignore[reportArgumentType]
49+
assert "when setting a multiple-select input" in str(err.value)
50+
with pytest.raises(TypeError) as err:
51+
state1.set(45) # pyright: ignore[reportArgumentType]
52+
assert "value must be a" in str(err.value)
4653

54+
state1.set(["IA", "CA"])
55+
state1.expect_selected(["IA", "CA"])
4756
value1.expect_value("('IA', 'CA')")
4857

4958
# -------------------------
@@ -89,7 +98,17 @@ def test_input_selectize_kitchen(page: Page, local_app: ShinyAppProc) -> None:
8998

9099
state3.expect_multiple(False)
91100

92-
state3.set(["NJ"])
101+
state3.set("NJ")
102+
103+
with pytest.raises(ValueError) as err:
104+
state3.set(["NJ", "NY"])
105+
assert "when setting a single-select input" in str(err.value)
106+
with pytest.raises(ValueError) as err:
107+
state3.set([])
108+
assert "when setting a single-select input" in str(err.value)
109+
with pytest.raises(TypeError) as err:
110+
state3.set(45) # pyright: ignore[reportArgumentType]
111+
assert "value must be a" in str(err.value)
93112

94113
state3.expect_selected(["NJ"])
95114
value3.expect_value("NJ")

0 commit comments

Comments
 (0)