From 5d14d7dcaa8333475ca5d1e2571dc0843ff612df Mon Sep 17 00:00:00 2001 From: tsuchiya-yu2 Date: Mon, 24 Aug 2026 17:38:47 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Turbo=20Drive=E3=81=AEDOM=E5=B7=AE?= =?UTF-8?q?=E3=81=97=E6=9B=BF=E3=81=88=E4=B8=AD=E3=81=AB=E8=B5=B7=E3=81=8D?= =?UTF-8?q?=E3=82=8Bsystem=20spec=E3=81=AE=E4=B8=8D=E5=AE=89=E5=AE=9A?= =?UTF-8?q?=E3=81=AA=E5=A4=B1=E6=95=97=E3=82=92=E8=A7=A3=E6=B6=88=E3=81=99?= =?UTF-8?q?=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CIでalarm_contents_specがまれに以下で落ちていた。 Selenium::WebDriver::Error::UnknownError: unknown error: unhandled inspector error: {"code":-32000,"message":"Node with given id does not belong to the document"} フォーム送信後のリダイレクトをTurbo Driveが処理する際、bodyの差し替えが 終わる前にCapybaraがhave_contentの問い合わせを投げると、chromedriverが 参照していたノードが旧ドキュメント側に取り残されてこのエラーになる。 差し替えが終われば解消する一過性のものだが、Capybaraが再試行する invalid_element_errorsにUnknownErrorは含まれないため、待機時間を使い切る ことなく即失敗していた。 そこでCapybara::Node::Base#catch_error?をprependで拡張し、このメッセージを 持つUnknownErrorに限りStaleElementReferenceErrorなどと同じ再試行対象として 扱うようにした。待機時間内に差し替えが完了すれば成功し、超過すれば元の例外が そのまま送出されるので、本物のエラーを握り潰すことはない。 specごとにsleepやwaitを足す案は採らなかった。同じ競合はTurboで遷移する すべてのsystem specで起こりうるため、発生箇所ごとの対処では漏れが出る。 --- spec/support/capybara.rb | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb index f4462f0..8e6132e 100644 --- a/spec/support/capybara.rb +++ b/spec/support/capybara.rb @@ -74,3 +74,32 @@ sleep 0.1 end end + +# Turbo DriveがDOMを差し替えている最中にCapybaraが問い合わせると、chromedriverが +# `unknown error: unhandled inspector error: {"code":-32000,"message":"Node with +# given id does not belong to the document"}` を返すことがある。差し替えが終われば +# 解消する一過性のエラーだが、Capybaraの再試行対象(invalid_element_errors)に +# UnknownErrorは含まれないため、待機せずそのまま失敗してしまう。 +# StaleElementReferenceErrorなどと同じく再試行の対象に加えて、待ち時間内であれば +# 差し替え完了後に成功できるようにする。なお待ち時間を過ぎれば元の例外がそのまま +# 送出されるので、本物のエラーを握り潰すことはない。 +module RetryTransientDocumentNodeError + TRANSIENT_MESSAGE = 'does not belong to the document'.freeze + + protected + + def catch_error?(error, errors = nil) + return true if transient_document_node_error?(error) + + super + end + + private + + def transient_document_node_error?(error) + error.is_a?(Selenium::WebDriver::Error::UnknownError) && + error.message.to_s.include?(TRANSIENT_MESSAGE) + end +end + +Capybara::Node::Base.prepend(RetryTransientDocumentNodeError)