diff --git a/src/kleinanzeigen_bot/__init__.py b/src/kleinanzeigen_bot/__init__.py index 63452de..e912f6e 100644 --- a/src/kleinanzeigen_bot/__init__.py +++ b/src/kleinanzeigen_bot/__init__.py @@ -1068,24 +1068,31 @@ class KleinanzeigenBot(WebScrapingMixin): 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(_("Failed to set attribute '%s'") % special_attribute_key) from ex try: elem_id:str = str(special_attr_elem.attrs.id) if special_attr_elem.local_name == "select": - LOG.debug("Attribute field '%s' seems to be a select...", special_attribute_key) + LOG.debug(_("Attribute field '%s' seems to be a select..."), special_attribute_key) await self.web_select(By.ID, elem_id, special_attribute_value_str) elif special_attr_elem.attrs.type == "checkbox": - LOG.debug("Attribute field '%s' seems to be a checkbox...", special_attribute_key) + 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) + 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) except TimeoutError as ex: - LOG.debug("Attribute field '%s' is not of kind radio button.", special_attribute_key) - raise TimeoutError(f"Failed to set special attribute [{special_attribute_key}]") from ex + LOG.debug(_("Failed to set attribute field '%s' via known input types."), special_attribute_key) + raise TimeoutError(_("Failed to set attribute '%s'") % special_attribute_key) from ex LOG.debug("Successfully set attribute field [%s] to [%s]...", special_attribute_key, special_attribute_value_str) async def __set_shipping(self, ad_cfg:Ad, mode:AdUpdateStrategy = AdUpdateStrategy.REPLACE) -> None: diff --git a/src/kleinanzeigen_bot/resources/translations.de.yaml b/src/kleinanzeigen_bot/resources/translations.de.yaml index c1f7f35..ece3787 100644 --- a/src/kleinanzeigen_bot/resources/translations.de.yaml +++ b/src/kleinanzeigen_bot/resources/translations.de.yaml @@ -121,10 +121,12 @@ kleinanzeigen_bot/__init__.py: "Setting special attribute [%s] to [%s]...": "Setze spezielles Attribut [%s] auf [%s]..." "Successfully set attribute field [%s] to [%s]...": "Attributfeld [%s] erfolgreich auf [%s] gesetzt..." "Attribute field '%s' could not be found.": "Attributfeld '%s' konnte nicht gefunden werden." + "Failed to set attribute '%s'": "Fehler beim Setzen des Attributs '%s'" "Attribute field '%s' seems to be a select...": "Attributfeld '%s' scheint ein Auswahlfeld zu sein..." - "Attribute field '%s' is not of kind radio button.": "Attributfeld '%s' ist kein Radiobutton." + "Failed to set attribute field '%s' via known input types.": "Fehler beim Setzen des Attributfelds '%s' über bekannte Eingabetypen." "Attribute field '%s' seems to be a checkbox...": "Attributfeld '%s' scheint eine Checkbox zu sein..." "Attribute field '%s' seems to be a text input...": "Attributfeld '%s' scheint ein Texteingabefeld zu sein..." + "Attribute field '%s' seems to be a Combobox (i.e. text input with filtering dropdown)...": "Attributfeld '%s' scheint eine Combobox zu sein (d.h. Texteingabefeld mit Dropdown-Filter)..." download_ads: "Scanning your ad overview...": "Scanne Anzeigenübersicht..." @@ -403,6 +405,14 @@ kleinanzeigen_bot/utils/web_scraping_mixin.py: web_check: "Unsupported attribute: %s": "Nicht unterstütztes Attribut: %s" + web_select: + "Option not found by value or displayed text: %s": "Option nicht gefunden nach Wert oder angezeigtem Text: %s" + + web_select_combobox: + "Combobox input field does not have 'aria-controls' attribute.": "Das Eingabefeld der Combobox hat kein 'aria-controls'-Attribut." + "Combobox missing aria-controls attribute": "Combobox fehlt aria-controls Attribut" + "No matching option found in combobox: '%s'": "Keine passende Option in Combobox gefunden: '%s'" + close_browser_session: "Closing Browser session...": "Schließe Browser-Sitzung..." diff --git a/src/kleinanzeigen_bot/utils/web_scraping_mixin.py b/src/kleinanzeigen_bot/utils/web_scraping_mixin.py index dae5be1..56eb528 100644 --- a/src/kleinanzeigen_bot/utils/web_scraping_mixin.py +++ b/src/kleinanzeigen_bot/utils/web_scraping_mixin.py @@ -969,23 +969,110 @@ class WebScrapingMixin: lambda: self.web_check(selector_type, selector_value, Is.CLICKABLE), timeout = timeout, timeout_error_message = f"No clickable HTML element with selector: {selector_type}='{selector_value}' found" ) - elem = await self.web_find(selector_type, selector_value) - 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; + elem = await self.web_find(selector_type, selector_value, timeout = timeout) + + js_value = json.dumps(selected_value) # safe escaping for JS + try: + await elem.apply(f""" + function (element) {{ + 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 not found by value or displayed text: " + wanted); }} - }} - throw new Error("Option with value {selected_value} not found."); - }} - """) + """) + except Exception as ex: + # Normalize selection failures to TimeoutError + raise TimeoutError(_("Option not found by value or displayed text: %s") % selected_value) from ex await self.web_sleep() return elem + async def web_select_combobox(self, selector_type:By, selector_value:str, selected_value:str | int, timeout:int | float | None = None) -> Element: + """ + Selects an option from a text-input combobox by typing the given value to + filter the dropdown and clicking the first
  • whose visible text matches. + Returns the dropdown