Help us understand the problem. What is going on with this article?

ブラウザ操作は Selenium? いえ WinAppDriver でも ―自動UIテストで遊ぼう:Qiita に「いいね」がついてたら

※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 の限界
  • 珍しめ以上アクロバティック以下の小技
    • SendKeysUSキーボードしかサポートされない('=' は '^' になる)問題の対策
    • SendKeys入力候補まで送られてしまう問題の対策(ここではアドレス欄)
  • Microsoft Edge(EdgeHTML/Chromium)のUIオートメーション対応に関するプチ情報

シナリオ

  1. Qiita にログインします。
  2. 右上の通知ボックスで新着通知の有無を確認します。
  3. 新着通知があったら通知一覧ページを開きます1
  4. 新しい順に10件ずつ表示されるので、新着件数分の内訳を確認します。

期待結果

新たに「いいね」がついていること。

想定パターン

  1. 新着通知がない。
  2. 新着通知はあるが、その中に「いいね」はない。
  3. 新着通知の中に「いいね」がある。

検証環境

テスト実行環境

UI実行環境(テスト実行環境と同居可)

テストコード

コードは 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}件の「いいね」がつきました。");
}

起動したブラウザを検証後に閉じたいときは、EdgeDriverClose または 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-colorRemoteWebElement.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 にスクリーンショットを取得するメソッドが用意されていますので、レンダリングされた色から判定してみましょう。

notify.png

これが RemoteWebDriver.GetScreenshot() メソッドでキャプチャした通知画像です。
2桁とかあると格好よかったのですが、これでも1日待ちました。
真っ赤に見えたのは背景色との対比のせいで、実際は意外とオレンジなんですね。

いいね色.png

ブラウザのデバッグツールで色指定を確認すると "#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


  1. 通知一覧を開いた瞬間に新着の赤い背景は消えてしまいます。自分の手で赤い通知ボックスをクリックしたい方、新着ありの状態を長めに味わいたい方はご注意ください。 

  2. chrome://accessibility/ で開いているページのツリー構造を確認できます。 

CodeOne
【品質と生産性にこだわるシステム開発】 .NET(C#/VB.NET)専門・リモート開発歴10年。即日・1時間から頼める常駐しないエンジニア。確かな技術で開発チームを手堅くサポートいたします。
https://codeone.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした