
9/1くらいに思い立って、
自分の業務効率化のためにはpuppeteer-rubyをFirefoxに対応させないといけないことがわかった。できるかな…
— Yusuke Iwaki (@yi01imagination) 2020年9月1日
からの、9/8にFirefox対応をリリースしてみた。
puppeteer-rubyのFirefox対応をリリースしたぞ〜https://t.co/mcU5YgsG21
— Yusuke Iwaki (@yi01imagination) 2020年9月7日

どんな感じで開発してたかを少しだけ書いておこうかなと思う。
Firefoxといっても、どんなFirefoxでも動くわけではない
puppeteer-rubyはブラウザダウンロード機能をつけていない。そのため、puppeteer-coreと同様に、ブラウザは自分で用意しないといけない。
さて、Firefoxはどれを使えばいいのだろう...?ここが微妙にハマりどころだった。

なんとFirefoxには4つのバージョンがある。 https://www.mozilla.org/ja/firefox/channel/desktop/
- 通常版
- Beta
- Developer
- Nightly
このうち puppeteerが動作するのはどれか?・・・下ほど不安定なバージョンになるのだが、名前的には、いかにもDeveloperで動きそうに見える。が、動かない・・・。
正解は・・・
puppeteer-coreでFirefox使うときはStableでは当然だめとして、BetaでもDevでもだめで、Nightly版のfirefoxじゃないといけないことを学んだ。
— Yusuke Iwaki (@yi01imagination) 2020年9月5日
そう、2020.09.01現在は、FirefoxのNightly版でのみpuppeteerが動作する。
ポーティング自体はすぐにできた
本家PuppeteerがTypeScript化されてから、だいぶソースが読みやすくなったこともあり、ほとんどコピペで実装は終わった。
ただ、さすがに単体テストくらいは書いておこう、と思い始め、書き始めたのが9/5. ちょうどRubykaigi 2020 takeoutが行われたころ。
Rubykaigiを横目に、puppeteer-rubyでFirefoxを自動操作できるようにした。
— Yusuke Iwaki (@yi01imagination) 2020年9月5日
puppeteer-core相当の動作しか実装はできてないけどhttps://t.co/E1fwTs1SMx
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)
ここのところで、完全にフリーズしていた。

テキストエリアにフォーカスがあたっていなくて、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だとfocusが効くのにpuppeteer-rubyだとfocusが(firefox限定で)動かない謎
— Yusuke Iwaki (@yi01imagination) 2020年9月7日
ただ、これを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_page か browser.pages.first || browser.new_page かのくらいの違いしかなかった。
では、試しにpuppeteerのコードも const page = await browser.newPage(); を const page = (await browser.pages())[0] || (await browser.newPage()) にすると・・・・ フリーズした!!
そんなわけで、Puppeteer#Pageを使い回さず、都度作り直すようにしたら、この問題は解消された。
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);
};
たとえばFirefoxでPuppeteer#Keyboard で絵文字を入力しようとすると 'Input.insertText' でプロトコルエラーが起きてしまうので、Firefoxでは試験しない、という感じである。
ところで、我らがRSpecには skip の他に pending がある。
skipは単にすっ飛ばすだけなのに対し、pendingは実行してエラーが出ても無視するというもの。さらにいうと、 pendingは実行してもエラーにならない場合は逆にエラーを出してくれたりもする(pendingじゃなくなったのを検知できる!)
本家Puppeteerでも実は結構適当にitFailsFirefoxが使われていて、実際にはpassするのにskipされているテストがたくさんある。そんな事情もあって、puppeteer-rubyでは pendingを採用した。
実際にpendingにしてみると、「それもう直ってるよ!」ってCIでいくつか指摘された

Firefoxでだけ正しく描画されない画面たち...
これが今回のFirefox対応で、最後の最後にハマったところ。
手元のMacだと通るのに、CircleCIだと落ちるspecがてごわい。Linux版のFirefoxでだけflexboxレイアウトが意図したとおりに配置されてないらしいんだけども、見えないものをデバッグできない... pic.twitter.com/DyMEvi5iWx
— Yusuke Iwaki (@yi01imagination) 2020年9月7日
手元のMac版Firefoxでは通るし、CIでもChromeは通っている。CI上でFirefoxで試験実行したときだけ落ちる。
ぱっと見、CSSの指定がいまいちでflexレイアウトが10pxずれてるのはわかるんだけど、手元にLinuxのGUI環境は無いので、画面をインスペクタで確認したり解析ができない。
仕方がないので、適当に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) を見比べてみる。

うーん、あきらかにFirefoxのlinuxだけ1列少ない!Flexレイアウトでいれるだけのスペースがあいてなかったのかもしれない。
(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とられている!!!
そんなわけで、少し試行錯誤して
スクロールバーどっかいけ!!ってやったら、
スクロールバーを攻略して、やっとCI通った pic.twitter.com/vn7j82EyJv
— Yusuke Iwaki (@yi01imagination) 2020年9月7日
一件落着・・・
まとめ
puppeteer-rubyでも本家Puppeteerと同様にFirefoxを操作できるようにしました。
細かい苦労はいろいろあったものの、やはり本家のソースコードがTypeScript化されたことで、かなり効率よく読めてFirefox対応できました。可読性は正義。