1 はじめに
少し前になりますが、TinySeleniumVBAユーザーから「どのような手段を尽くしても要素が取得できない。どうしたらよいか」との趣旨による以下の質問がありました。
確認したところ、案の定取得したい要素がシャドウルート内にあったので、機能拡張版のSeleniumVBAの導入を推奨しました。
SeleniumVBAは、2021年11月のSeleniumアップデートでShadow DOM内の要素検索が便利になった(JavaScriptが必須でなくなりFindElementが使用可能になった)ことに対応しています。
2 やりたいこと
ServiceNow様トップページ右上にある「Sign In」の箇所(以下画像の赤枠)をクリックすることを目標とします。
「なんだクリックするだけか」と思われるかもしれませんが、通常の要素検索の方法では「no such element」のエラーが出て要素取得できません。
ログイン画面を出すだけで、実際にログインする訳ではありませんので、どなたでもご確認いただけます。
3 事前準備
(1)シャドウルートの判定
ServiceNow様トップページを開き「Sign In」を右クリックし「検証」からデベロッパーツールで「完全なXPath」をコピーしたところ、以下のとおりになりました。
操作はChromeで行っていますが、Edgeでも同様に確認できます。
【Sign In までの完全なXPath】
/html/body/dps-app//div/header/dps-navigation-header//header/div/div[2]/ul/li[2]/dps-login//div/dps-button//button
パス内に「//」が存在すればシャドウルートと判定します。「//」の左側「dps-app」「dps-navigation-header」「dps-login」がシャドウルート入口です。
結果からお伝えすると「dps-button」はクリックできなかったので対象外としました。試行のうえ上位要素の「dps-login」がクリックできる要素でした。
(2)構造確認
次に、Chromeのソース画面を見ますと、クリックしたい要素が一番下の四角囲み部分となり、その上位層をみると、シャドウルートであることが分かる四角囲みの「#shadow-root(open)」が複数表示されていることが分かります。
#shadow-rootが表示されない場合は、デベロッパーツール上部の歯車ボタンを押し、PreferencesタブのElementsの「Show user agent shadow DOM」の選択肢にチェックを入れます。Edge利用の場合も同様です。
4 ExcelVBAコード
以下のコードを丸ごとコピーして、SeleniumVBA.xlsmに新規の標準モジュールを作成して貼り付けますと実際に動作確認ができます。
もし、SeleniumVBAがまったく初めての方は、こちらの記事を参考に新規の標準モジュール作成まで行ってください。
このコードは「Sign In」クリック後のログイン画面で止まるようにしてあります。ブラウザバックして、元の画面になれば正常に動作しています。
Sub test_shadowroot2()
Dim driver As SeleniumVBA.WebDriver
Dim caps As SeleniumVBA.WebCapabilities
Set driver = SeleniumVBA.New_WebDriver
'============================================
'【事例】Sign Inのリンクをクリックしたい
'完全なXpathをコピーすると以下のとおりとなった
'/html/body/dps-app//div/header/dps-navigation-header//header/div/div[2]/ul/li[2]/dps-login//div/dps-button
'上記の「//」の左部を参考にしてシャドウルートは、dps-app、dps-navigation-header、dps-loginであることが推定できる
'待機処理エラーになる場合 On Error~で対応
'(GetShadowRootの処理完了前にFindElementが実行されるとエラーになるケース)
'============================================
With driver
.StartChrome
Set caps = .CreateCapabilities()
caps.SetDetachBrowser True 'ドライバを終了してもブラウザは開いたまま
.OpenBrowser caps
Dim webElem As WebElement
Dim cnt As Long
'シャドウルートの階層が深いため、順番に潜っていく必要がある
.NavigateTo "https://developer.servicenow.com/"
Set webElem = .FindElement(By.tagName, "dps-app")
cnt = 0
On Error GoTo Err_Wait
Set webElem = webElem.GetShadowRoot.FindElement(By.tagName, "dps-navigation-header")
Set webElem = webElem.GetShadowRoot.FindElement(By.tagName, "dps-login")
On Error GoTo 0
'ようやくSingn Inをクリックできた
webElem.Click
'ドライバ終了(画面はそのまま)
driver.Shutdown
Exit Sub
Err_Wait:
If cnt >= 20 Then Stop '最大1秒待機
.Wait 50
cnt = cnt + 1
Resume
End With
End Sub
SeleniumVBAでは「GetShadowRoot」を利用して、1階層ずつシャドウルートの深層に潜っていくことができます。GetShadowRootを記述したあとには、コード例のように、ピリオドでつなげて必ず「FindElement(By.~」で続けることになります。
要素がクリックできなかった場合は、その上層にある要素ならクリックできる可能性もあります。若干の試行錯誤が入りますが御承知ください。
5 補足事項
(1)待機処理
GetShadowRootの処理に時間がかかることがあり、処理完了前にFindElementが実行されるとno such shadow rootと表示され、要素が取得できずにエラーになる場合があります。暗黙的待機を設定してもGetShadowRootの処理の待機には適用されません。
その対応として「On Error GoTo 〜」で最大1秒の待機処理を入れています。
(2)XPathの利用
シャドウルート内で検索する際のお約束があり、By.Xpathが使用できなくなりますので、それ以外で代用することが必要となります。
今回の例では、By.TagNameでシャドウルートを特定及びクリックしたいリンクを特定しています。もし、シャドウルート内でも複雑な検索をしたい場合は、By.CssSelectorが使えるのでBy.Xpathの代用になると思います。
6 参考記事
出典を明記した詳細な記事がありますので、以下を御参照ください。