おがさわらなるひこのオープンソースとかプログラミングとか印刷技術とか

おがさわらなるひこ @naru0ga が技術系で興味を持ったりなんだりしたことをたまーに書くブログです。最近はてなダイアリー放置しすぎて記事書くたびにはてな記法忘れるのではてなブログに移行しました。

クリエイティブ・コモンズ・ライセンス
特に断りがない場合は、本ブログの筆者によるコンテンツは クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスの下に提供されています。

フローティングヘッダつきのWebサイトでスクロールしながらのスクショを撮る

私が仕事で愛用しておりますオープンソースなライブラリにAShotというのがありまして:

github.com

こいつはSeleniumスクリーンショットを超強力にしてくれるやつなのです。特定の要素だけのスクショ撮るとか、特定の要素を除外して撮るとか、関係ないところにぼかし処理を入れたりとか、さらに複数のイメージのdiffを取ってくれる機能まであって最高です。

なんといっても一番多用しているのが、「ブラウザ上で見切れている部分もスクロールしながらスクリーンショットを取ってくれる」機能*1なんです。むっちゃ便利ですよ。とくにエビデンスを重視するエンプラな現場では。

……が。こいつには一つ問題があって、「固定ヘッダがあるページだとヘッダが写り込んで、代わりにページの内容の一部が隠れてしまうのです。こんなふうに(サンプルはW3Schoolsの例を参考にさせていただきました)。

f:id:naruoga:20190209005245p:plain
ヘッダが写り込んでしまった悲しいサンプル

AShotには豊富なShooting Strategyがあるので、こういう機能もあるんじゃないの? と思って調べたんですが、どうもダメっぽく……こんなIssueが上がってました。

github.com

そっかーダメなのかーと思いつつみんなのコメントを読んでて……ん?

f:id:naruoga:20190209010008p:plain

So, you can change the attributes of an element with Selenium (using JavascriptExecutor).

My solution was change the visibility of header menu to hide it and make the screenshots.

After that, I turned back the attribute to the original one (just for keep the behavior along the tests).

With that change I can peform a full screenshot perfectly. Without black bars or overlapped menu.

(雑な訳)

Seleniumで(JavascriptExecutor)を使って要素の属性を変更できるじゃん。 ヘッダーメニューを非表示にしてからスクショを撮るってのがぼくの解決策だ。撮り終わったら属性を元に戻す(テストでの振る舞いを変えたくないからね)。 この変更でフルのスクリーンショットをカンペキに撮ることができたよ。黒いバーもメニューの重なりもなしでね。

おおーーー、なるほどー。

試してみた。こんな感じのメソッド用意して……

public static BufferedImage scrollshotWithFixedHeader(WebDriver driver, WebElement header) {
    if (!(driver instanceof JavascriptExecutor)) {
        throw new RuntimeException("the driver can not become JavascriptExecutor");
    }
    JavascriptExecutor jsexec = (JavascriptExecutor) driver;

    // ヘッダ部分だけスクショ撮る。JQueryなしのWebでも動くようにcoordsProviderを指定
    BufferedImage headerImage = new AShot()
            .coordsProvider(new WebDriverCoordsProvider())
            .takeScreenshot(driver, header)
            .getImage();

    // JSでヘッダを非表示に
    jsexec.executeScript("arguments[0].style.visibility='hidden'", header);
    // スクロールしながらスクリーンショットを撮る。ヘッダ部分は非表示になっただけなので空白になる
    BufferedImage bodyImage = new AShot()
            .shootingStrategy(ShootingStrategies.viewportPasting(100))
            .takeScreenshot(driver)
            .getImage();
    // ヘッダ表示を元に戻す
    jsexec.executeScript("arguments[0].style.visibility=''", header);
    // スクロールして撮った画像にヘッダを貼り付ける
    Graphics2D graphics = bodyImage.createGraphics();
    graphics.drawImage(headerImage, 0, 0, null);
    graphics.dispose();

    // 加工後の画像を返す
    return bodyImage;
}

呼ぶ側はこんな感じ*2。ここで save()BufferedImage を引数で渡されたファイル名で保存するメソッドです。

open(new File("HTML/header.html").toURI().toURL());

BufferedImage screenshot = scrollshotWithFixedHeader(
        getWebDriver(),
        $("#myHeader").toWebElement());
save(screenshot, "test2");

f:id:naruoga:20190209005935p:plain
ちゃんとスクショ撮れた!

メソッド名もいいかげんだし、JSのコードとか結構雑だし*3、テストもちゃんとやってないので、もちょっとブラッシュアップする必要がありそうですけど、まあ一応動いたので満足しました。という、お話でした。

PS. 関係ないけどはてなブログの脚注 ((...)) って、中で ` 使えないんですねー。知らなかった。

*1:が、こいつIEだと動かないという弱点があります。内部で呼んでるJavaScriptIEだとダメという話なので、直したPull-reqを投げてるんですけど、マージされる気配がない……というか、開発が停滞している気配がある。フォークしちゃおうかなあって考えてます。

*2:Selenium WebDriverのラッパーのSelenideを使ってるので open() とか $() とか getWebDriver() についてはSelenideについての解説を参照してくだされ。

*3:element.style.visibilityの値いきなり上書きしちゃうのは大丈夫なのかな……。