valid,invalid

関心を持てる事柄について

React で SVG を描画するための use タグでは xlinkHref を使う

React で use tag 使うときの躓き

結論

sprite - SVG use tag and ReactJS - Stack Overflow にある通り xlinkHref を使うとよい。

<svg>
  <use xlinkHref="#my-logo" />
</svg>

古いバージョンでは dangerouslySetInnerHTML を使うしか回避策が無かったようだが、今ではこの方法は避けたほうが良い。

経緯

HTML と同じノリで SVG を render できるかと思ったら compile error になってしまった

<svg>
  <use href="#my-logo" />
</svg>
TS2559: Type '{ href: string; }' has no properties in common with type 'SVGProps<SVGUseElement>'.

xlink もダメ

<svg>
  <use xlink:href="#my-logo" />
</svg>
TS1003: Identifier expected.

環境

  • TypeScript 2.7.2
  • React 16.0.4

JavaScript が動作する Capybara の feature spec でブラウザのコンソールを確認する

結論

page.driver.browser.manage.logs.get(:browser)

調べた経緯

フォームの text input に対する fill が働かないのでどうしたかと思い、Selenium で操作中のブラウザをのぞいてみると React の component が一切 render されていなかった。

どこかで JavaScript の error が起きているだろうとと思ったがブラウザの console を確認する方法がわからず調べた。

visit 直後に binding.pry を張り、このワンライナーで確認したところたしかに js error で死んでいた。csrf_meta_tag で生成される meta tag から CSRF token を js 側から取り出そうとしているが、そもそもその要素が DOM tree 内に存在せずにエラーになる、というものだった。

たしかにテスト時の環境では CSRF チェックを無効にするよう config/environments/test.rb で以下のように設定しているのでわかればどうということはない問題だった。

# config/environments/test.rb
config.action_controller.allow_forgery_protection = false

しかし、直接の原因はクライアントサイドの js にあっても RSpec の failure message 上は Element Not Found で失敗しているので何が起きているか突き止めるのに結構悩んでしまった。

環境


(追記)

記事を書いた後に見つけたのだが、以下の記事では SEVERE レベルの js error が起きた場合にテストを fail させる方法を紹介している。これは良いかも。

medium.com

休日の成果を手放しに称賛しない

土日祝日などの勤務時間外にがんばって出した成果を「やっていき」「圧倒的当事者意識」などと手放しに称賛しない方が良いと思っている。

「いやー土日にがんばるなんてスゴイっすね〜〜〜」と褒められて気分良くなったりするんだけど往々にしてそもそも実現不可能なスケジュールの帳尻合わせに加担してしまっていたりする。そういうのは個人の頑張りで巻き返すのではなくいっそ破綻させた方が全体の教訓になるので好ましい。

こういう振る舞いを迂闊に繰り返すとだんだん周囲の期待値も変わってきて「休日で巻き返せる/巻き返してくれるからいっか」「今週末は働いてくれなかったのか…」となってくる。*1

ボランティア精神に近い個人の貢献は当たり前ではないことを共有し続けないといけない。


誤解しないようにしたいのが問題なのは「やり方」であって「出した成果」それ自体は尊いということ。「休日に対応したからゴミ」みたいなことは、ない。平日にやろうが土日にやろうが生んだ価値は変わらない。

なので成果に対しては「めちゃ良いですね〜〜」と言いつつ「でも次からはスケジュールやリソースの使い方をみんなでもっと考えましょう!!!」と言っていかないといけない。そして最後にもう一度「いや〜それにしても成果はめちゃ良いですね〜〜〜」と言っておく。


リモートワーク可能な勤務環境にある場合、ふと思い立った実装を試してみたり休日前やりかけて気になっている仕事に手を付けたりするのはとても簡単で自分もやってしまいがちなので一層気をつけたい。という気持ちの表明、以上。

とはいえアイデアはナマ物、勢いでやってしまう時はある。その時はちゃんと勤務時間として計上するとか代休をきちんと取るといった当たり前のところから始めましょう。

*1:それは突飛すぎると思われるかも知れないが過去にあった話。超絶ホワイトなチームだったが、やたらスケジュールを巻きたがるチームリーダーがこれを繰り返した結果、顧客のスケジュールに対する期待値が変わってしまった。他のチームメンバーの働き方も加熱する"巻き"に引っ張られた

PhantomJS + Poltergeist を Selenium + Headless Chrome で置き換える (2) Teaspoon による JavaScript test 編

PhantomJS + Poltergeist を Selenium + Headless Chrome で置き換える (1) Rails + Capybara による feature spec 編 - valid,invalid の続き。

teaspoon.env の変更内容について

Capybara + Selenium + headless Chrome の設定例はググると結構出てくるものの、Teaspoon + Selenium + headless Chrome の例はぜんぜん出てこなかった…。

それもそのはず、詳細は後述するが Teaspoon 自体のメンテナンスがあまりアクティブでないために組み合わせるためにモンキーパッチを当てなければいけない始末だった。

変更部分

以下はコードの追加分。

# spec/teaspoon_env.rb

# Teaspoon doesn't allow you to pass client driver options to the Selenium WebDriver. This monkey patch
# is a temporary fix until this PR is merged: https://github.com/jejacks0n/teaspoon/pull/519.
require 'teaspoon/driver/selenium'

Teaspoon::Driver::Selenium.class_eval do
  def run_specs(runner, url)
    # この1行のためのモンキーパッチ
    driver = ::Selenium::WebDriver.for(driver_options[:client_driver], @options.except(:client_driver))
    driver.navigate.to(url)

    ::Selenium::WebDriver::Wait.new(driver_options).until do
      done = driver.execute_script("return window.Teaspoon && window.Teaspoon.finished")
      driver.execute_script("return window.Teaspoon && window.Teaspoon.getMessages() || []").each do |line|
        runner.process("#{line}\n")
      end
      done
    end
  ensure
    driver.quit if driver
  end
end

Teaspoon.configure do |config|
  config.driver = :selenium

  options = if ENV['SELENIUM_URL'].present?
              http_client = Selenium::WebDriver::Remote::Http::Default.new(read_timeout: 120)
              {
                client_driver: :remote,
                url: ENV['SELENIUM_URL'],
                http_client: http_client,
              }
            else
              { client_driver: :chrome }
            end

  options.merge!(
    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
      chrome_options: {
        args: %w(headless window-size=1680,1050)
      }
    )
  )

  config.driver_options = options

  # test suite の記述が続く
end

driver options あたりは Capybara のときとほぼ同じ。

実際にはこのリプレイスのほか、remote の Selenium Chrome でも動くようにする変更を足したりして大変だったのだが本題ではないので省略する。

driver options 渡せない問題

まずいちばんの問題が Teaspoon v1.1.5 では Selenium::Webdriver に driver options を渡せないこと。これを改善する PR が提案されているのだが10ヶ月ほど放置されている…。

teaspoon.env は同様に困っている人々が書いた monkey patch を拝借しつつ書いた。

Net::ReadTimeout 問題

Capybara の設定との大きな違いは HTTP client を渡しているところ。これは remote Chrome -> Teaspoon server へのリクエストがデフォルトの HTTP client だと頻繁に Net::ReadTimeout してしまうため。

最初は Chrome の起動が遅いのかと疑ったが driver の初期化付近にブレイクポイントを張ってデバッグしたところ、driver.navigate.to(url) が高確率でタイムアウトしていることがわかった。

    driver = ::Selenium::WebDriver.for(driver_options[:client_driver], @options.except(:client_driver))
+   binding.pry
    driver.navigate.to(url)

Teaspoon の立ち上がりがそれだけ遅いのは実行されるテスト側に問題があるのかもしれないが、ほぼ初めて触るこのレポジトリのテストコードがどんな感じか見定めるのは厳しかったので read_timeout を伸ばした HTTP client を渡すことで回避した。

設定は 30 -> 60 -> 90 -> 120と試したが安定したのは 120 だけだった。

感想

PhantomJS + Poltergeist を Selenium + Headless Chrome で置き換えて多幸感を得たはずが Teaspoon との戦いで疲弊してしまった。次は Teaspoon を消そう。

PhantomJS + Poltergeist を Selenium + Headless Chrome で置き換える (1) Rails + Capybara による feature spec 編

レガシーに半身浴しているような、ふだんなかなか触らないけれど現役のレポジトリに潜んでいた亡霊を退治!!! Poltergeist + PhantomJS を消し去り、 Selenium + Headless Chromium に置き換えた。

このレポジトリは Rails による API バックエンドと SPA フロントエンド両方を管理しているもので、以下の2箇所で PhantomJS に依存していた。

  1. Capybara による feature spec。PhantomJS の driver として poltergeist を使用。
  2. Teaspoon による frontend の JavaScript test。Teaspoon は JS のテストランナーでもあり PhantomJS driver も内包している 。

両方について書いたら思いの外長くなってしまったのでこの記事では 1. の方のリプレイスについてのみ書く。後半の 2. の方は PhantomJS + Poltergeist を Selenium + Headless Chrome で置き換える (2) Teaspoon による JavaScript test 編 - valid,invalid で。

動機

  1. PhantomJS は元々ベースにしている Webkit が古くて最新のブラウザで利用可能な JS, CSS を充分サポートできていなかったのに加え、Chrome 59 の headless mode リリース以降コアメンテナが離れたことでプロジェクトの将来性が危ぶまれる。

  2. PhantomJS が依存している Qt のインストールでしょっちゅう問題が起きる。

  3. 他のレポジトリは既に Selenium + headless Chrome を使うようにアップデート済。

結果、可及的速やかにリプレイスすることが望ましい状況だと思いました。(というか、全部リプレイス済だとなぜか勝手に思ってた)

変更内容について

このリプレイスは同僚が既に他のレポジトリで実践していた内容をほとんど拝借した。とはいえ自分で pull request を送るのだからきちんと理解したいと思い Selenium webdriver の Ruby binding 実装を少し読んでみたりもした。

実際のコード差分

Gemfile は少しスッキリ。開発環境では Docker compose を使い、アプリケーションとは別の Docker コンテナ上で立ち上げている selenium/standalone-chrome に接続しているので今回は chromedriver や chromedriver-helper gem のインストールはしていない。

# Gemfile
group :test do
  gem 'capybara'
-  gem 'capybara-webkit'
-  gem 'phantomjs', :require => 'phantomjs/poltergeist'
-  gem 'poltergeist'
+  gem 'selenium-webdriver'
end

capybara.rb の設定方法はぐぐると諸々出てくるので今なら悩みどころは多くなさそうだ。

Headless Capybara Feature Specs with Chrome RSpec の feature spec でヘッドレス Chrome を使う - Speee DEVELOPER BLOG

など。

# spec/support/capybara.rb
- require 'capybara/poltergeist'
- require 'phantomjs'
+ require 'capybara/rspec'
+ require 'selenium-webdriver'

- Capybara.javascript_driver = :poltergeist
+ Capybara.javascript_driver = :selenium

- Capybara.register_driver(:poltergeist) do |app|
+ Capybara.register_driver :selenium do |app|

-  options = { } # 省略

+  options = {
+    browser: :chrome,
+    desired_capabilities: Selenium::WebDriver::Remote::Capabilities.chrome(
+      chrome_options: {
+        args: %w(headless window-size=1680,1050), 
+      }
+    )
+  }

+  options.merge!(url: ENV['SELENIUM_URL']) if ENV['SELENIUM_URL'].present?

-  Capybara::Poltergeist::Driver.new(app, options)
+  Capybara::Selenium::Driver.new(app, options)

  # その他 remote で Selenium を動かすための設定が続くがリプレイスとあまり関係ないので省略
end

実際はアプリケーション固有の設定などもあってもう少し複雑になったがだいたいこのような感じ。

Selenium の URL を環境変数で渡せることで local でも remote でも実行できるようになっている(はず)。

ちなみにサンプルによく出てくる disable-gpu フラグは Google Developers ガイドにも載っているので広まったのだと思うが、現時点で最新の Chrome 65 では不要になっている。Chrome 63で動いたという報告も見かけた。(ただし Windows では動くかわからない)

737678 - Headless: make --disable-gpu flag unnecessary - chromium - Monorail


Selenium なんとなく設定周りが複雑でとっつきづらい印象があったのだが wiki は丁寧に書かれている上にコードも思ったほど入り組んでいなかった。

どうやら自分の敬遠は Selenium をラップしてる各々のライブラリの統一感の無さとかから来ているのかもしれない…。


PhantomJS + Poltergeist を Selenium + Headless Chrome で置き換える (2) Teaspoon による JavaScript test 編 - valid,invalidに続く

Chrome Music Lab の Song Maker で遊ぶ

Chrome Music Lab 触るだけでめちゃ面白いし、成果物をかんたんにシェアできる仕組みも web って感じで最高

ゲーム音楽っぽいの作ってみた

https://goo.gl/EE7HFH

https://goo.gl/R4NHHL

洞窟

https://goo.gl/CpWJNk

Bootstrap3 要素の順番をレスポンシブに入れ替える

Bootstrap3 のグリッドシステムでは pull / push という要素を押し込む/引っ張るためのクラスが使える。これを使うことでレスポンシブに要素の順番を入れ替えることができる。

押し込む/引っ張ると言ってもわかりづらいので実例を見たい。

要素を入れ替える例

PC view

「戻る」が左、「すすむ」が右。一般的な構成。

f:id:ohbarye:20180225104646p:plain

Mobile view

モバイルで閲覧しているときには「すすむ」が上、「戻る」が下。これも一般的な構成。しかし要素の順番が PC と入れ替わっている。

f:id:ohbarye:20180225104654p:plain

コード

See the Pen push/pull with Bootstrap by Masato Ohba (@ohbarye) on CodePen.

PC view では右側にある「すすむ」が HTML では先に記述するのがポイント。この「すすむ」を col 6つぶん押し込む (push)。逆に後に記述する「戻る」を col 6つぶん引っ張る (pull)。

この組み合わせで順序が入れ替わる。また、おなじみの col-**- prefix でレスポンシブなマークアップができる。