Source code for dal_alight.test
"""Test helpers for dal_alight — mirrors dal_select2.test without select2."""
import time
[docs]
class AlightStory:
"""CSS selectors and wait logic for the autocomplete-light web component.
Drop-in replacement for ``Select2Story``. Mix into
``AutocompleteTestCase`` subclasses instead of ``Select2Story``.
HTML structure produced by ``AlightWidgetMixin``::
<autocomplete-select>
<select slot="select" id="id_{field}">…</select>
<div slot="deck">
<div data-value="1">Label<span class="clear">×</span></div>
</div>
<autocomplete-select-input slot="input" url="…">
<input name="{field}-input" slot="input" class="vTextField" />
</autocomplete-select-input>
</autocomplete-select>
The dropdown box is appended to ``<body>`` by the component::
<div class="autocomplete-light-box">
<div data-value="1">Label</div>
<div data-create data-value="foo">Create "foo"</div>
</div>
"""
# The text input lives inside the widget, not in a global dropdown.
# This lets InlineSelectOption scope enter_text to the field container.
input_in_field_container = True
# Text input embedded inside the web component.
input_selector = 'autocomplete-select-input input'
# Floating dropdown box appended to body.
dropdown_selector = '.autocomplete-light-box'
# Selectable result items (excludes create / group headers).
option_selector = '.autocomplete-light-box [data-value]:not([data-create])'
# Create-on-the-fly entry only (has data-create attribute).
create_option_selector = '.autocomplete-light-box [data-create]'
# Currently selected value label in the deck (single-select).
label_selector = 'autocomplete-select [slot=deck] [data-value]'
# Deck items (multiple-select).
labels_selector = 'autocomplete-select [slot=deck] [data-value]'
# Clear (×) button inside a deck item.
clear_selector = '.clear'
# Clicking this opens the dropdown (same as input_selector for alight).
widget_selector = 'autocomplete-select-input input'
# The outer web component element.
container_selector = 'autocomplete-select'
[docs]
def get_create_option_selector(self, name):
"""Return a selector for the create option that matches ``name`` exactly.
Using ``[data-value="name"]`` prevents clicking a stale create option
left over from a previous query before the new XHR has returned.
"""
escaped = name.replace('"', '\\"')
return f'.autocomplete-light-box [data-create][data-value="{escaped}"]'
[docs]
def wait_script(self):
"""Wait until all alight web components have finished connectedCallback.
autocomplete-select.connectedCallback fires before its child
autocomplete-select-input's callback (parent-first connection order),
so it schedules a 100 ms retry. We must wait for BOTH elements to
have data-bound before interacting: autocomplete-select installs the
autocompleteChoiceSelected listener only on its own retry, and
clicking an option before that listener exists silently does nothing.
"""
tries = 100
while tries:
try:
result = self.browser.evaluate_script(
"customElements.get('autocomplete-select') !== undefined"
" && !document.querySelector("
"'autocomplete-select-input:not([data-bound]),"
" autocomplete-select:not([data-bound])') "
)
if result:
return result
except Exception:
pass
time.sleep(0.15)
tries -= 1
raise Exception(
'autocomplete-select custom element was not defined after 15 seconds.'
)
[docs]
def toggle_autocomplete_widget(self, selector):
"""Open the autocomplete, ensuring focus fires even if already focused.
For alight, ``focus`` on the text input is what triggers the XHR.
When the input already has focus (e.g. after a previous call in
``refresh_autocomplete``), a plain Selenium click does not re-fire
``focus``. Blurring first resets the focus state so the subsequent
click fires a real ``focus`` event and starts a fresh XHR.
"""
self.browser.execute_script(
"""
var el = document.querySelector(arguments[0]);
if (el && document.activeElement === el) { el.blur(); }
""",
selector,
)
self.click(selector)
[docs]
def clean_label(self, label):
"""Remove the × clear button text from a label string."""
return label.replace('×', '').strip()