Closed7

[Tauri] Tauri の React アプリを Vitest でテストする

ピン留めされたアイテム
はっぱはっぱ
  • Tauri App の作成
yarn create tauri-app

? What is your app name? tauri-app
? What should the window title be? Tauri App
? What UI recipe would you like to add? create-vite
? Add "@tauri-apps/api" npm package? Yes
? Which vite template would you like to use? react-ts
はっぱはっぱ

インストール

yarn add -D vitest jsdom @vitest/coverage-c8
yarn add -D @testing-library/{react,jest-dom,user-event,dom}
はっぱはっぱ

設定

https://vitest.dev/config/#configuring-vitest

vite.config.ts
+ /// <reference types="vitest" />
  import { defineConfig } from 'vite';
  import react from '@vitejs/plugin-react';
  
  // https://vitejs.dev/config/
  export default defineConfig({
    plugins: [react()],
+   test: {
+     globals: true,
+     environment: 'jsdom',
+     coverage: {
+       enabled: true,
+       reporter: ['text'],
+     },
+   },
  });
tsconfig.json
  {
    "compilerOptions": {
+     "types": ["vitest/globals"]
    }
  }
package.json
    "scripts": {
      "dev": "vite",
      "build": "tsc && vite build",
      "preview": "vite preview",
      "tauri": "tauri",
+     "test": "vitest"
    }
.gitignore
+ coverage/tmp
  
  # Logs
  logs
  *.log
はっぱはっぱ

React アプリ

  • count ステートが更新されたらウィンドウのタイトルバーへ反映
  • Rust へ投げる処理を tauri.invoke() で呼び出す
src/App.tsx
import { useEffect, useState } from 'react';
import reactLogo from './assets/react.svg';
import './App.css';

import { invoke } from '@tauri-apps/api/tauri';
import { getCurrent } from '@tauri-apps/api/window';

function App() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    getCurrent().setTitle(`Tauri App: ${count}`);
  }, [count]);

  return (
    <div className="App">
      <div>
        <a href="https://vitejs.dev" target="_blank">
          <img src="/vite.svg" className="logo" alt="Vite logo" />
        </a>
        <a href="https://reactjs.org" target="_blank">
          <img src={reactLogo} className="logo react" alt="React logo" />
        </a>
      </div>
      <h1>Vite + React</h1>
      <div className="card">
        <button
          data-testid="count"
          onClick={() => setCount((count) => count + 1)}
        >
          count is {count}
        </button>
        <button
          data-testid="add"
          onClick={() => {
            invoke('add', { a: 1, b: 2 }).then((result) =>
              console.log(`result: ${result}`)
            );
          }}
        >
          Add
        </button>
      </div>
      <p className="read-the-docs">
        Click on the Vite and React logos to learn more
      </p>
    </div>
  );
}

export default App;

はっぱはっぱ

Rust サイド

invoke_handler へ add 関数を登録。

https://tauri.app/v1/guides/features/command

src-tauri/src/main.rs
#[tauri::command]
async fn add(a: i32, b: i32) -> i32 {
    a + b
}

fn main() {
    tauri::Builder::default()
        .invoke_handler(tauri::generate_handler![add])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

Tauri ウィンドウにラベルを付けておく。

src-tauri/tauri.config.json
  {
    "tauri": {
      "allowlist": {
        "all": true
      },
      "windows": [
        {
          "title": "Tauri App",
+         "label": "main"
        }
      ]
    }
  }
はっぱはっぱ

テストを書く

https://tauri.app/v1/guides/testing/mocking

src/App.test.tsx
import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import { randomFillSync } from 'node:crypto';
import { mockIPC, mockWindows } from '@tauri-apps/api/mocks';

import App from './App';

/** window.crypto の拡張が必要らしい */
beforeAll(() => {
  window.crypto = {
    // @ts-ignore
    getRandomValues: (buffer: NodeJS.ArrayBufferView) => {
      return randomFillSync(buffer);
    },
  };
});

test('render App component', async () => {
  // 'tauri.invoke()' をモック
  mockIPC((cmd, args) => {
    if (cmd === 'add') {
      return (args.a as number) + (args.b as number);
    }
  });

  // '@tauri-apps/api/window' をモック
  mockWindows('main');
  const { getCurrent } = await import('@tauri-apps/api/window');
  expect(getCurrent()).toHaveProperty('label', 'main');

  // <App /> をレンダー
  render(<App />);

  const countButton = screen.getByTestId('count');
  await userEvent.click(countButton);
  expect(countButton).toHaveTextContent('count is 1');

  // 発生するIPC通信をスパイ
  const spy = vi.spyOn(window, '__TAURI_IPC__');

  await userEvent.click(screen.getByTestId('add'));
  expect(spy).toHaveBeenCalled();
});
このスクラップは2022/06/26にクローズされました