Skip to content
186 changes: 181 additions & 5 deletions shiny/playwright/controller/_controls.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,16 +1032,192 @@ def expect_selectize(self, value: bool, *, timeout: Timeout = None) -> None:
)


class InputSelectize(_InputSelectBase):
class InputSelectize(
_UiWithLabel,
):
"""Controller for :func:`shiny.ui.input_selectize`."""

def __init__(self, page: Page, id: str) -> None:
super().__init__(
page,
id=id,
select_class=".selectized",
super().__init__(page, id=id, loc=f"#{id} + .selectize-control")
self._loc_dropdown = self.loc.locator("> .selectize-dropdown")
self._loc_events = self.loc.locator("> .selectize-input")
self._loc_selectize = self._loc_dropdown.locator(
"> .selectize-dropdown-content"
)
self.loc = self.loc_container.locator(f"select#{id}")
self.loc_choice_groups = self._loc_selectize.locator(
"> .optgroup > .optgroup-header"
)
# Do not use `.option` class as we are not guaranteed to have it.
# We are only guaranteed to have `data-value` attribute for each _option_
self.loc_choices = self._loc_selectize.locator("[data-value]")
self.loc_selected = self.loc_container.locator(f"select#{id} > option")

def set(
self,
selected: str | list[str],
*,
timeout: Timeout = None,
) -> None:
"""
Sets the selected option(s) of the input selectize.

Parameters
----------
selected
The value(s) of the selected option(s).
timeout
The maximum time to wait for the selection to be set. Defaults to `None`.
"""
if isinstance(selected, str):
selected = [selected]
self._loc_events.click()
for value in selected:
self._loc_selectize.locator(f"[data-value='{value}']").click(
timeout=timeout
)
self._loc_events.press("Escape")

def expect_choices(
self,
# TODO-future; support patterns?
choices: ListPatternOrStr,
*,
timeout: Timeout = None,
) -> None:
"""
Expect the available options of the input selectize to be an exact match.

Parameters
----------
choices
The expected choices of the input select.
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
self._populate_dom()
# Playwright doesn't like lists of size 0. Instead, check for empty locator
if len(choices) == 0:
playwright_expect(self.loc_choices).to_have_count(0, timeout=timeout)
return

_MultipleDomItems.expect_locator_values_in_list(
page=self.page,
loc_container=self._loc_selectize,
el_type=self.page.locator("[data-value]"),
arr_name="choices",
arr=choices,
key="data-value",
timeout=timeout,
)

def expect_selected(
self,
value: ListPatternOrStr,
*,
timeout: Timeout = None,
) -> None:
"""
Expect the selected option(s) of the input select to be an exact match.

Parameters
----------
value
The expected value(s) of the selected option(s).
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
# Playwright doesn't like lists of size 0
if isinstance(value, list) and len(value) == 0:
playwright_expect(self.loc_selected).to_have_count(0, timeout=timeout)
return

_MultipleDomItems.expect_locator_values_in_list(
page=self.page,
loc_container=self.loc,
el_type=self.page.locator("> option"),
arr_name="value",
arr=value,
key="value",
)

def _populate_dom(self, timeout: Timeout = None) -> None:
"""
The click and Escape keypress is used to load the DOM elements
"""
self._loc_events.click(timeout=timeout)
expect_to_have_style(self._loc_dropdown, "display", "block", timeout=timeout)
self.page.locator("body").click(timeout=timeout)
expect_to_have_style(self._loc_dropdown, "display", "none", timeout=timeout)

def expect_choice_groups(
self,
choice_groups: ListPatternOrStr,
*,
timeout: Timeout = None,
) -> None:
"""
Expect the choice groups of the input select to be an exact match.

Parameters
----------
choice_groups
The expected choice groups of the input select.
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
self._populate_dom()
# Playwright doesn't like lists of size 0. Instead, use `None`
if len(choice_groups) == 0:
playwright_expect(self.loc_choice_groups).to_have_count(0, timeout=timeout)
return

playwright_expect(self.loc_choice_groups).to_have_text(
choice_groups, timeout=timeout
)

def expect_choice_labels(
self,
value: ListPatternOrStr,
*,
timeout: Timeout = None,
) -> None:
"""
Expect the choice labels of the input selectize to be an exact match.

Parameters
----------
value
The expected choice labels of the input select.
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
self._populate_dom()
# Playwright doesn't like lists of size 0. Instead, use `None`
if len(value) == 0:
playwright_expect(self.loc_choices).to_have_count(0, timeout=timeout)
return
playwright_expect(self.loc_choices).to_have_text(value, timeout=timeout)

# multiple: bool = False,
def expect_multiple(self, value: bool, *, timeout: Timeout = None) -> None:
"""
Expect the input selectize to allow multiple selections.

Parameters
----------
value
Whether the input select allows multiple selections.
timeout
The maximum time to wait for the expectation to be fulfilled. Defaults to `None`.
"""
if value:
expect_attribute_to_have_value(
self.loc, "multiple", "multiple", timeout=timeout
)
else:
expect_attribute_to_have_value(self.loc, "multiple", None, timeout=timeout)


class _InputActionBase(_UiBase):
def expect_label(
Expand Down
73 changes: 73 additions & 0 deletions tests/playwright/shiny/inputs/input_selectize/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
from shiny import App, Inputs, Outputs, Session, render, ui

states = {
"East Coast": {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"},
"West Coast": {"WA": "Washington", "OR": "Oregon", "CA": "California"},
"Midwest": {"MN": "Minnesota", "WI": "Wisconsin", "IA": "Iowa"},
}

state_without_groups = {"NY": "New York", "NJ": "New Jersey", "CT": "Connecticut"}
state_without_keys = ["New York", "New Jersey", "Connecticut"]

app_ui = ui.page_fluid(
ui.input_selectize(
"state1",
"Choose a state:",
states,
multiple=True,
),
ui.input_selectize(
"state2",
"Selectize Options",
states,
multiple=True,
options=(
{
"placeholder": "Enter text",
"render": ui.js_eval(
'{option: function(item, escape) {return "<div><strong>Select " + escape(item.label) + "</strong></div>";}}'
),
"create": True,
}
),
),
ui.input_selectize(
"state3",
"Single Selectize",
state_without_groups,
multiple=False,
options={"plugins": ["clear_button"]},
),
ui.input_selectize(
"state4",
"Simple Selectize",
state_without_keys,
multiple=False,
),
ui.hr(),
ui.output_code("value1"),
ui.output_code("value2"),
ui.output_code("value3"),
ui.output_code("value4"),
)


def server(input: Inputs, output: Outputs, session: Session):
@render.code
def value1():
return str(input.state1())

@render.code
def value2():
return str(input.state2())

@render.code
def value3():
return str(input.state3())

@render.code
def value4():
return str(input.state4())


app = App(app_ui, server)
116 changes: 116 additions & 0 deletions tests/playwright/shiny/inputs/input_selectize/test_input_selectize.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
from playwright.sync_api import Page, expect

from shiny.playwright import controller
from shiny.run import ShinyAppProc


def test_input_selectize_kitchen(page: Page, local_app: ShinyAppProc) -> None:
page.goto(local_app.url)

state1 = controller.InputSelectize(page, "state1")
state2 = controller.InputSelectize(page, "state2")
state3 = controller.InputSelectize(page, "state3")
state4 = controller.InputSelectize(page, "state4")

value1 = controller.OutputCode(page, "value1")
value2 = controller.OutputCode(page, "value2")
value3 = controller.OutputCode(page, "value3")
value4 = controller.OutputCode(page, "value4")

# -------------------------

expect(state1.loc_label).to_have_text("Choose a state:")
state1.expect_label("Choose a state:")

state1.expect_choices(["NY", "NJ", "CT", "WA", "OR", "CA", "MN", "WI", "IA"])
state1.expect_choice_labels(
[
"New York",
"New Jersey",
"Connecticut",
"Washington",
"Oregon",
"California",
"Minnesota",
"Wisconsin",
"Iowa",
]
)
state1.expect_choice_groups(["East Coast", "West Coast", "Midwest"])

state1.expect_multiple(True)

state1.set(["IA", "CA"])

state1.expect_selected(["IA", "CA"])

value1.expect_value("('IA', 'CA')")

# -------------------------

state2.expect_label("Selectize Options")

state2.expect_choices(["NY", "NJ", "CT", "WA", "OR", "CA", "MN", "WI", "IA"])
state2.expect_choice_labels(
[
"Select New York",
"Select New Jersey",
"Select Connecticut",
"Select Washington",
"Select Oregon",
"Select California",
"Select Minnesota",
"Select Wisconsin",
"Select Iowa",
]
)
state2.expect_choice_groups(["East Coast", "West Coast", "Midwest"])

state2.expect_multiple(True)

state2.set(["IA", "CA"])

state2.expect_selected(["IA", "CA"])
value2.expect_value("('IA', 'CA')")

# -------------------------

state3.expect_label("Single Selectize")

state3.expect_choices(["NY", "NJ", "CT"])

state3.expect_choice_labels(
[
"New York",
"New Jersey",
"Connecticut",
]
)

state3.expect_multiple(False)

state3.set(["NJ"])

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

# -------------------------

state4.expect_label("Simple Selectize")

state4.expect_choices(["New York", "New Jersey", "Connecticut"])

state4.expect_choice_labels(
[
"New York",
"New Jersey",
"Connecticut",
]
)

state4.expect_multiple(False)

state4.set(["New York"])

state4.expect_selected(["New York"])
value4.expect_value("New York")
Loading