ChatGPT.get_last_response() no longer crashes when the response includes web-search result images — the previous img selector grabbed the first <img> in the turn, so a web-search thumbnail was clicked instead of the generated image, the "Save" button never appeared, and wait.until(...) raised an uncaught TimeoutException. The selector is now scoped to [class*="imagegen-image"] img, which matches only the generated-image wrapper and skips the group/search-image thumbnails
Gemini.get_state() rewritten for the latest UI: the send/stop button moved out of [data-node-type="input-area"] and the mic-button container is no longer the source of truth for whether the input has content. State is now derived from the [data-test-id="send-button-container"] element — stop class → GENERATING, missing has-input class → IDLE, aria-disabled="true" → UPLOADING, otherwise TYPING. Works on both desktop and mobile layouts
Gemini.get_last_response() now waits ~1.5 s before clicking the image download button, giving the button time to become responsive after the response finishes rendering
send_message() argument order changed — submit moved to the end of the parameter list. Calls passing submit positionally must be updated; keyword calls are unaffected
query() and simple_query() argument order changed — timeout moved to the end of the parameter list (the two now share an identical parameter order). Calls passing timeout positionally must be updated; keyword calls are unaffected
Docstrings for shared arguments aligned across send_message(), query(), and simple_query() for consistency
ChatGPT.send_message() and Gemini.send_message() signatures aligned with the base Scraper.send_message() (added type hints)
Fluent Scraper methods (those returning self, e.g. open_url(), send_message(), short_wait()) are now annotated with typing.Self, so type checkers preserve the concrete subclass type through chained calls
Gemini file upload updated for the latest UI: the upload-menu button's aria-label changed from Open upload file menu to Upload & tools, and the "Upload files" trigger is now located via the shared images-files-uploader[data-test-id="uploader-images-files-button-advanced"] wrapper so it resolves correctly in both the maximized and narrow/mobile menu layouts
Gemini file upload now restores the patched HTMLInputElement.prototype.click even when an intermediate step fails (best-effort restore in try/finally), preventing the override from leaking into the page without masking the original error
Corrected documentation for ChatGPT login requirements — file upload works without login; only image generation requires a logged-in session
ChatGPT.get_state() now correctly returns GENERATING during image generation — after a recent ChatGPT UI update, the stop button disappears while the image loading skeleton is still visible, causing the state to appear idle prematurely; fixed by additionally checking for [data-testid="image-gen-loading-state"]
ChatGPT.get_last_response() no longer raises a spurious "neither text nor image" error on image-only responses — the <img> tag is now awaited for up to 5 seconds to account for the brief DOM delay after the loading skeleton clears
ChatGPT._detect_login() now uses WebDriverWait with TimeoutException instead of a bare find_element with except Exception, consistent with Gemini's approach
_detect_chrome_version() now raises a clear RuntimeError when Chrome is not found, the subprocess fails, or the version string cannot be parsed — previously crashed with a cryptic TypeError or AttributeError
close() now calls self._temp_dir.cleanup() to release the temporary download directory
setup() browser-close detection now catches WebDriverException instead of bare Exception, and wraps the loop in try/finally to guarantee close() is always called
_get_downloaded_file() now filters out .crdownload partial files to avoid returning incomplete downloads
simple_query() now wraps the query in try/finally so the browser is always closed even if an exception is raised
File existence is now checked before upload in both scrapers — raises FileNotFoundError with a clear message instead of a cryptic driver error
TemporaryDirectory creation moved from __init__ to _initialize_driver() so its lifecycle matches the driver — reusing an instance after close() no longer points Chrome at a deleted download directory, and calling close() twice is now a safe no-op