docs/app.html に1200行のJavaScriptがインラインで埋まっている。
TypeScriptに移行するが、TDD方式で進める:まずテストで「現在の正しい動作」を記録し、その後TypeScriptに書き換え、テストが通ることで動作の同一性を保証する。
現状: HTML + CSS + JS が1ファイル(1940行)に同居。varベース、IIFE ラッパー。
ゴール: テストで守りながら src/app.ts に抽出し、esbuild で docs/app.js にビルド。
reinfolib-service/
├── package.json # 新規: esbuild + typescript + vitest
├── tsconfig.json # 新規: DOM + ESNext(workers/ とは別)
├── vitest.config.ts # 新規: vitest 設定
├── src/
│ ├── app.ts # メインロジック(~1200行)
│ ├── types.ts # API レスポンス型・内部データ型
│ └── __tests__/
│ ├── stats.test.ts # 統計計算のテスト
│ ├── zoning.test.ts # 用途地域ヘルパーのテスト
│ ├── address.test.ts # 住所解析のテスト
│ ├── building.test.ts # 建物残価推定のテスト
│ └── format.test.ts # 表示フォーマットのテスト
├── docs/
│ ├── app.html # CSS + HTML のみ
│ ├── app.js # esbuild 出力(コミット対象)
│ ├── index.html # LP(変更なし)
│ └── stations.json # 駅データ(変更なし)
└── workers/ # 変更なし
| 判断 | 結論 | 理由 |
|---|---|---|
| テストFW | vitest | CLAUDE.md 指定。esbuild 互換。TS ネイティブ対応 |
| ビルドツール | esbuild | 設定ファイル不要。1コマンドで完結 |
| ファイル分割 | 2ファイル(app.ts + types.ts) | KISS優先。テストは別ディレクトリ |
| モノレポ設定 | 独立した root package.json | workers/ の tsconfig は DOM 型なし。共有不可 |
| TS 厳格度 | strict: true | DOM要素は as HTMLInputElement 等で型アサーション |
| ビルド出力 | docs/app.js をコミット | GitHub Pages は CI なし |
| CSP | unsafe-inline → self | 外部JSファイル化でCSP強化 |
新規作成:
/package.json — esbuild + typescript + vitest/tsconfig.json — strict: true, lib: ["DOM", "ESNext"], target: "ES2017"/vitest.config.ts — happy-dom でDOM API をエミュレート{
"scripts": {
"build": "esbuild src/app.ts --bundle --outfile=docs/app.js --target=es2017",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"test:watch": "vitest",
"dev": "esbuild src/app.ts --bundle --outfile=docs/app.js --target=es2017 --watch"
}
}
t-wada の「仕様化テスト」アプローチ リファクタリング前にテストを書く手法を「仕様化テスト(Characterization Test)」という。 t-wada は「テストがないコードはレガシーコード」と言う。移行前にテストで現在の動作を"写真に撮る"ことで、移行後に何が壊れたかを即座に検出できる。純粋関数(=ブラウザの画面操作に依存しない関数)に絞るのは「テストの信頼性と速度を最大化する」ため。
document.getElementByIdやaddEventListenerを使う関数はブラウザ環境がないと動かないのでテストが不安定になる。一方、「数値を渡したら数値が返る」ような関数は環境に左右されず、高速・確実にテストできる。
テスト対象は DOM非依存の純粋関数(=ブラウザの画面操作を使わない、入力→出力だけで完結する関数)に絞る。DOM操作(画面の要素を取得・変更する処理)はブラウザ手動確認。
既存 docs/app.html のコードから関数を export 可能な形で抽出しつつ、テストを書く。
stats.test.ts — 統計計算(最も重要。坪単価の正確性に直結)
calcTsuboStats(items) — 土地のみ取引から坪単価を算出calcBuildingLandStats(items) — 建物込み取引から推定坪単価を算出removeOutliers(results) — 異常値除外(中央値の1/5未満を除外)median(arr) — 中央値計算summarizeStats(results) — 統計サマリー(min/max/median)summarizeByUseDistrict(results) — 用途地域別集計zoning.test.ts — 用途地域ヘルパー
getUseDistrict(item) — UseDistrict/CityPlanning フォールバックgetZoningCategory(useDistrict) — 大分類判定(住居系/商業系/工業系)address.test.ts — 住所解析
getPrefName(muniCd) — 市区町村コード→都道府県名getDistrictName(lv01Nm) — 地名から丁目を除去filterByDistrict(data, pref, muni, district) — 地区フィルタbuilding.test.ts — 建物残価推定
convertJapaneseYear(yearStr) — 和暦→西暦変換classifyStructure(structure) — 構造分類estimateBuildingResidual(item) — 建物残価推定format.test.ts — 表示フォーマット
formatManYen(value) — 万円表示formatPrice(value) — 億/万円表示APIレスポンスの型定義。テストのテストデータにも使用。
export するvar → const / letnpm test)鉄則: テストが失敗しても、テストを書き換えてはいけない テストは Step 2 で「現在の正しい動作」を記録したもの。失敗したら、それは実装(
src/app.ts)のバグ。テストを直すのではなく、実装を直してテストを通す。テストを書き換えると「何が正しい動作か」の基準が失われ、TDD の意味がなくなる。
ポイント: DOM操作部分(イベントハンドラ、レンダリング)は export しない。 テスト可能な「計算ロジック」と「UI操作」を分離する。これが移行の最大の価値。
テストが通った状態で安全にリファクタ:
for (var i...) → for...of / .map() / .filter() に適宜変換function(x) {} → アロー関数に適宜変換npm test で確認<script>...</script>(1200行)→ <script src="app.js"></script>(1行)script-src 'unsafe-inline' → script-src 'self'npm run typecheck # 型エラー 0
npm test # 全テスト PASS
npm run build # docs/app.js 生成
# ブラウザで手動確認
tests/test_e2e.py)と同等の網羅性を目指すtest_e2e.py の MOCK_TRANSACTIONS と同じデータを使用| ファイル | 操作 | 内容 |
|---|---|---|
package.json |
新規 | esbuild + typescript + vitest |
tsconfig.json |
新規 | フロントエンド用 TS 設定 |
vitest.config.ts |
新規 | vitest 設定(happy-dom) |
src/types.ts |
新規 | 型定義(~80行) |
src/app.ts |
新規 | メインロジック(~1200行、関数を export) |
src/__tests__/*.test.ts |
新規 | テスト5ファイル |
docs/app.html |
編集 | script タグ→外部参照、CSP 更新 |
docs/app.js |
生成 | esbuild 出力(コミット対象) |
npm test — 全テスト PASSnpm run typecheck — 型エラー 0npm run build — esbuild 正常終了docs/app.html を開き、住所検索・駅名検索が動作することpytest tests/test_e2e.py)も引き続き PASS