Viteで作成した環境変数のあるプロジェクトにCIを実装する

プロジェクトをリモートリポジトリにpushした際にテストを自動で行うことを目的としています。
※すでにプロジェクトがあることが前提です。
※.envに格納した環境変数を用いてSupabaseに接続するアプリケーションです。
※本記事ではテストファイル作成時にモック関数を使用しません。

## A : テスト環境導入
A-1: jestとreact-testing-libraryのインストール
```sh
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event babel-jest @babel/preset-env @babel/preset-react
```
A-2: jest.config.mjs ファイルの作成
```javascript
export default {
    testEnvironment: "jsdom",
    moduleNameMapper: {
      "\\.(css|less)$": "identity-obj-proxy",
    },
    setupFilesAfterEnv: ["./jest.setup.js"],
  };
```
A-3: dotenvのインストール(.env内の環境変数を使用するため)
```sh
  npm i dotenv
``` 
A-4: jest.setup.js ファイルの作成
```javascript
import "@testing-library/jest-dom";

// dotenvの設定
require("dotenv").config();
```

A-5 : babelのインストール
```sh
npm install --save-dev jest-environment-jsdom @babel/core @babel/preset-env
```
A-6: .babelrc ファイルの作成
```.babelrc
{
    "presets": [
      "@babel/preset-env",
      ["@babel/preset-react", {
        "runtime": "automatic"
      }]
    ]
  }
  ```

## B : 既存のファイル調整
B-1: package.jsonの更新("test": "jest",を加える)
```json
  "scripts": {
    "test": "jest",
    "dev": "vite",
    "build": "vite build",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  }
  ```
B-2: .eslintrc.cjsを修正する(利用していない変数がエラーになるのを防ぐ)
```javascript
module.exports = {
  root: true,
  env: { browser: true, es2020: true },
  extends: [
    'eslint:recommended',
    'plugin:react/recommended',
    'plugin:react/jsx-runtime',
    'plugin:react-hooks/recommended',
  ],
  ignorePatterns: ['dist', '.eslintrc.cjs'],
  parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
  settings: { react: { version: '18.2' } },
  plugins: ['react-refresh'],
  rules: {
    'react/jsx-no-target-blank': 'off',
    'react-refresh/only-export-components': [
      'warn',
      { allowConstantExport: true },
    ],
    "no-unused-vars": false
  },
}
```
B-3    : 環境変数呼び出し修正

　コード内でimport.meta~で環境変数を参照している場合は,　process.env~に変更しておく
  (import.meta は ES Modules（ESM）の機能であり、CommonJS 環境ではサポートされない為)
```diff_javascript
//例 src/supabase.js
import { createClient } from '@supabase/supabase-js';

- const supabaseUrl = import.meta.VITE_SUPABASE_URL;
- const supabaseAnonKey = import.meta.VITE_SUPABASE_ANON_KEY;
+ const supabaseUrl = process.env.VITE_SUPABASE_URL;
+ const supabaseAnonKey = process.env.VITE_SUPABASE_ANON_KEY;

export const supabase = createClient(supabaseUrl, supabaseAnonKey);
```
B-4 : vite.config.jsの修正

:::note
prefix: 環境変数のプレフィックスを指定しています。VITE_で始まる環境変数が対象になります
:::

:::note
mountedPath: 環境変数がマウントされるオブジェクトを指定します。ここではprocess.envとして設定しています
:::

```diff_javascript
import { defineConfig } from "vite"
import react from "@vitejs/plugin-react"
import env from "vite-plugin-env-compatible";

export default defineConfig({
    plugins: [
    react(), 
    env({ prefix: "VITE", mountedPath: "process.env" }) // 環境変数プラグインを追加
  ],
  build: {
    outDir: "dist"
  }
});
```

## C : テストファイルの作成・実行
C-1 :  src/tests/sample.spec.js ファイルを作成

ここでは例として3つのテストを作成します。

①should display the title - アプリケーションのタイトルが正しく表示されるか
②should add and delete a new record - Add機能とDelete機能が正しく動作するか
③should show an error if inputs are empty - 入力フィールドが空のままAddボタンを押したときにエラーメッセージが表示されるか

```javascript
import { render, screen, fireEvent, waitFor } from '@testing-library/react'; // テストに必要なメソッドをインポート
import { App } from '../App'; // テスト対象のAppコンポーネントをインポート
import { supabase } from '../supabase'; // supabaseインスタンスをインポート

// テストを実行する前に、既存のデータをクリア
beforeAll(async () => {
  // テーブルをクリア
  await supabase.from('sample-app').delete().neq('id', 0);
});

test('should display the title', async () => {
  render(<App />); // Appコンポーネントをレンダリング

  // "Loading..."テキストが表示されなくなるまで待つ
  await waitFor(() => {
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });

  // テストID 'title' を持つ要素を取得
  const titleElement = await screen.findByTestId('title');
  // タイトルのテキスト内容が「サンプルアプリ」であることを確認
  expect(titleElement).toHaveTextContent('サンプルアプリ');
});

test('should add and delete a new record', async () => {
  render(<App />); // Appコンポーネントをレンダリング

  // "Loading..."テキストが表示されなくなるまで待つ
  await waitFor(() => {
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });

  // "str_1"のプレースホルダーを持つ入力フィールドに' A'を入力
  fireEvent.change(screen.getByPlaceholderText('str_1'), { target: { value: 'A' } });
  // "val_1"のプレースホルダーを持つ入力フィールドに' 100'を入力
  fireEvent.change(screen.getByPlaceholderText('val_1'), { target: { value: '100' } });
  // "Add"ボタンをクリック
  fireEvent.click(screen.getByText('Add'));

  // "Delete"ボタンを全て取得
  const deleteButtons = screen.getAllByText('Delete');
  // 最後のDeleteボタンを取得
  const lastDeleteButton = deleteButtons[deleteButtons.length - 1];
  // 最後のDeleteボタンをクリック
  fireEvent.click(lastDeleteButton);
});

test('should show an error if inputs are empty', async () => {
  render(<App />); // Appコンポーネントをレンダリング

  // "Loading..."テキストが表示されなくなるまで待つ
  await waitFor(() => {
    expect(screen.queryByText('Loading...')).not.toBeInTheDocument();
  });

  // "Add"ボタンをクリック（入力が空の状態）
  fireEvent.click(screen.getByText('Add'));

  // エラーメッセージが表示されるまで待つ
  await waitFor(() => {
    expect(screen.getByText('入力されていない項目があります')).toBeInTheDocument();
  });
});
```

C-2 :  sample.spec.jsの実行
```sh
npm run test
```
※上記を実行して下記のように表示されれば成功です
　この記事の記述が足りずエラーが起きたらごめんなさい・・・
```sh
PASS  src/tests/sample.spec.js

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total
Snapshots:   0 total
Time:        4.036 s
Ran all test suites.
```

## D : GitHubActionsでテストを実行する
  D-1 :  githubのSecrets を作成する

  ・対象リポジトリのSettingを開く
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784604/49582e16-16a1-807f-36f2-39696c44048d.png)
・Secrets and ~ のActionsを選択
  ![スクリーンショット 2024-07-29 151935.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784604/b5281521-1a59-bf01-401a-55e85d2760ce.png)
・New repository secrets　から　SUPABASE_ANON_KEY　と　SUPABASE_URL を登録します
  ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784604/c0b8e1d2-a261-c03f-d68f-85b6d9d1755c.png)
    ↓↓↓↓↓↓
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784604/237bf3a2-ac31-c9af-677b-faaea04cb650.png)


  D-2 :  github/workflows/test.yml を作成する
※Node.jsのバージョンは環境に合わせて選択してください
  ```yml
name: Push test
# ワークフローの名前。GitHub ActionsのUIに表示されます。

on:
  push:
    branches:
      - main
    # 'main'ブランチへのプッシュイベントでこのワークフローがトリガーされます。
  workflow_dispatch:
    # 手動でワークフローをトリガーできるようにします。GitHub UIから手動実行可能になります。

jobs:
  test:
    runs-on: ubuntu-latest
    # このジョブは最新のUbuntu環境で実行されます。

    steps:
    - name: Checkout code
      uses: actions/checkout@v3
      # リポジトリのコードをチェックアウトするためのGitHub公式アクションです。

    - name: Set up Node.js
      uses: actions/setup-node@v3
      with:
        node-version: '18'
      # Node.jsをセットアップするためのアクションです。ここではNode.jsバージョン18を指定しています。

    - name: Install dependencies
      run: npm install
      # プロジェクトの依存関係をインストールするためのコマンドです。

    - name: Run Jest tests
      env:
        VITE_SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
        VITE_SUPABASE_ANON_KEY: ${{ secrets.SUPABASE_ANON_KEY }}
      run: npm test
      # Jestテストを実行します。環境変数はGitHub Secretsから取得します。
      # `VITE_SUPABASE_URL`と`VITE_SUPABASE_ANON_KEY`は、セキュリティのためにリポジトリのSecretsから取得します。
```

D-3 :  gitにpushする
```sh
git add .
git commit -m "AutoTest_v1"
git push origin main
```
push後にリポジトリのActionsからさらに入っていって詳細を見ることができます。
緑の✅は正常に完了したことを表しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784604/434a4dba-fd1c-bd02-3c55-52f175a30ee6.png)

エラーが起こった際には詳細を確認することもできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3784604/e9a5e97d-05c0-5466-36f9-0eeb3c8379fc.png)

以上で終了です！お疲れ様でした！