From da5c662db68a03a6f82bb14d7a5b7930de0528af Mon Sep 17 00:00:00 2001 From: Wout Date: Mon, 6 Apr 2026 13:41:31 +0200 Subject: [PATCH 1/5] Add helper to wait for app server to be live In most cases this will be negligible, but it's another safety. --- src/lucky_flow.cr | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/lucky_flow.cr b/src/lucky_flow.cr index 39a3cb4..d70a15a 100644 --- a/src/lucky_flow.cr +++ b/src/lucky_flow.cr @@ -45,6 +45,23 @@ class LuckyFlow LuckyFlow::Registry.current_driver.try(&.reset) end + def self.wait_for_server(timeout : Time::Span = 5.seconds) + uri = URI.parse(settings.base_uri) + host = uri.host || "127.0.0.1" + port = uri.port || 3000 + retry_interval = 100.milliseconds + retries = (timeout / retry_interval).to_i + + retries.times do + TCPSocket.open(host, port) { } + return + rescue IO::Error + sleep(retry_interval) + end + + raise "Server at #{host}:#{port} did not start within #{timeout}" + end + def visit(path : String) driver.visit("#{settings.base_uri}#{path}") end From 5fcfbd5be665d8a8b2e94cf800744281533ad048 Mon Sep 17 00:00:00 2001 From: Wout Date: Mon, 6 Apr 2026 13:54:54 +0200 Subject: [PATCH 2/5] Make sure session retry limit is considered in full Becuase the `@retry_limit` time span was initialised with the object, the time had already partially elapsed becaus of any setups that would happen afterwards. This fix makes sure the full time span is available from the moment the session is started. --- src/lucky_flow/selenium/driver.cr | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/lucky_flow/selenium/driver.cr b/src/lucky_flow/selenium/driver.cr index 03cce99..e6687f0 100644 --- a/src/lucky_flow/selenium/driver.cr +++ b/src/lucky_flow/selenium/driver.cr @@ -1,5 +1,6 @@ abstract class LuckyFlow::Selenium::Driver < LuckyFlow::Driver - @retry_limit : Time = 2.seconds.from_now + SESSION_RETRY_LIMIT = 2.seconds + @driver : ::Selenium::Driver? @capabilities : ::Selenium::Capabilities @@ -82,18 +83,16 @@ abstract class LuckyFlow::Selenium::Driver < LuckyFlow::Driver end private def start_session : ::Selenium::Session - driver.create_session(@capabilities) - rescue e : IO::Error - retry_start_session(e) - end + retry_interval = 100.milliseconds + retries = (SESSION_RETRY_LIMIT / retry_interval).to_i - private def retry_start_session(e) - if Time.utc <= @retry_limit - sleep(100.milliseconds) - start_session - else - raise e + retries.times do + return driver.create_session(@capabilities) + rescue IO::Error + sleep(retry_interval) end + + driver.create_session(@capabilities) end private def find_elements( From 156e663e6c7ddbb55989a44a9eb8fe50cfaca048 Mon Sep 17 00:00:00 2001 From: Wout Date: Mon, 6 Apr 2026 13:56:52 +0200 Subject: [PATCH 3/5] Wait for DOM ready state This is just a precaution because in most cases the DOM should be ready --- src/lucky_flow/selenium/driver.cr | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/lucky_flow/selenium/driver.cr b/src/lucky_flow/selenium/driver.cr index e6687f0..196f86e 100644 --- a/src/lucky_flow/selenium/driver.cr +++ b/src/lucky_flow/selenium/driver.cr @@ -16,6 +16,18 @@ abstract class LuckyFlow::Selenium::Driver < LuckyFlow::Driver def visit(url : String) session.navigate_to(url) + wait_for_ready + end + + private def wait_for_ready + retry_interval = 10.milliseconds + retries = (LuckyFlow.settings.stop_retrying_after / retry_interval).to_i + retries.times do + ready = session.document_manager.execute_script("return document.readyState;") + return if ready == "complete" + + sleep(retry_interval) + end end def window_size : NamedTuple(width: Int64?, height: Int64?) From 38c8a82c689bf43265926282c22a03c8f4c6faef Mon Sep 17 00:00:00 2001 From: Wout Date: Mon, 6 Apr 2026 13:58:30 +0200 Subject: [PATCH 4/5] Make sure stale state is cleared Cookies were already cleared, but any remants form the previous DOM could still be present. This makes sure the DOM starts fresh every time. --- src/lucky_flow/selenium/driver.cr | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lucky_flow/selenium/driver.cr b/src/lucky_flow/selenium/driver.cr index 196f86e..7ed09ac 100644 --- a/src/lucky_flow/selenium/driver.cr +++ b/src/lucky_flow/selenium/driver.cr @@ -82,7 +82,10 @@ abstract class LuckyFlow::Selenium::Driver < LuckyFlow::Driver end def reset : Nil - @session.try &.cookie_manager.delete_all_cookies + @session.try do |session| + session.navigate_to("about:blank") + session.cookie_manager.delete_all_cookies + end end def stop From bf759a47d8e4d076e46d9f43b2dd451747189973 Mon Sep 17 00:00:00 2001 From: Wout Date: Sat, 18 Apr 2026 13:52:01 +0200 Subject: [PATCH 5/5] Ensure retry on "stale element" errors --- src/lucky_flow/find_element.cr | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lucky_flow/find_element.cr b/src/lucky_flow/find_element.cr index 94347c9..9ff2f3e 100644 --- a/src/lucky_flow/find_element.cr +++ b/src/lucky_flow/find_element.cr @@ -44,13 +44,16 @@ class LuckyFlow::FindElement private def find_matching_elements : Array(LuckyFlow::Element) self.tries += 1 driver.find_css(selector).select do |element| - text_to_check_for = inner_text - if text_to_check_for + if text_to_check_for = inner_text element.text.includes?(text_to_check_for) else true end end + rescue ex : ::Selenium::Error + raise ex unless ex.message.try(&.includes?("stale element")) + + [] of LuckyFlow::Element end private def raise_element_not_found_error