VRT用の大量パターンのStory作成をコード生成で自動化した話

[去年のアドカレ記事](https://qiita.com/sakamuuy/items/44a109532f06e0b619e3)もVRTのネタで書いて、今年は違うのにしようと思っていたのですが、またVRTのネタです。よろしくお願いいたします。

## はじめに  
現在参画しているプロジェクトにて、下のような課題を持つ画面がありました。

- 1画面内で表示されるレイアウトが9パターン（今後更に増える想定）と、同画面内に入稿されるHTMLで使われるCSSが約40パターン（こちらも今後更に増える想定）存在し、これらの特有の組み合わせのみスタイルの崩れが発生することがある。
- これらを全パターン目視で確認することはかなり大変...

今回の記事はこの課題をVRTを使って解決した話と、その際に苦労した話です。
前半で軽く実装内容を紹介しますが、技術的な話はあまりしません。

また、前提としてVRTは[storycap](https://github.com/reg-viz/storycap)と[reg-cli](https://github.com/reg-viz/reg-cli)を使って実装しています。
そもそもVRT is 何？という方や、storycapとreg-cliを利用したVRTについて知りたい方は[去年のアドカレ記事](https://qiita.com/sakamuuy/items/44a109532f06e0b619e3)を読んでいただけたら嬉しいです :pray: 

https://qiita.com/sakamuuy/items/44a109532f06e0b619e3

## なぜ自動化したか
そもそも自動化した理由ですが、今回の課題を解決するためにはVRT用に大量のStoryを作成する必要がありました。
加えて、画面パターンとCSSのパターンは今後も増える想定で、増えるたびに `増えたレイアウトパターン x 増えたCSSパターン` 分のStoryを作成する運用となってしまうことが予想されました。
この運用はそもそも大量のStory作成コストが高いこと、また作成後も膨大な数のStory管理[^1]にコストがかさむことが想定されたため、これらのStory作成を自動化する方針を立てました。  

また、コード生成はせずにランタイムで動的にStoryを生成する案もあったのですが、CSFを使った記述では難しそうだったので今回は採用しませんでした。

## やったこと
まず作成したスクリプト自体について軽く説明します。  

#### 概観
実現したいことは、`レイアウトパターン x CSSパターン`分のStoriesを作成すること、です。
CSSパターンはすでにJSONで一覧化されており、レイアウトパターンもfixture（入稿されるHTMLを含んだモックデータ）で一覧化されているため、これらを組み合わせたfixtureとStoriesを新たに作成してこれを実現しています。

![script.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/889717/c2bb5e87-ffd4-5a5e-52ed-c67912ffddc0.png)


結果、構成としては↑の画像のようになりました。
なお、環境はNode.jsで言語はTypeScriptを使っています。

それぞれ具体どのようなことをやっているか説明します。

#### スクリプト１
このスクリプトでやっていることは、
1. `入稿されるCSS/HTMLのパターン一覧のJSON` を読み込む
1. Story用にサンプルデータを組み込んだHTML/CSSに変換し、それらを再びCSSパターン名をキーとしたJSONとして吐き出す。

の2つです。

なぜ `入稿されるCSS/HTMLのパターン一覧のJSON` のような都合の良いJSONが用意されているかといいますと、こちら今回の自動化のために用意されたものではなく、Visual Studio Codeのsnippets機能[^2]を利用した入稿ツール用に用意されているものの流用になります。

イメージ共有のためにざっくり具体の話をすると、1のJSONの中身は大体次のようなものです。（説明に必要ないプロパティは割愛しています。）
```jsonc:入稿されるCSS/HTMLのパターン一覧のJSON
{
  "style1": {
    "body": "<div class=\"style1\">${1}</div>",
    "exampleData": [
      "これはstyle1だよ。"
    ],
  },
  "style2": {
    "body": "<div class=\"style2\">${1}</div>",
    "exampleData": [
      "これはstyle2だよ。"
    ],
  },
  ...
}
```

入稿ツール用に`exampleData`も用意されていたので、こちらを利用してHTML内のプレースホルダーを置換し、↓のようなJSONに加工し直してファイルに書き出しています。

```jsonc:Story用に整形されたHTMLをもったCSSパターン一覧JSON
{
  "style1": {
    "html": "<div class=\"style1\">これはstyle1だよ。</div>",
  },
  "style2": {
    "html": "<div class=\"style2\">これはstyle2だよ</div>",
  },
  ...
}
```


#### スクリプト２
こちらのスクリプトでやっていることは下記のとおりです。
1. `レイアウトパターンごとのfixture` を読み込む
1. スクリプト１で生成されたCSSパターン一覧JSONを読み込む
1. レイアウトパターンとCSSパターンを組み合わせたfixture, Storiesを生成する

ここでのfixtureはStorybookに必要なモックデータを指しています。このfixtureには入稿されるHTMLテキストが含まれているため、このHTMLテキストをスクリプト１で生成したJSONのHTMLテキストに置き換えて、新たなfixtureを `レイアウトパターン x CSSパターン` 分生成します。
生成したfixtureに対応するStoryを生成し、1つの`.stories.tsx`に吐き出して処理終了になります。

具体的な成果物はざっくり下のようになります。
```ts
const fixture = {
  ...data // その他のテストデータ諸々
  html: formattedHtml // スクリプト１で生成された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`などでソースコードに変換すると日本語がエスケープされたままの状態で吐き出されてしまいます。

具体的には
```ts
const hoge = "ほげ";
```
のようなコードを一度AST nodeに変換して、`ts.Printer.printfile`で吐き出すと
```ts
const hoge = "\u307B\u3052";
```
という状態で吐き出されてしまいます。

ここで結構ハマってしまったのですが、いろいろ漁っていたら[このissue](https://github.com/microsoft/TypeScript/issues/36174#issuecomment-597564149)を見つけて、一旦こちらで紹介されているワークアラウンドを使って回避できました。

https://github.com/microsoft/TypeScript/issues/36174#issuecomment-597564149

```ts:紹介されていたワークアラウンド
let content = printer.printFile(file);
content = unescape(content.replace(/\\u/g, "%u"));
```

しかし、[unescapeがdeprecated](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/unescape) なこともあって懸念が残っています。妙案募集中です。

### flakyなテストの対応
VRTにはある程度つきものだとおもうのですが、今回もご多分に漏れずflakyな(安定しない)テストの挙動に苦労しました。

今回の場合は、追加したStoryの中にはimgタグを持っているものがあり、画像の読み込みに時間がかかっていると、layout shiftなどの影響でコンポーネントの見た目が変わってしまい、テストが落ちてしまう、というflakyでした。

Snapshotを撮るまでに一定の遅延を入れる[^3]などを試しては失敗してを繰り返し、CIの待ち時間もあり、結構時間がかかってしまいました。

結論、今回のテストではスクリプトの中で画像のsrcをbase64エンコードしたダミー画像に置き換えることで、画像へのリクエストを発生させないようにして、flakyを解決しました。

スクリプトの完成後、flakyを撲滅する時間が割と長くかかってしまったため、もう少しここのダミー画像に置き換えるという判断を早くしたかったな、と反省しています。

## おわりに
この実装全体を通して、個人的に反省点はたくさんあったなーと思います。
完了までの時間が想定よりも長くかかってしまったので、次回似たようなことをやるときは今回の反省点を活かして、もう少しスムーズに完成まで持っていきたいです。（その時はその時でまた別の問題が出てくると思ってますが...）
とはいえ、課題となっていた画面のVRTと必要なStory作成の自動化は達成できたので、よかったなぁという気持ちです :clap: 


[^1]: 大量のStoryから重複したパターンの検知や、削除されたCSSパターン分のStoryの削除など人間がやるには辛い作業...
[^2]: Visual Studio Codeのsnippets機能については[こちら](https://code.visualstudio.com/docs/editor/userdefinedsnippets)
[^3]: 利用しているsnapshotライブラリの[storycap](https://github.com/reg-viz/storycap)ではStoryのParameterにdelayを設定することで、snapshotのタイミングを遅延することができます。