Next.jsでJestを実行した際にvanilla-extractがうまく読み込まれない問題を暫定的に解決させる

JestのmoduleNameMapperをごにょごにょして解決しました
2024.06.07

こんにちは、CX事業本部のmorimorikochanです。

表題の通りNext.jsで構築したフロントエンドでJestを実行させようとしていたのですが、vanilla-extractがうまく読み込むことができませんでした。
あれこれ試行錯誤して暫定的な解決方法がわかったので、もし同じ問題ではまっている人のために共有したいと思います。

問題

vanilla-extractのフォーマットに沿った*.css.tsをimportしているコンポーネントのテストをJestで書くと、Jest実行時に*.css.tsの中身が読み込まれずundefinedになってしまいエラーが出てしまいます。

テスト対象コンポーネント

import { ButtonStyles as styles } from './button.css'
// ...
export const ButtonA: FC<ButtonProps> = ({ type, children }) => (
  <button type={type} className={clsx([styles.color])}>
    {children}
  </button>
)
  ● Buttonのテスト › `isSubmit`がONの場合、typeが`submit`になっていること

    TypeError: Cannot read properties of undefined (reading 'color')

      12 |  */
      13 | export const ButtonA: FC<ButtonProps> = ({ type, children }) => (
    > 14 |   <button type={type} className={clsx([styles.color])}>
         |

このときjest.config.jsは以下のような状態です。

const nextJest = require('next/jest')
const createJestConfig = nextJest({
  dir: './',
})

// ...

const customJestConfig = {
  moduleDirectories: ['node_modules', '<rootDir>/'],
  testEnvironment: 'jsdom',
  transform: { '^.+\\.css\\.ts$': '@vanilla-extract/jest-transform' },
}

module.exports = createJestConfig(customJestConfig)

この問題の原因は、Jestが*.css.tsを読み込む場合、あらかじめtransformに設定された@vanilla-extract/jest-transformによって[hash].cssに変換されるのですが、それをJestがmoduleNameMapperを通して読み込む際にモックファイル(styleMock.js)が優先されてしまい本来読み込ませたい[hash].cssが読み込めなくなってしまうためでした。

// Next.js(next/jest)が提供する`moduleNameMapper`はこんな感じになってる。これにvanilla-extractのcssファイルがマッチしてしまう
{
  '^.+\\.(css|sass|scss)$': '/Users/xxxxx/yyyyy/node_modules/next/dist/build/jest/__mocks__/styleMock.js'
}

これらのモックファイルの仕組みはNext.jsが提供している仕組みであり、グローバルなcssファイル(やsassやscss)をJestで読み込めるように用意された仕組みです。
実はvanilla-extractの公式サイトではこの問題と回避策がすでに記載されています

2パターン紹介されていますが、いずれもvanilla-extractによって作成されたcssファイルとこれらのcssファイルを区別する方法です。
しかし、実際はこの方法で解決できないことが多いのではないかと思います。

例えば外部パッケージから読み込むcssファイルはファイル名やディレクトリが変更できないので、その場合は結果的に区別することができません。
だとすると逆にvanilla-extractが吐き出すcssファイルを区別できるような命名規則に変更(例えば[hash].vanilla.css)できれば解決するのですが、残念ながら今現在vanilla-extractからこの設定は提供されていません。

もしかするとJestに vanilla-extractが吐き出したCSSファイルをさらにrenameするtransform を追加すればこれらが区別できそうですが、Jest力がないため諦めました。。

暫定的な対策

暫定的な対策としては、以下のようにグローバルなcssファイルをimportしているファイルはJestでは一切読み込まないようにするしかないかと思っています。
実際、Next.jsではグローバルなCSSファイルはpages/_app.jsでしか読み込むことができないため、このpages/_app.jsをJestで読み込まなければ正常にテストできるはずです

const nextJest = require('next/jest')

// ...

module.exports = async () => {
  const config = await createJestConfig(customJestConfig)()

  // WARN: キーはNext.jsのバージョンなどにより変化する可能性があります
  delete config.moduleNameMapper['^.+\\.(css|sass|scss)$']

  // WARN: なぜか`customJestConfig.transform`に含めても動作しないのでここで定義
  config.transform = {
    '^.+\\.css\\.ts$': '@vanilla-extract/jest-transform',
    ...config.transform,
  }
  return config
}

また、別問題ですが私の環境では transform@vanilla-extract/jest-transformを追加する記述がcreateJestConfigに含まれていても正常に動作しませんでした。
なのでcreateJestConfigが解決した後のconfigに強制的に追加しています。

 FAIL  tests/unit/atoms/Button.test.tsx
  ● Test suite failed to run

    Styles were unable to be assigned to a file. This is generally caused by one of the following:

    - You may have created styles outside of a '.css.ts' context
    - You may have incorrect configuration. See https://vanilla-extract.style/documentation/getting-started