去年のアドカレ記事もVRTのネタで書いて、今年は違うのにしようと思っていたのですが、またVRTのネタです。よろしくお願いいたします。
はじめに
現在参画しているプロジェクトにて、下のような課題を持つ画面がありました。
- 1画面内で表示されるレイアウトが9パターン(今後更に増える想定)と、同画面内に入稿されるHTMLで使われるCSSが約40パターン(こちらも今後更に増える想定)存在し、これらの特有の組み合わせのみスタイルの崩れが発生することがある。
- これらを全パターン目視で確認することはかなり大変...
今回の記事はこの課題をVRTを使って解決した話と、その際に苦労した話です。
前半で軽く実装内容を紹介しますが、技術的な話はあまりしません。
また、前提としてVRTはstorycapとreg-cliを使って実装しています。
そもそもVRT is 何?という方や、storycapとreg-cliを利用したVRTについて知りたい方は去年のアドカレ記事を読んでいただけたら嬉しいです
なぜ自動化したか
そもそも自動化した理由ですが、今回の課題を解決するためにはVRT用に大量のStoryを作成する必要がありました。
加えて、画面パターンとCSSのパターンは今後も増える想定で、増えるたびに 増えたレイアウトパターン x 増えたCSSパターン
分のStoryを作成する運用となってしまうことが予想されました。
この運用はそもそも大量のStory作成コストが高いこと、また作成後も膨大な数のStory管理1にコストがかさむことが想定されたため、これらのStory作成を自動化する方針を立てました。
また、コード生成はせずにランタイムで動的にStoryを生成する案もあったのですが、CSFを使った記述では難しそうだったので今回は採用しませんでした。
やったこと
まず作成したスクリプト自体について軽く説明します。
概観
実現したいことは、レイアウトパターン x CSSパターン
分のStoriesを作成すること、です。
CSSパターンはすでにJSONで一覧化されており、レイアウトパターンもfixture(入稿されるHTMLを含んだモックデータ)で一覧化されているため、これらを組み合わせたfixtureとStoriesを新たに作成してこれを実現しています。
結果、構成としては↑の画像のようになりました。
なお、環境はNode.jsで言語はTypeScriptを使っています。
それぞれ具体どのようなことをやっているか説明します。
スクリプト1
このスクリプトでやっていることは、
-
入稿されるCSS/HTMLのパターン一覧のJSON
を読み込む - Story用にサンプルデータを組み込んだHTML/CSSに変換し、それらを再びCSSパターン名をキーとしたJSONとして吐き出す。
の2つです。
なぜ 入稿されるCSS/HTMLのパターン一覧のJSON
のような都合の良いJSONが用意されているかといいますと、こちら今回の自動化のために用意されたものではなく、Visual Studio Codeのsnippets機能2を利用した入稿ツール用に用意されているものの流用になります。
イメージ共有のためにざっくり具体の話をすると、1のJSONの中身は大体次のようなものです。(説明に必要ないプロパティは割愛しています。)
{
"style1": {
"body": "<div class=\"style1\">${1}</div>",
"exampleData": [
"これはstyle1だよ。"
],
},
"style2": {
"body": "<div class=\"style2\">${1}</div>",
"exampleData": [
"これはstyle2だよ。"
],
},
...
}
入稿ツール用にexampleData
も用意されていたので、こちらを利用してHTML内のプレースホルダーを置換し、↓のようなJSONに加工し直してファイルに書き出しています。
{
"style1": {
"html": "<div class=\"style1\">これはstyle1だよ。</div>",
},
"style2": {
"html": "<div class=\"style2\">これはstyle2だよ</div>",
},
...
}
スクリプト2
こちらのスクリプトでやっていることは下記のとおりです。
-
レイアウトパターンごとのfixture
を読み込む - スクリプト1で生成されたCSSパターン一覧JSONを読み込む
- レイアウトパターンとCSSパターンを組み合わせたfixture, Storiesを生成する
ここでのfixtureはStorybookに必要なモックデータを指しています。このfixtureには入稿されるHTMLテキストが含まれているため、このHTMLテキストをスクリプト1で生成したJSONのHTMLテキストに置き換えて、新たなfixtureを レイアウトパターン x CSSパターン
分生成します。
生成したfixtureに対応するStoryを生成し、1つの.stories.tsx
に吐き出して処理終了になります。
具体的な成果物はざっくり下のようになります。
const fixture = {
...data // その他のテストデータ諸々
html: formattedHtml // スクリプト1で生成されたJSON内のHTML
};
export const StoryComponent = Template.bind({}) // StoryのComponentを定義
StoryComponent.args = {
fixture, // ここでfixtureを指定
}
// ここまでのセットがパターン数分作成され、1ファイルに吐き出される
苦しんだこと
日本語とts.Printer
今回のスクリプトでは最終的な .stories.tsx
のコード生成にTypeScript Compiler APIとASTを利用しています。
AST nodeからString型のソースコードに変換する際に ts.Printer
を利用するのですが、AST node内に日本語のテキストが含まれている場合に罠がありました。(このパターン自体あまりないような気がするのですが...)
日本語のテキストが含まれたAST nodeをts.Printer.printfile
などでソースコードに変換すると日本語がエスケープされたままの状態で吐き出されてしまいます。
具体的には
const hoge = "ほげ";
のようなコードを一度AST nodeに変換して、ts.Printer.printfile
で吐き出すと
const hoge = "\u307B\u3052";
という状態で吐き出されてしまいます。
ここで結構ハマってしまったのですが、いろいろ漁っていたらこのissueを見つけて、一旦こちらで紹介されているワークアラウンドを使って回避できました。
let content = printer.printFile(file);
content = unescape(content.replace(/\\u/g, "%u"));
しかし、unescapeがdeprecated なこともあって懸念が残っています。妙案募集中です。
flakyなテストの対応
VRTにはある程度つきものだとおもうのですが、今回もご多分に漏れずflakyな(安定しない)テストの挙動に苦労しました。
今回の場合は、追加したStoryの中にはimgタグを持っているものがあり、画像の読み込みに時間がかかっていると、layout shiftなどの影響でコンポーネントの見た目が変わってしまい、テストが落ちてしまう、というflakyでした。
Snapshotを撮るまでに一定の遅延を入れる3などを試しては失敗してを繰り返し、CIの待ち時間もあり、結構時間がかかってしまいました。
結論、今回のテストではスクリプトの中で画像のsrcをbase64エンコードしたダミー画像に置き換えることで、画像へのリクエストを発生させないようにして、flakyを解決しました。
スクリプトの完成後、flakyを撲滅する時間が割と長くかかってしまったため、もう少しここのダミー画像に置き換えるという判断を早くしたかったな、と反省しています。
おわりに
この実装全体を通して、個人的に反省点はたくさんあったなーと思います。
完了までの時間が想定よりも長くかかってしまったので、次回似たようなことをやるときは今回の反省点を活かして、もう少しスムーズに完成まで持っていきたいです。(その時はその時でまた別の問題が出てくると思ってますが...)
とはいえ、課題となっていた画面のVRTと必要なStory作成の自動化は達成できたので、よかったなぁという気持ちです