<-- mermaid -->

Visual Regression Testが誤検知した動画やアニメーションの問題解決

NewsPicksのWeb Reader Experience Unitで学生インターンをしています。西(@yukinissie)です。

弊チームの開発基盤では、reg-suitstorycapを利用したVisual Regression Test(以降 VRT)を導入しています。本ブログではVRTが誤検知した動画やアニメーション周りの問題に対してどのように解決したかをそれぞれご紹介します。

VRTの誤検知とは?

弊チームではVRTを以下の6ステップで行なっており、これらはGitHub Actions(CI)上で自動化されています。

  1. Storybookでプレビューできるコンポーネント(以降、story)のソースコードを更新する
  2. Storybookをビルドする
  3. storyをstorycapでスクリーンショットする
  4. reg-suitで3.のスクリーンショットからスナップショットを作成する
  5. reg-suitで変更前のスナップショットと比較する
  6. reg-suitで外観変化に関するHTML形式のレポートを作成する

VRTを行うことで外観レベルでの意図していない機能の変化や欠損を検知することが可能になり、いわゆるデグレの防止に繋がります。

同時に意図している変化に対しても差分として検知されます。主な原因として上記3.のステップでスクリーンショットのタイミングのずれで表示されているものが違う場合があることです。

例えば動画を自動再生するコンポーネントでは以下の表中の画像*1のようにコードに変更がなくてもスナップショットAで撮影された映像は、また別のスナップショットBで撮影された映像と同じとは限りません。

スナップショットA スナップショットB

表の例では3人写っているキャラクターのうち、真ん中のキャラクターの手の位置が異なっているため、差分として検知されています。

そのため開発者は検知されたものが意図したものか、そうでないのかを判断する余計な手間を必要とします。NewsPicksの開発基盤でも同様の問題を抱えています。

行ったこと

以下2つの対応をしましたので順番にご紹介します。

  1. 既存のコードに触れずにReactPlayerの再生をテスト時には静止させる
  2. 描画に時間を要するアニメーションには撮影に遅延秒数を設定する

1. 既存のコードに触れずにReactPlayerの再生をテスト時には静止させる

先ほどの例にも挙げましたが、NewsPicksには自動再生される動画コンポーネントがあります。*2(図1中、赤枠の部分)

図1 自動再生される動画コンポーネント(図中、赤枠の部分)

弊チームでは上記の自動再生される動画コンポーネントの実装にReactPlayerを用いています。

最初から静止していれば差分はでないと考えましたのでVRT時は自動再生を無効化させてスクリーンショットを撮ることにします。

以下のようなコードが書けるとよさそうです。

<ReactPlayer {...props} playing={isVrt ? false : props.playing} />

ただし、このコードを既存のコンポーネントに直接埋め込むとすると、変更箇所が多かったりVRTを無くしたりする際に大変そうです。

ということで、既存のコードには触れず、.storybook/preview.jsに下記のコードを追記し、Storybook内ではReactPlayerの仕様をVRT向けにグローバルに上書きすることにしました。

import * as ReactPlayer from 'react-player'
import { isVrt } from 'src/utils/env' // export const isVrt = Boolean(process.env.STORYBOOK_IS_VRT)

const OriginalReactPlayer = ReactPlayer.default

// playing propsに渡される値を上書き
Object.defineProperty(ReactPlayer, 'default', {
  configurable: true,
  value: (props) => (
    <OriginalReactPlayer {...props} playing={isVrt ? false : props.playing} />
  ),
})

このように実装することでStorybookでのみ環境変数の値によって全てのReactPlayerをVRT時には静止させることができるようになりました。

また、上記のテクニックはReactPlayerに限らず、ほかのライブラリにも適用できます。たとえばnext/imageにお手製のダミー画像挿入用のメソッドを注入する場合は以下のようになります。

import * as NextImage from 'next/image'
import { getTestImageUrl, getTestImagePx } from 'src/utils/testData'

const OriginalNextImage = NextImage.default

Object.defineProperty(NextImage, 'default', {
  configurable: true,
  value: (props) => (
    <OriginalNextImage
      {...props}
      src={getTestImageUrl(props.src, {
        width: getTestImagePx(props.layout, props.width, 300),
        height: getTestImagePx(props.layout, props.height, 200),
      })}
    />
  ),
})

既存のソースコードやテストデータを編集せずにVRT時のみ動画を静止させたりダミー画像を挿入することができるようになり誤検知を減らすことができました。

2. 描画に時間を要するアニメーションには撮影に遅延秒数を設定する

NewsPicksにはサイトの表現にいくつかのアニメーションを取り入れています。1つの例として特集ページ*3の最上部にあるコンポーネントはアクセス直後、黒背景から明転してコンテンツが浮かび上がるようなアニメーションを適用しています。以下のGIFの通りです。

特集ページのアニメーション

このアニメーションを持ったコンポーネントはVRTのスクリーンショットの際に撮影時間の差で写るものが変わってしまうのでよく誤検知されていました。以下のreg-suitの結果のように、左は明転後の画像、右は明転前の黒背景の画像が写っており、これが差分として検知されてしまっていました。

アニメーションにより誤検知している様子

この問題を解決するために対象のstoryのスクリーンショットに撮影遅延秒数を設定しました。*4

以下はstorycapのREADME.mdから引用したソースコードです。storyに与えるparametersの中でscreenshotパラメータを挿入し、その中で遅延秒数delayを指定しています。例では200msが指定されています。

import React from 'react';
import MyComponent from './MyComponent';

export default {
  title: 'MyComponent',
  parameters: {
    screenshot: {
      delay: 200,
    },
  },
};

export const normal = () => <MyComponent />;
export const small = () => <MyComponent text="small" />;
small.story = {
  parameters: {
    screenshot: {
      viewport: 'iPhone 5',
    },
  },
};

このように指定することで画像の読み込みやアニメーションの終了を待った上で撮影を行うことができます。

誤検知されているコンポーネントはアニメーションに1000ms掛ける仕様のようでしたので、撮影遅延秒数はバッファを持たせて1300msを指定しました。これによって誤検知を防ぐことができました。

補足として、撮影遅延秒数は個別のstoryに対してだけでなく全てのstoryに対してもグローバルに設定することができます。.storybook/preview.jsに以下のように記述するだけです。

export const parameters = {
  screenshot: {
    delay: 300,
  },
}

全てのstoryにバッファを持たせた撮影をしたい場合に便利です。ただし、グローバルに設定した上記の値は個別のstoryで指定する値で上書きされるのでその点は注意が必要です。*5

まとめ

storycapreg-suitを利用したVRTが誤検知した場合の対処方法について紹介しました。時間が存在する限り撮影のタイミングで誤検知してしまう可能性があります。しかし、テストとして許容できるのであれば、動画を意図的に静止させたりスクリーンショットの撮影タイミングを遅延させたりすることで誤検知を回避することができることがわかりました。

VRTの導入背景や導入の流れについて知りたい方へ

VRTの導入背景や目的、導入の流れについては弊チームのじゆんきち(@junkisai)さんがブログに書いておりますのでこちらもぜひご覧ください。

tech.uzabase.com

*1:画像はreg-suitが生成したHTML形式のレポートから引用したものです。

*2:NewsPicksでは話題の経済ニュース・ビジネスに関するオリジナル動画を提供しています。こちらからアクセスできます:番組:NewsPicksオリジナル動画 - 最先端のビジネスや経済に関する動画が見放題

*3:NewsPicksでは経済に関する事柄をわかりやすくお届けするためにNewsPicks編集部が制作するオリジナル記事を提供しており、その全てを特集:NewsPicksオリジナル記事で見ることができます。

*4:1. のReactPlayerに対する対応でVRT時に動画を静止させたようにアニメーションを静止させる方法も考えられますが、追加の実装コストが大きそうでしたのでやめました。

*5:グローバルで300msの遅延を設定している際、任意のstoryに対して1000ms追加で遅延させる場合はグローバルで指定した300msを足して1300msを任意のコンポーネントに対して遅延を指定する必要があります。

Page top