remix (react-router v7) で動かす sqlite-wasm を vitest でテストしたい
ふとしです。
テスト書きたいですよねえ。
概要
sqlite-wasm はブラウザで動かすことを前提としているため、普通に import
すると例外が発生します。これはダイナミックインポートで解決しますが、vitest を普通に動かすと Node 上の動作となり、やはり動きませんので、vitest ブラウザモードでテストできるようにしたいと思います。
とりあえず sqlite-wasm が動くようにする
ここらへんはリファレンスや他の記事があるので適当に。
設定を書き換え、
import { reactRouter } from "@react-router/dev/vite";
import autoprefixer from "autoprefixer";
import tailwindcss from "tailwindcss";
import { defineConfig } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
export default defineConfig({
css: {
postcss: {
plugins: [tailwindcss, autoprefixer],
},
},
plugins: [reactRouter(), tsconfigPaths()],
server: {
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
},
optimizeDeps: {
exclude: ["@sqlite.org/sqlite-wasm"],
},
});
適当なセットアップ的と sqlite-wasm を利用する関数を書き、
export type Sqlite3Static = Awaited<
ReturnType<Awaited<typeof import("@sqlite.org/sqlite-wasm")>["default"]>
>;
export type Database = ReturnType<typeof connect>;
// ⚠️サーバーサイドで import すると ReferenceError: self is not defined になる
export const sqliteInstance =
typeof window === "undefined"
? undefined
: import("@sqlite.org/sqlite-wasm").then((v) => v.default());
export function connect(sqlite: Sqlite3Static) {
return new sqlite.oo1.JsStorageDb("local");
}
export function initializeTables(db: Database) {
db.exec({
sql: `
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
age INTEGER
)
`,
});
}
export function getTableNames(db: Database): string[] {
return db
.exec({
sql: `SELECT name FROM sqlite_master WHERE type='table'`,
rowMode: "object",
returnValue: "resultRows",
})
.map(({ name }) => name?.toString() || "");
}
react-router 上で呼んで、
import { useEffect, useState } from "react";
import {
connect,
getTableNames,
initializeTables,
sqliteInstance,
} from "~/libs/sqlite";
export default function Home() {
const [info, setInfo] = useState<string[]>([]);
useEffect(() => {
sqliteInstance?.then(connect).then((db) => {
initializeTables(db);
setInfo(getTableNames(db));
db.close();
});
}, []);
return <pre>{JSON.stringify(info, null, 2)}</pre>;
}
表示を確認します。
[
"users",
"sqlite_sequence"
]
テストできるようにする
テストファイル
適当なテストを書いて、
import { expect, test, vi } from "vitest";
import {
connect,
getTableNames,
initializeTables,
sqliteInstance,
} from "~/libs/sqlite";
test("has users", async () => {
// arrange
const sqlite = await sqliteInstance!;
const db = connect(sqlite);
initializeTables(db);
// act
const tables = getTableNames(db);
// assert
expect(tables).include("users");
});
このまま走らせると sqlite
が undefined
なので普通に例外で落ちます。
vitest ブラウザモードにする
vitest には実験的機能としてブラウザ上で動作させるモードがありますので、それを有効にします。
詳しい導入方法は https://vitest.dev/guide/browser/ にあるので、ササッと。
初期化して、
npx vitest init browser
vitest.config.ts
を作成。
/// <reference types="vitest" />
import { defineConfig } from "vite";
import * as path from "path";
export default defineConfig({
test: {
alias: {
"~": path.resolve(__dirname, "./app"),
},
browser: {
provider: "playwright", // or 'webdriverio'
enabled: true,
name: "chromium", // browser name is required
headless: true,
},
},
});
vitest エラーを解決する
これでテストを走らせると以下のエラーがでます。
FAIL |0| app/libs/sqlite.test.ts >
RuntimeError: Aborted(both async and sync fetching of the wasm failed). Build with -sASSERTIONS for more info.
❯ abort ../../../../node_modules/.vite/deps/@sqlite__org_sqlite-wasm.js:203:15
❯ ../../../../node_modules/.vite/deps/@sqlite__org_sqlite-wasm.js:244:9
ブラウザ上で動かす wasm ファイルは必ずいずこかからダウンロードする必要があるのですが、それが失敗しているというエラーです。
headless: false
で立ち上がるブラウザで確認するとダウンロード失敗している模様が確認できます。
node_modules/.vite/deps
は Vite がキャッシュを保存する場所?らしいのですが、任意のファイルを配置する方法はわかりませんでした。plugin などで無理やりコピーしても、タイミングが早いせいか、初回起動時は削除されてしまいます。
sqlite-wasm のダウンロード先を明示的に指定する
そこで sqlite-wasm のコードを眺めていると、wasm ファイルのダウンロード先を指定できることがわかりました。
declare type InitOptions = {
// これ
locateFile?: (path: string, prefix: string) => string;
print?: (msg: string) => void;
printErr?: (msg: string) => void;
};
vitest を利用しているときは手動配置して配信される wasm ファイルを指定することにします。
node_modules/@sqlite.org/sqlite-wasm/sqlite-wasm/jswasm/sqlite3.wasm
を public/sqlite3.wasm
としてコピーした上で sqlite-wasm
初期化メソッドに locateFile
を指定します。
const locateFile =
process.env.NODE_ENV === "test"
? (file: string, prefix: string) => {
return file === "sqlite3.wasm"
? `/${file}`
: new URL(file, prefix).toString();
}
: undefined;
// ⚠️サーバーサイドで import すると ReferenceError: self is not defined になる
export const sqliteInstance =
typeof window === "undefined"
? undefined // 👇👇👇
: import("@sqlite.org/sqlite-wasm").then((v) => v.default({ locateFile }));
テストする
以上の設定でテストが完遂するようになりました。
✓ |0| app/libs/sqlite.test.ts (1)
✓ has users
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 12:39:52
Duration 868ms (transform 0ms, setup 0ms, collect 16ms, tests 77ms, environment 0ms, prepare 247ms)
まとめ
テストできなくて散々ググったのですが、みなさん特に困っておられないので、自分のためにとりあえずの解決策をひねり出しました。
スマートな方法をご存知の方はご教示いただければ幸いです。
Discussion