puppeteer-rubyをFirefoxに対応させた

puppeteer-ruby

9/1くらいに思い立って、

からの、9/8にFirefox対応をリリースしてみた。

image

どんな感じで開発してたかを少しだけ書いておこうかなと思う。

Firefoxといっても、どんなFirefoxでも動くわけではない

puppeteer-rubyはブラウザダウンロード機能をつけていない。そのため、puppeteer-coreと同様に、ブラウザは自分で用意しないといけない。

さて、Firefoxはどれを使えばいいのだろう...?ここが微妙にハマりどころだった。

f:id:YusukeIwaki:20200908023952p:plain

なんとFirefoxには4つのバージョンがある。 https://www.mozilla.org/ja/firefox/channel/desktop/

  • 通常版
  • Beta
  • Developer
  • Nightly

このうち puppeteerが動作するのはどれか?・・・下ほど不安定なバージョンになるのだが、名前的には、いかにもDeveloperで動きそうに見える。が、動かない・・・。

正解は・・・

そう、2020.09.01現在は、FirefoxのNightly版でのみpuppeteerが動作する

ポーティング自体はすぐにできた

本家PuppeteerがTypeScript化されてから、だいぶソースが読みやすくなったこともあり、ほとんどコピペで実装は終わった。

github.com

ただ、さすがに単体テストくらいは書いておこう、と思い始め、書き始めたのが9/5. ちょうどRubykaigi 2020 takeoutが行われたころ。

CIを通すのが結構たいへんだった

Chromeでしか流していなかったCIの単体テストを、Firefoxでも流すようにしてみて、最初のCI結果がこれ。

Top 3 slowest example groups:
  Puppeteer::Page
    69.67 seconds average (626.99 seconds / 9 examples) ./spec/integration/click_spec.rb:3
  Puppeteer::BrowserContext
    5.74 seconds average (57.35 seconds / 10 examples) ./spec/integration/browser_context_spec.rb:3
  Puppeteer::Browser
    2.34 seconds average (9.35 seconds / 4 examples) ./spec/integration/browser_spec.rb:3

Finished in 11 minutes 34 seconds (files took 0.66881 seconds to load)
23 examples, 6 failures

Failed examples:

rspec ./spec/integration/browser_context_spec.rb:62 # Puppeteer::BrowserContext target events should fire target events
rspec ./spec/integration/browser_context_spec.rb:95 # Puppeteer::BrowserContext wait for target should wait for a target
rspec ./spec/integration/browser_context_spec.rb:163 # Puppeteer::BrowserContext isolation should work across sessions
rspec ./spec/integration/browser_spec.rb:11 # Puppeteer::Browser user_agent should include WebKit
rspec ./spec/integration/click_spec.rb:41 # Puppeteer::Page with button page even if window.Node is removed should click button
rspec ./spec/integration/click_spec.rb:139 # Puppeteer::Page even when JavaScript is disabled should click with disabled javascript

「6個しか落ちていない。余裕!」とか思っていたわけだが・・・

page.focusでDOM要素にフォーカスしてくれない問題

まて。

69.67 seconds average (626.99 seconds / 9 examples) ./spec/integration/click_spec.rb

この秒数、明らかにおかしい。

手元で流してみたところ、

    before {
      page.goto("http://127.0.0.1:4567/textarea")
    }

    it 'should select the text by triple clicking' do
      page.focus('textarea')
      text = "This is the text that we are going to try to select. Let's see how it goes."
      page.keyboard.type_text(text)

ここのところで、完全にフリーズしていた。

f:id:YusukeIwaki:20200908030046p:plain

テキストエリアにフォーカスがあたっていなくて、URL入力欄にフォーカスが当たりっぱなしだ。これが、focusを5回連続で呼んだり、適当にsleep入れたりしても解消されなかった。

page.focusをpage.clickにすれば直るんだけども、それだと本家puppeteerと違うことを単体テストしてしまうことになり望ましくない。

puppeteer-rubyの固有の問題なのかpuppeteer本家でも起きるのかの切り分けのために、

const puppeteer = require("puppeteer");

launchOptions = {
  product: "firefox",
  headless: false,
  slow_mo: 50,
};

puppeteer.launch(launchOptions).then(async (browser) => {
  const pages = await browser.newPage();

  await page.goto("http://localhost:8888/textarea.html");

  await page.focus("textarea");
  const text =
    "This is the text that we are going to try to select. Let's see how it goes.";
  await page.keyboard.type(text);
  await page.click("textarea");
  await page.click("textarea", { click_count: 2 });
  await page.click("textarea", { click_count: 3 });
});

ほぼ同じ処理内容をJSで書いてみて実行したところ・・・・・・・ 起きない!!

ただ、これをpuppeteer-rubyで書き直してみると・・・

Puppeteer.launch(product: 'firefox', headless: false, slow_mo: 50) do |browser|
  page = browser.new_page

  page.goto("http://localhost:8888/textarea.html")

  sleep 5
  page.focus("textarea")
  sleep 5
  text =  "This is the text that we are going to try to select. Let's see how it goes."
  page.keyboard.type_text(text)
  page.click("textarea")
  page.click("textarea", click_count: 2)
  page.click("textarea", click_count: 3)
end

・・・ あれれ??起きない!! RSpecだと起きるのはなんでだ?!

違いというと、Rubyのコードについては browser.new_pagebrowser.pages.first || browser.new_page かのくらいの違いしかなかった。

では、試しにpuppeteerのコードも const page = await browser.newPage();const page = (await browser.pages())[0] || (await browser.newPage()) にすると・・・・ フリーズした!!

そんなわけで、Puppeteer#Pageを使い回さず、都度作り直すようにしたら、この問題は解消された。

github.com

Firefoxではどうあがいても通せないspecたち

本家Puppeteerには mochaの it と同様に使える itFailsFirefox っていうメソッドが定義されている。何も特別なことはなくて、firefoxで実行されている場合にはskipするだけだ。

export const itFailsFirefox = (
  description: string,
  body: Mocha.Func
): Mocha.Test => {
  if (isFirefox) return xit(description, body);
  else return it(description, body);
};

https://github.com/puppeteer/puppeteer/blob/7b24e5435b8450a99445149e3395ac0642ca574e/test/mocha-utils.ts#L139

たとえばFirefoxでPuppeteer#Keyboard で絵文字を入力しようとすると 'Input.insertText'プロトコルエラーが起きてしまうので、Firefoxでは試験しない、という感じである。

ところで、我らがRSpecには skip の他に pending がある。

qiita.com

skipは単にすっ飛ばすだけなのに対し、pendingは実行してエラーが出ても無視するというもの。さらにいうと、 pendingは実行してもエラーにならない場合は逆にエラーを出してくれたりもする(pendingじゃなくなったのを検知できる!)

本家Puppeteerでも実は結構適当にitFailsFirefoxが使われていて、実際にはpassするのにskipされているテストがたくさんある。そんな事情もあって、puppeteer-rubyでは pendingを採用した。

github.com

実際にpendingにしてみると、「それもう直ってるよ!」ってCIでいくつか指摘された

f:id:YusukeIwaki:20200908103607p:plain

Firefoxでだけ正しく描画されない画面たち...

これが今回のFirefox対応で、最後の最後にハマったところ。

手元のMacFirefoxでは通るし、CIでもChromeは通っている。CI上でFirefoxで試験実行したときだけ落ちる。

ぱっと見、CSSの指定がいまいちでflexレイアウトが10pxずれてるのはわかるんだけど、手元にLinuxGUI環境は無いので、画面をインスペクタで確認したり解析ができない。

仕方がないので、適当にDocker環境を作って、binding.pryを仕掛けながら、どこで10pxずれてるのかを探ってみることにした。

From: /Users/yusuke-iwaki/src/github.com/YusukeIwaki/puppeteer-ruby/spec/integration/element_handle_spec.rb:74 :

    69: 
    70:         element_handle = page.S('.box:nth-of-type(13)')
    71:         box = element_handle.bounding_box
    72:         require 'pry'
    73:         binding.pry
 => 74:         expect(box.x).to eq(100)
    75:         expect(box.y).to eq(50)
    76:         expect(box.width).to eq(50)
    77:         expect(box.height).to eq(50)
    78:       end
    79:     end

[1] pry(#<RSpec::ExampleGroups::PuppeteerElementHandle::BoundingBox::WithGridPage>)> box
=> #<Puppeteer::ElementHandle::BoundingBox:0x00007f7f1f0fa438 @height=50, @width=50, @x=100, @y=50>

手元のMacだと↑こんな感じで動いている。しかし、DockerでLinux上のFirefoxは違っていて、

From: /puppeteer-ruby/spec/integration/element_handle_spec.rb:74 :

    69: 
    70:         element_handle = page.S('.box:nth-of-type(13)')
    71:         box = element_handle.bounding_box
    72:         require 'pry'
    73:         binding.pry
 => 74:         expect(box.x).to eq(100)
    75:         expect(box.y).to eq(50)
    76:         expect(box.width).to eq(50)
    77:         expect(box.height).to eq(50)
    78:       end
    79:     end

[1] pry(#<RSpec::ExampleGroups::PuppeteerElementHandle::BoundingBox::WithGridPage>)> box
=> #<Puppeteer::ElementHandle::BoundingBox:0x00007f809c0734f0 @height=50, @width=50, @x=150, @y=50>

確かに変な値になっている。

「とりあえず画面が見たい!」ということで、

[2] pry(#<RSpec::ExampleGroups::PuppeteerElementHandle::BoundingBox::WithGridPage>)> page.screenshot path: "./firefox.png" ; nil

えい!スクショをとりあえず取ってみる。Firefox (mac), Chrome (linux), Firefox (linux) を見比べてみる。

f:id:YusukeIwaki:20200908142718p:plain

うーん、あきらかにFirefoxlinuxだけ1列少ない!Flexレイアウトでいれるだけのスペースがあいてなかったのかもしれない。

f:id:YusukeIwaki:20200908142922p:plain (Special thanks to ウインドウサイズを取得する 【JavaScript 動的サンプル】 !)

を参考に、画面の幅を取得してみる。

[2] pry(#<RSpec::ExampleGroups::PuppeteerElementHandle::BoundingBox::WithGridPage>)> page.evaluate("() => document.body.offsetWidth")
D, [2020-09-07T17:11:14.262008 #1] DEBUG -- : SEND >> {"sessionId":1,"method":"Runtime.callFunctionOn","params":{"functionDeclaration":"() => document.body.offsetWidth\n//# sourceURL=__puppeteer_evaluation_script__\n","executionContextId":7,"arguments":[],"returnByValue":true,"awaitPromise":true,"userGesture":true},"id":25}
D, [2020-09-07T17:11:14.275635 #1] DEBUG -- : RECV << {"sessionId"=>1, "id"=>25, "result"=>{"result"=>{"type"=>"number", "value"=>490, "description"=>"490"}}}
=> 490
[3] pry(#<RSpec::ExampleGroups::PuppeteerElementHandle::BoundingBox::WithGridPage>)> page.evaluate("() => window.innerWidth")
D, [2020-09-07T17:11:23.171738 #1] DEBUG -- : SEND >> {"sessionId":1,"method":"Runtime.callFunctionOn","params":{"functionDeclaration":"() => window.innerWidth\n//# sourceURL=__puppeteer_evaluation_script__\n","executionContextId":7,"arguments":[],"returnByValue":true,"awaitPromise":true,"userGesture":true},"id":26}
D, [2020-09-07T17:11:23.185157 #1] DEBUG -- : RECV << {"sessionId"=>1, "id"=>26, "result"=>{"result"=>{"type"=>"number", "value"=>500, "description"=>"500"}}}
=> 500

なんと、スクロールバーかなにかの分だけ10pxとられている!!!

そんなわけで、少し試行錯誤して

スクロールバーどっかいけ!!ってやったら、

一件落着・・・

まとめ

puppeteer-rubyでも本家Puppeteerと同様にFirefoxを操作できるようにしました。

細かい苦労はいろいろあったものの、やはり本家のソースコードがTypeScript化されたことで、かなり効率よく読めてFirefox対応できました。可読性は正義。

明日からRailsFirefoxをぐりぐり動かす業務改善やるぞー!