diff --git a/src/kleinanzeigen_bot/__init__.py b/src/kleinanzeigen_bot/__init__.py index d669a22..0339159 100644 --- a/src/kleinanzeigen_bot/__init__.py +++ b/src/kleinanzeigen_bot/__init__.py @@ -1006,6 +1006,8 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904 if getattr(self, "page", None) is not None: LOG.debug("Current page URL after opening homepage: %s", self.page.url) + await self._dismiss_consent_banner() + state = await self.get_login_state() if state == LoginState.LOGGED_IN: LOG.info("Already logged in as [%s]. Skipping login.", self.config.login.username) @@ -1085,6 +1087,21 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904 # GDPR banner not shown within timeout. pass + async def _dismiss_consent_banner(self) -> None: + """Dismiss the GDPR/TCF consent banner if it is present. + + This banner can appear on any page navigation (not just after login) and blocks + all form interaction until dismissed. Uses a short timeout to avoid slowing down + the flow when the banner is already gone. + """ + try: + banner_timeout = self._timeout("quick_dom") + await self.web_find(By.ID, "gdpr-banner-accept", timeout = banner_timeout) + LOG.debug("Consent banner detected, clicking 'Alle akzeptieren'...") + await self.web_click(By.ID, "gdpr-banner-accept") + except TimeoutError: + pass # Banner not present; nothing to dismiss + async def _auth_probe_login_state(self) -> LoginState: """Probe an auth-required endpoint to classify login state. @@ -1613,6 +1630,8 @@ class KleinanzeigenBot(WebScrapingMixin): # noqa: PLR0904 LOG.info("Updating ad '%s'...", ad_cfg.title) await self.web_open(f"{self.root_url}/p-anzeige-bearbeiten.html?adId={ad_cfg.id}") + await self._dismiss_consent_banner() + if loggers.is_debug(LOG): LOG.debug(" -> effective ad meta:") YAML().dump(ad_cfg.model_dump(), sys.stdout) diff --git a/tests/unit/test_init.py b/tests/unit/test_init.py index 8432e0a..18e90c3 100644 --- a/tests/unit/test_init.py +++ b/tests/unit/test_init.py @@ -704,6 +704,7 @@ class TestKleinanzeigenBotAuthentication: patch("kleinanzeigen_bot.ainput", new_callable = AsyncMock) as mock_ainput, ): # Mock the sequence of web_find calls: + # 0. Consent banner not found (in _dismiss_consent_banner, before login state check) # First login attempt: # 1. Captcha iframe found (in check_and_wait_for_captcha) # 2. Phone verification not found (in handle_after_login_logic) @@ -715,6 +716,7 @@ class TestKleinanzeigenBotAuthentication: # 7. Email verification not found (in handle_after_login_logic) # 8. GDPR banner not found (in handle_after_login_logic) mock_find.side_effect = [ + TimeoutError(), # Consent banner (before login state check) AsyncMock(), # Captcha iframe (first login) TimeoutError(), # Phone verification (first login) TimeoutError(), # Email verification (first login) @@ -731,7 +733,7 @@ class TestKleinanzeigenBotAuthentication: await test_bot.login() # Verify the complete flow - assert mock_find.call_count == 8 # Exactly 8 web_find calls + assert mock_find.call_count == 9 # 1 consent banner + 8 original web_find calls assert mock_ainput.call_count == 2 # Two captcha prompts assert mock_input.call_count == 6 # Two login attempts with username, clear password, and set password assert mock_click.call_count == 2 # Two submit button clicks