詩と創作・思索のひろば

詩と創作・思索のひろば

ドキドキギュンギュンダイアリーです!!!

Fork me on GitHub

Storybook向けのReact propsをPlaywrightのE2Eテストから錬成する

Reactを書いていてStorybookは便利だけどargsを作るのは面倒。Reactコンポーネントのpropsの構成や型が変わるたびにこれらを更新していくのがいかにも骨が折れそうだ。それにせっかくStorybookを使うなら、propsに渡すのは人工的なものではなく実地のもの、せめてアプリケーションが作ったものであってほしい。うまくコンポーネントの設計ができてれば問題はないのだけど、現実にありえないpropsを与えることができることはままあるから……。

そういうわけでPlaywrightを使ったE2EテストからReactコンポーネントのpropsを生成する方法を探る。

キレイなやり方を狙うなら React Devtools も利用する __REACT_DEVTOOLS_GLOBAL_HOOK__ を使いたいものだけど、これはまだそんなに使いやすいわけではない(将来に期待)。なので今回は汚いやり方でいく。

Playwrightを使うと書いたが、そもそもPlaywrightが以下のようにReactコンポーネントを特定する方法もわりと汚いので悪いことをしてる気分にはならないね。

await page.locator('_react=BookItem').click();

いつからかは知らないけど少なくともReact 18ではReactが生成したDOMには __reactFiber$648z12axv7 のようなプロパティが生えていて、ここを辿ると元のpropsが得られるっぽい(観察による)。

$0.__reactFiber$648z12axv7._debugOwner.memoizedProps

ということはこれをJSONにでもして保存してしまえば、Storybookから再利用できる生きたデータが得られるわけだ。

E2Eテストを走らせるたびにpropsファイルが更新されるのは嬉しくないので、Playwrightのsnapshot生成の仕組み(--update-snapshot 周辺)に乗っかることにする。ライフサイクル的には適してるだろう。Playwrightのテスト中、いまスナップショットをアップデートするモードかどうかは test.info().config.updateSnapshots で知ることができる。

そういうわけでこういうユーティリティ関数をPlaywrightのテストから呼べるようにしてやる。

import { existsSync, writeFileSync } from "fs";
import { test, Page } from "@playwright/test";

async function recordReactComponentProps(
  page: Page,
  componentName: string,
  path: string
) {
  const mode = test.info().config.updateSnapshots;
  if (mode === "none") {
    return;
  }

  if (mode === "missing") {
    if (existsSync(path)) {
      return;
    }
  }

  const props = await page
    .locator(`_react=${componentName}`)
    .evaluate((el): unknown => {
      const reactFiberKey = Object.keys(el).find((k) =>
        /^__reactFiber/.test(k)
      );
      if (!reactFiberKey) {
        throw new Error("Could not find reactFiberKey");
      }

      return el[reactFiberKey]?._debugOwner?.memoizedProps;
    });

  writeFileSync(path, JSON.stringify(props, null, 2));
}

あとはテスト中、よい状態になったところで

  await recordReactComponentProps(
    page,
    "MyComponent",
    "./src/components/MyComponent.storyprops.json"
  );

などとして保存して、Storybookから読み出せばいいだろう。更新したくなったら playwright test -u すればいい。Reactのバージョンが上がったら壊れる可能性はぜんぜんあるが、仕組みが壊れてもスナップショット自体は有用なはず。

Rancher Desktopでdocker build時、巨大コンテキストの転送中に反応がなくなる問題

……に遭遇した。でかいnode_modulesを.dockerignoreもせずにいると、"=> transferring context: " が返ってこなくなるイメージ。こうなるとビルドをキャンセルしてもdockerの操作にまったく反応がなくなり、Factory Resetするしかなくなってしまう。これは困った(ちなみにその後の調査があたってれば、ホストマシンの再起動でもいいはず)。

結論

裏側のLima VMの net.core.rmem_max あたりを大きめにとってあげるとよさそう。~/Library/Application Support/rancher-desktop/lima/0/lima.yamlprovision: に以下のようにひと項目足す。 数字は適当!

--- ~/Library/Application Support/rancher-desktop/lima/0/lima.yaml.bak
+++ ~/Library/Application Support/rancher-desktop/lima/0/lima.yaml
@@ -153,6 +153,10 @@
       mount bpffs -t bpf /sys/fs/bpf
       mount --make-shared /sys/fs/bpf
       mount --make-shared /sys/fs/cgroup
+  - mode: system
+    script: |
+      sudo sysctl -w net.core.wmem_max=12582912
+      sudo sysctl -w net.core.rmem_max=12582912
 portForwards:
   - guestPortRange:
       - 1

調査

調査の過程もおもしろかったので残しておく。

ログのありかは設定画面からわかったので、tail -F ~/Library/Logs/rancher-desktop/* しつつ状況を再現してみると、"lima write unixgram -\u003e: write: no buffer space available" という怪しいメッセージを吐いてから行動不能になっている模様。こうなると rdctl shell もできなくなってしまう。

どこかしらでUnixドメインソケットによる通信が行われているらしいが、調べてみたところ no buffer space available になるときはどうも net.core.wmem_max とか net.core.rmem_max のチューニングが効きそう。おそらくlimaのVM側の設定だとあたりをつけたが、そもそもRancher Desktopの裏側で動いてるVMの設定はどこにあるんだ。

psしてみると limactl.ventura hostagent 0 というプロセスが動いてるので ps eww <PID> してLIMA_HOMEが ~/Library/Application Support/rancher-desktop/lima になっていることがわかった。なので設定の本体は上記のYAMLファイル。

このYAMLファイルを見てみるとこういう設定があって、ホストとゲストのUNIXソケットをポートフォワードしてるのだとわかる。

portForwards:
  - guestSocket: /var/run/docker.sock
    hostSocket: /Users/motemen/.rd/docker.sock

え、ソケットのポートフォワードってどうやるの、と調べると sshのポートフォワードを使っていた。sshがUNIXソケットのフォワードもできるのは知らなかった。limaのポートフォワードがsshで実現されていることを知ったので、Rancher DesktopがUDPのポートフォワードに対応してないのも理由を理解できた。今回sshの内部までは追わなかったけど、ここでsshが双方のソケットの調停をうまくできてないってことなんだろうか〜。気になったけど、いったん調査はここまでとした。

ちなみにこの記事を書くため状況を作り直してみたのだけどうまく再現させられませんでした……。

WezTermに移行した

PCを新調したのを期に、ターミナルの環境をiTerm2+tmuxをWezTermに移行した。とくに不満はなかったのだけど、iTerm2の設定をぽちぽちする*1ことを考えるとこれ数年おきにやるのか……と思ってしまったので心機一転、設定をLuaで管理できるというWezTermを使ってみることにした。

偶然以下の記事を見ていたのが大きい。設定も基本これをぱくった。

Okay, I really like WezTerm | Alex Plescan

いいところ

  • Luaで設定できる。別にLuaが書きたいわけではなくてVCSで管理できるのがよい。
  • WezTerm単体で、キーボードのみで文字列選択・コピーができる(Copy Mode)。これまではこの用途にtmuxを使っていたのでシンプルになってうれしい。
  • さらに、コピーしたいところまでカーソルを動かさなくていい Quick Select というモードがある。URLやパス、ハッシュ値などのパターンを画面内で認識して少ないキータイプでコピーできる。
  • tmuxと組み合わせるとiTerm2で実現できなかった(と思う)、シェルプロンプトへのジャンプができるようになった(Shell IntegrationScrollToPrompt)。
  • コマンドパレットがあるので、キーバインドを設定してない、自分が詳しくないコマンドもわりと簡単に実行できる。
  • wezterm のサブコマンドで画像の表示もできる(iTerm Image Protocol)。
  • カラーテーマがデフォルトで提供されている、設定ファイルは自動リロードされるなど細かいところも便利。

自分の設定

https://github.com/motemen/dotfiles/blob/master/.config/wezterm/wezterm.lua

だいたいドキュメントを読めばやりたいことは実現できるが、いくつか設定中に困ったところがあったので、メモしておく。

  • コピーモードに入る際に前回の検索内容をクリアできなくて、思ってないところにカーソルが飛んでしまうことがある。これはまだ修正されていないので、ワークアラウンド的に検索モードを抜ける際に検索内容をクリアすることで対応とした。
  • IMEでの変換時にCtrl-HするとWezTermにキー入力が食われちゃうので、config.macos_forward_to_ime_modifier_mask = 'SHIFT|CTRL' する。
  • カーソルを点滅させる設定は config.default_cursor_style = 'BlinkingBlock'

WezTermいいね。いっぽうでiTerm2には大変お世話になった。ありがとうiTerm2(寄付済み)。

*1:たぶん設定のエクスポートはできるが、やってなかった

はてなで一緒に働きませんか?