※2020年1月15日に正式リリースされた Chromium 版 Edge には対応していません。Chromium 版 Edge を操作するコードについては次の記事をご参照ください。
[C#] 自動UIテストで遊ぼう:生まれ変わったエッヂのはるかさんがクリスマスイブの予定を読み上げる(WinAppDriver でできること/できないこと)
はじめに
マイクロソフト社の開発した Windows Application Driver (WinAppDriver) を使用すると、Windows 10 上で動くアプリケーションのUIテストを自動化することができます。
Webブラウザの自動操作と言えば、一般的には Selenium WebDriver を使いますが、ふと興味が湧いて Appium + WinAppDriver でも挑戦してみました。
身近なところで、Qiita の「いいね」を検証しましょう。
新たに「いいね」がついていたら成功、ついていなければ失敗とします。
この記事で得られるかもしれない知見
- Selenium WebDriver と WinAppDriver のテストコード差異
- Selenium WebDriver/WinAppDriver で**色を検証**する方法
- WinAppDriver の限界
- 珍しめ以上アクロバティック以下の小技
-
SendKeys
でUSキーボードしかサポートされない('=' は '^' になる)問題の対策 -
SendKeys
で 入力候補まで送られてしまう問題の対策(ここではアドレス欄)
-
- Microsoft Edge(EdgeHTML/Chromium)のUIオートメーション対応に関するプチ情報
シナリオ
- Qiita にログインします。
- 右上の通知ボックスで新着通知の有無を確認します。
- 新着通知があったら通知一覧ページを開きます1。
- 新しい順に10件ずつ表示されるので、新着件数分の内訳を確認します。
期待結果
新たに「いいね」がついていること。
想定パターン
- 新着通知がない。
- 新着通知はあるが、その中に「いいね」はない。
- 新着通知の中に「いいね」がある。
検証環境
テスト実行環境
- Windows 10
- Microsoft Edge(EdgeHTML)
- Visual Studio
- NuGet パッケージ
- Selenium WebDriver API .NET Bindings v4.0.0-alpha04
- Selenium.Support (Selenium WebDriver .NET Bindings support classes) v4.0.0-alpha04
- [Appium.WebDriver (appium-dotnet-driver)] (https://www.nuget.org/packages/Appium.WebDriver/) v4.1.1
- DotNetSeleniumExtras.WaitHelpers v3.11.0
UI実行環境(テスト実行環境と同居可)
- Microsoft Edge(EdgeHTML)
- Microsoft WebDriver (Chromium Edge 向け)
-
WinAppDriver (Windows Application Driver) v1.1
※Windows の「開発者モード」を有効にしたうえで、管理者権限で起動しておきます。
テストコード
コードは C# で記述しますが、ほかの言語でも同じロジックで実現できると思います。
Selenium WebDriver 版
一般的な Selenium WebDriver の方から実装していきます。
Edge で Qiita に自動ログインできるようにしておきます。
実行前に Edge を開いていると Url
設定時に NoSuchWindowException
が発生してしまいますので閉じてから実行しましょう。
テキストで検証
まず EdgeDriver
を使用して Qiita のトップページを開きます。
次に通知ボックスの div
要素を見つけ、そのテキストから新着件数を取得します。
ここで件数が 0 なら「いいね」はありませんので検証は失敗となります。
新着通知があった場合、その中に「いいね」が含まれていることを検証します。
10件までは通知件数クリックからドロップダウンで確認できますが、それを超えることもありえます。
通知一覧ページを開いて確認することにしましょう。
通知一覧ページは10件ごとですので、新着件数がそれを超えるときは page クエリパラメータを指定して次ページ以降を確認していきます。
「いいね」の場合は <span class="bold">いいね</span>
で強調表示されます。
それがついた通知が1つでもあれば検証成功です。
コードを見てみましょう。
[TestMethod]
public void SomebodyShouldGiveMeLike_WebDriver_ByText()
{
const string QiitaRootUrl = "https://qiita.com/";
const string QiitaNotificationsUrl = "https://qiita.com/notifications";
var edgeDriver = new EdgeDriver();
// Qiita にアクセス
try
{
edgeDriver.Url = QiitaRootUrl;
}
catch (NoSuchWindowException ex)
{
Console.WriteLine(ex.Message);
Assert.Fail("Edge を閉じてから再試行してください。");
}
new WebDriverWait(edgeDriver, TimeSpan.FromSeconds(20))
.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.UrlToBe(QiitaRootUrl));
// 新着はあるか
var notificationDiv = edgeDriver.FindElement(By.ClassName("st-Header_notifications"));
int notificationCount = int.Parse(notificationDiv.Text);
if (notificationCount == 0)
{
Assert.Fail("新着通知はありません。");
return;
}
Console.WriteLine($"新着通知が{notificationCount}件ありました。");
// 新着の中に「いいね」はあるか
const int PageItemCount = 10;
int newLikeCount = 0;
for (int pageIndex = 0; pageIndex <= notificationCount / PageItemCount; pageIndex++)
{
string url = QiitaNotificationsUrl + $"?page={pageIndex + 1}";
edgeDriver.Url = url;
new WebDriverWait(edgeDriver, TimeSpan.FromSeconds(20))
.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.UrlToBe(url));
newLikeCount += edgeDriver.FindElements(By.CssSelector("li.notification"))
.Take(notificationCount - (pageIndex * PageItemCount))
.Count(notification =>
{
// <span class="bold">いいね</span> を探す。
return notification.FindElements(By.CssSelector("span.bold"))
.Any(e => e.Text == "いいね");
});
}
if (newLikeCount == 0)
{
Assert.Fail("「いいね」はありませんでした。");
}
Console.WriteLine($"{newLikeCount}件の「いいね」がつきました。");
}
起動したブラウザを検証後に閉じたいときは、EdgeDriver
を Close
または Dispose
します。
色で検証
新着があるかどうは色でも判定できます。
[TestMethod]
public void SomebodyShouldGiveMeLike_WebDriver_ByColor()
{
const string QiitaUrl = "https://qiita.com/";
var arrivalBackgroundArgb = ColorTranslator.FromHtml("#E14B22").ToArgb();
var edgeDriver = new EdgeDriver();
// Qiita にアクセス
try
{
edgeDriver.Url = QiitaUrl;
}
catch (NoSuchWindowException ex)
{
Console.WriteLine(ex.Message);
Assert.Fail("Edge を閉じてから再試行してください。");
}
new WebDriverWait(edgeDriver, TimeSpan.FromSeconds(20))
.Until(SeleniumExtras.WaitHelpers.ExpectedConditions.UrlToBe(QiitaUrl));
// 新着はあるか
var notificationDiv = edgeDriver.FindElement(By.ClassName("st-Header_notifications"));
string backgroundColorCssValue = notificationDiv.GetCssValue("background-color");
var match = Regex.Match(backgroundColorCssValue, @"rgb\((\d+),\s*(\d+),\s*(\d+)\)");
Assert.IsTrue(match.Success, $"想定と違う色書式:{backgroundColorCssValue}");
var backgroundColor = Color.FromArgb(int.Parse(match.Groups[1].Value), int.Parse(match.Groups[2].Value), int.Parse(match.Groups[3].Value));
if (backgroundColor.ToArgb() != arrivalBackgroundArgb)
{
Assert.Fail("新着通知はありません。");
}
// 新着の中に「いいね」はあるかは色では判定できません。
// 上のテキストによる検証と同じになるので省略します。
}
div
要素のスタイル background-color
を RemoteWebElement.GetCssValue
メソッドで参照すると、"rgb(88,29,13)" のような書式で色指定が返ってきます。
ここから正規表現を使ってRGB値をそれぞれ抜き出し、Color
構造体オブジェクトに変換しています。
同一色かどうかは ToArgb()
した結果の int
値で判定します。
理由については別記事『[.NET] コードを見直したくなる「値型」等価判定の思わぬ落とし穴(特殊編)』をご参照ください。
ここまでは比較的普通ですね。
ここからが冒険です。
WinAppDriver 版
一般的には WinAppDriver はデスクトップアプリケーションを扱うためのものですが、Microsoft Edge(ここでは EdgeHTML 版を使用)はHTMLドキュメント部分も MSAA (Microsoft Active Accessibility) の後継である「UIオートメーション」に対応しており、WinAppDriver で扱うことができます。
※Windows 版 Chrome の場合、「UIオートメーション」対応は「非常に限定的」とされていますが、MSAA の一部である IAccessible や、MSAA を補完した IAccessible2 に対応しており2、v78.0.3904.97 で試したところ、Edge と同レベルのことは実現できそうでした。
テキストで検証
通知ボックスをダイレクトに取得する情報が WindowsElement
から得られず、この点に苦労しました。
左隣りの [投稿する] リンクが Text
プロパティに収められた href
属性値から特定できたので、そこから相対位置で取得することにします。
XPath の following-sibling
は効きませんので、親要素から FindElementsByXPath("*/*")
で全子要素コレクションを取得し、[投稿する] の次の要素を通知ボックスとして取得しています。
Selenium WebDriver と違い、WinAppDriver ではHTML要素は取得できません。
通知が「いいね」であることは、
」 に いいね しました。
という文字列を含んでいるかどうかで判定しました。
次の小技もコード内に含まれています。
- 入力候補が適用されないよう、消してから検索実行する。
- USキーボードレイアウトしかサポートされておらず、'=' は '^' に変わってしまうのでASCIIコードで入力(『ASCII 文字の挿入』参考)。
[TestMethod]
public void SomebodyShouldGiveMeLike_WinAppDriver_ByText()
{
const string EdgeAppId = "Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge";
const string QiitaUrl = "https://qiita.com/";
const string QiitaNotificationsUrl = "https://qiita.com/notifications";
var remoteAddress = new Uri("http://127.0.0.1:4723");
var edgeCapabilities = new AppiumOptions();
edgeCapabilities.AddAdditionalCapability("app", EdgeAppId);
var edgeWinDriver = new WindowsDriver<WindowsElement>(remoteAddress, edgeCapabilities);
// Qiita にアクセス
var addressEditBox = edgeWinDriver.FindElementByAccessibilityId("addressEditBox");
addressEditBox.Clear();
addressEditBox.SendKeys(QiitaUrl);
// 入力候補を消して検索実行
Thread.Sleep(TimeSpan.FromSeconds(2));
addressEditBox.SendKeys(Keys.Delete + Keys.Enter);
Thread.Sleep(TimeSpan.FromSeconds(5));
// 新着はあるか
var qiitaPane = edgeWinDriver.FindElementByXPath(
$".//Pane[@ClassName=\"Internet Explorer_Server\"][@Name=\"{QiitaUrl}\"]/Pane[@Name=\"Qiita\"]");
// [投稿する] の次の要素が通知ボックス
// ※ダイレクトに特定するための情報が得られない。
// Inspect.exe で見ると Name が "投稿する" だが、テスト実行時の Text 値は href 属性値だった。
// following-sibling も効かないので全子要素コレクションから Skip 取得。
var notificationGroup = qiitaPane.FindElementsByXPath("*/*").Cast<WindowsElement>()
.SkipWhile(e => e.Text != "https://qiita.com/drafts/new").Skip(1)
.First();
int notificationCount = int.Parse(notificationGroup.FindElementByTagName("Text").Text);
if (notificationCount == 0)
{
Assert.Fail("新着通知はありません。");
}
Console.WriteLine($"新着通知が{notificationCount}件ありました。");
// 新着の中に「いいね」はあるか
const int PageItemCount = 10;
int newLikeCount = 0;
// ※USキーボードレイアウトしかサポートされておらず、'=' は '^' に変わってしまうのでASCIIコードで入力。
// Keys.Shift + "-" + Keys.Shift でも '=' になる。
string usKeyboardEqual = Keys.Alt + Keys.NumberPad6 + Keys.NumberPad1 + Keys.Alt;
for (int pageIndex = 0; pageIndex <= notificationCount / PageItemCount; pageIndex++)
{
string pageUrl = $"{QiitaNotificationsUrl}?page{usKeyboardEqual}{pageIndex + 1}";
addressEditBox.Clear();
addressEditBox.SendKeys(pageUrl);
Thread.Sleep(TimeSpan.FromSeconds(2));
addressEditBox.SendKeys(Keys.Delete + Keys.Enter);
Thread.Sleep(TimeSpan.FromSeconds(5));
var notifications = edgeWinDriver.FindElementsByXPath("//Pane[@Name=\"通知一覧 - Qiita\"]/List/ListItem");
newLikeCount += notifications
.Take(notificationCount - (pageIndex * PageItemCount))
.Count(n => n.Text.Contains("」 に いいね しました。"));
}
if (newLikeCount == 0)
{
Assert.Fail("「いいね」はありませんでした。");
}
Console.WriteLine($"{newLikeCount}件の「いいね」がつきました。");
}
色で検証
Selenium WebDriver と違い、HTMLのスタイルは参照できません。
RemoteWebDriver
にスクリーンショットを取得するメソッドが用意されていますので、レンダリングされた色から判定してみましょう。
これが RemoteWebDriver.GetScreenshot()
メソッドでキャプチャした通知画像です。
2桁とかあると格好よかったのですが、これでも1日待ちました。
真っ赤に見えたのは背景色との対比のせいで、実際は意外とオレンジなんですね。
ブラウザのデバッグツールで色指定を確認すると "#E14B22" でした。
ウィンドウのスクリーンショットから通知ボックス部分を抜き出し、そこに "#E14B22" のピクセルが含まれているかどうかを検証します。
[TestMethod]
public void SomebodyShouldGiveMeLike_WinAppDriver_ByColor()
{
const string EdgeAppId = "Microsoft.MicrosoftEdge_8wekyb3d8bbwe!MicrosoftEdge";
const string QiitaUrl = "https://qiita.com/";
const string QiitaNewPostHref = "https://qiita.com/drafts/new";
var arrivalBackgroundArgb = ColorTranslator.FromHtml("#E14B22").ToArgb();
var remoteAddress = new Uri("http://127.0.0.1:4723");
var edgeCapabilities = new AppiumOptions();
edgeCapabilities.AddAdditionalCapability("app", EdgeAppId);
var edgeWinDriver = new WindowsDriver<WindowsElement>(remoteAddress, edgeCapabilities);
// Qiita にアクセス
var addressEditBox = edgeWinDriver.FindElementByAccessibilityId("addressEditBox");
addressEditBox.Clear();
addressEditBox.SendKeys(QiitaUrl);
// 入力候補を消して検索実行
Thread.Sleep(TimeSpan.FromSeconds(2));
addressEditBox.SendKeys(Keys.Delete + Keys.Enter);
Thread.Sleep(TimeSpan.FromSeconds(5));
// 新着はあるか
var qiitaPane = edgeWinDriver.FindElementByXPath(
$"//Pane[@ClassName=\"Internet Explorer_Server\"][@Name=\"{QiitaUrl}\"]/Pane[@Name=\"Qiita\"]");
// [投稿する] の次の要素が通知ボックス
var notificationGroup = qiitaPane.FindElementsByXPath("*/*").Cast<WindowsElement>()
.SkipWhile(e => e.Text != "https://qiita.com/drafts/new").Skip(1)
.First();
var notificationRect = new Rectangle(notificationGroup.Location, notificationGroup.Size);
var edgeScreen = edgeWinDriver.GetScreenshot();
using (var edgeScreenStream = new MemoryStream(edgeScreen.AsByteArray))
using (var edgeScreenBitmap = new Bitmap(edgeScreenStream))
using (var notificationBitmap = edgeScreenBitmap.Clone(notificationRect, edgeScreenBitmap.PixelFormat))
{
// 左上から右下に向けて赤を探す(背景が赤なら見つかるはず)
bool hasNew = Enumerable.Range(0, Math.Min(notificationBitmap.Width, notificationBitmap.Height))
.Any(xy => notificationBitmap.GetPixel(xy, xy).ToArgb() == arrivalBackgroundArgb);
if (!hasNew)
{
Assert.Fail("新着通知はありません。");
}
}
// 新着の中に「いいね」はあるかは色では判定できません。
// 上のテキストによる検証と同じになるので省略します。
}
できましたね。
「いいね」はあったでしょうか。
Microsoft Edge のUIオートメーション対応
Edge(EdgeHTML)はHTMLドキュメント部分も限定的ながら「UIオートメーション」に対応しており、上のように WinAppDriver で扱うことができましたが、Windows フォームなどデスクトップアプリケーションと比べると、Inspect.exe に出てくる Name に Appium + WinAppDriver でアクセスできない、AccessibilityId が提供されないなど、機能的に物足りない感は否めず、実装に苦労しました。
あまり実用的とは言えませんね。
HTMLドキュメントの操作には素直に Selenium WebDriver を使用した方がよいでしょう。
なお、レンダリングエンジンが Chromium に変わった後の Edge でUI自動化がどのようにサポートされるのか、気になるところですが、ベータ版を Inspect.exe で確認したところ、Chrome 同様に IAccessible2 を通してサポートされているようでした(レンダリングエンジンが同じなのでそれはそうですね)。
※2019年11月4日、Chromium エンジンを採用した Microsoft Edge の安定版が 2020年1月15日にリリースされる予定と発表されました。
Getting your sites ready for the new Microsoft Edge - Microsoft Edge Blog