Skip to content
13 changes: 10 additions & 3 deletions src/kleinanzeigen_bot/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1059,9 +1059,13 @@ async def __set_special_attributes(self, ad_cfg:Ad) -> None:
try:
# finding element by name cause id are composed sometimes eg. autos.marke_s+autos.model_s for Modell by cars
special_attr_elem = await self.web_find(By.XPATH, f"//*[contains(@name, '{special_attribute_key}')]")
except TimeoutError as ex:
LOG.debug("Attribute field '%s' could not be found.", special_attribute_key)
raise TimeoutError(f"Failed to set special attribute [{special_attribute_key}] (not found)") from ex
except TimeoutError:
# Trying to find element by ID instead cause sometimes there is NO name attribute...
try:
special_attr_elem = await self.web_find(By.ID, special_attribute_key)
except TimeoutError as ex:
LOG.debug("Attribute field '%s' could not be found.", special_attribute_key)
raise TimeoutError(f"Failed to set special attribute by either ID or Name [{special_attribute_key}] (not found)") from ex

try:
elem_id:str = str(special_attr_elem.attrs.id)
Expand All @@ -1071,6 +1075,9 @@ async def __set_special_attributes(self, ad_cfg:Ad) -> None:
elif special_attr_elem.attrs.type == "checkbox":
LOG.debug("Attribute field '%s' seems to be a checkbox...", special_attribute_key)
await self.web_click(By.ID, elem_id)
elif special_attr_elem.attrs.type == "text" and special_attr_elem.attrs.get("role") == "combobox":
LOG.debug("Attribute field '%s' seems to be a Combobox (i.e. text input with filtering dropdown)...", special_attribute_key)
await self.web_select_combobox(By.ID, elem_id, special_attribute_value_str)
else:
LOG.debug("Attribute field '%s' seems to be a text input...", special_attribute_key)
await self.web_input(By.ID, elem_id, special_attribute_value_str)
Expand Down
72 changes: 64 additions & 8 deletions src/kleinanzeigen_bot/utils/web_scraping_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -848,22 +848,78 @@ async def web_select(self, selector_type:By, selector_value:str, selected_value:
timeout_error_message = f"No clickable HTML element with selector: {selector_type}='{selector_value}' found"
)
elem = await self.web_find(selector_type, selector_value)

js_value = json.dumps(selected_value) # safe escaping for JS
await elem.apply(f"""
function (element) {{
for(let i=0; i < element.options.length; i++)
{{
if(element.options[i].value == "{selected_value}") {{
element.selectedIndex = i;
element.dispatchEvent(new Event('change', {{ bubbles: true }}));
break;
const wanted = String({js_value});

// 1) Try by value
for (let i = 0; i < element.options.length; i++) {{
if (element.options[i].value === wanted) {{
element.selectedIndex = i;
element.dispatchEvent(new Event('change', {{ bubbles: true }}));
return;
}}
}}

// 2) Fallback by displayed text (trimmed)
const needle = wanted.trim();
for (let i = 0; i < element.options.length; i++) {{
const opt = element.options[i];
const shown = (opt.label ?? opt.text ?? opt.textContent ?? '').trim();
if (shown === needle) {{
element.selectedIndex = i;
element.dispatchEvent(new Event('change', {{ bubbles: true }}));
return;
}}
}}
}}
throw new Error("Option with value {selected_value} not found.");

throw new Error("Option not found by value or displayed text: " + wanted);
}}
""")
await self.web_sleep()
return elem

async def web_select_combobox(self, selector_type:By, selector_value:str, selected_value:Any, timeout:int | float = 5) -> Element:
"""
Selects an <li> Optoin of a <input/> HTML Combobox element by visible text.

:param timeout: timeout in seconds
:raises TimeoutError: if element could not be found within time
:raises UnexpectedTagNameException: if element is not a <select> element
"""
input_field = await self.web_find(selector_type, selector_value, timeout = timeout)
await input_field.clear_input()
await input_field.send_keys(str(selected_value))
await self.web_sleep()

# From the Inputfield, get the attribute "aria-controls" which POINTS to the Dropdown ul #id:
dropdown_id = input_field.attrs.get("aria-controls")
if not dropdown_id:
LOG.error("Combobox input field does not have 'aria-controls' attribute to locate dropdown options.")
raise AssertionError("Cannot locate combobox dropdown options.")

dropdown_elem = await self.web_find(By.ID, dropdown_id)
ok = await dropdown_elem.apply("""
function (element) {
// Click on first <li>: because the Input is filtered already...
const li = element.querySelector(':scope > li') ||
(element.firstElementChild && element.firstElementChild.tagName === 'LI' ? element.firstElementChild : null);
if (!li)
return false;

li.click();
return true;
}
""")
if not ok:
LOG.error("No <li> options found in combobox dropdown with id '%s'.", dropdown_id)
raise AssertionError("Cannot locate combobox dropdown options.")

await self.web_sleep()
return dropdown_elem

async def _validate_chrome_version_configuration(self) -> None:
"""
Validate Chrome version configuration for Chrome 136+ security requirements.
Expand Down
Loading