📊
CSVマッピングツールを改善した!ツアー機能・E2Eテスト・複数カラムの結合
はじめに
前回の記事で紹介した「ブラウザで完結するCSVマッピングツール」に、いくつかの機能を追加しました。
デモサイト: https://tamoco-mocomoco.github.io/csv-mapping-tool/
今回追加した機能は以下の3つです:
- インタラクティブなツアー機能 - 初めてのユーザー向けガイド
- E2Eテストとci設定 - Playwright + GitHub Actions
- 複数カラム結合機能 - 「姓」+「名」→「氏名」のような結合
1. インタラクティブなツアー機能
課題
ツールを公開したものの、初めて使うユーザーにとっては操作方法が分かりにくいという問題がありました。READMEやドキュメントを読まなくても、直感的に使い方を理解できるようにしたいと考えました。
解決策:Shepherd.js
Shepherd.jsを使って、インタラクティブなツアー機能を実装しました。

実装のポイント
import Shepherd from 'shepherd.js';
export function useTour(actions?: TourActions) {
const startTour = useCallback(() => {
const tour = new Shepherd.Tour({
useModalOverlay: true,
defaultStepOptions: {
cancelIcon: { enabled: true },
classes: 'shepherd-theme-custom',
scrollTo: { behavior: 'smooth', block: 'center' },
},
});
tour.addStep({
id: 'welcome',
text: `
<h3>CSV マッピングツールへようこそ!</h3>
<p>このツアーでは、サンプルデータを使って実際の操作を体験できます。</p>
`,
buttons: [
{ text: 'スキップ', action: tour.cancel, secondary: true },
{ text: '始める', action: tour.next },
],
});
// 各ステップを追加...
tour.start();
}, [actions]);
return { startTour, shouldShowTour, resetTour };
}
ツアーの流れ
| ステップ | 内容 |
|---|---|
| 1. ウェルカム | ツアーの説明 |
| 2. 変換前カラム | サンプルCSVのダウンロードとインポート |
| 3. 変換後カラム | カラムの追加方法 |
| 4. CSVデータ | データのインポート |
| 5. マッピング設定 | 自動でサンプルマッピングを追加 |
| 6. プレビュー | 変換結果の確認方法 |
| 7. プロファイル | 設定の保存方法 |
工夫した点
- サンプルCSVのダウンロード機能: ツアー中にサンプルデータをダウンロードできるようにした
- サンプルマッピングの自動追加: 「次へ」ボタンを押すと、適切なマッピングが自動で設定される
- MUIとの共存: MUIのモーダルとShepherdのオーバーレイのz-index調整
/* MUIのモーダルをShepherdより上に表示 */
.MuiModal-root {
z-index: 1300 !important;
}
.shepherd-element {
z-index: 1250;
}
2. E2Eテストとci設定
Playwrightによるe2eテスト
複雑なCSV変換のシナリオをE2Eテストでカバーしました。
test('氏名の分割 + トリム', async ({ page }) => {
const csvContent = `氏名
田中 太郎
鈴木 花子 `;
// CSVをインポート
const sourceFileInput = page.locator('[data-tour="source-columns"] input[type="file"]');
await sourceFileInput.setInputFiles(csvPath);
// マッピングを設定
await page.getByRole('button', { name: 'マッピング追加' }).click();
const firstMapping = page.locator('[data-testid="mapping-row"]').first();
// コンバーター設定: 分割 → トリム
await firstMapping.locator('[data-testid="converter-type-select"]').click();
await page.getByRole('option', { name: '分割' }).click();
await firstMapping.locator('[data-testid="split-delimiter-input"] input').fill(' ');
// 結果を検証
await page.getByRole('tab', { name: '変換後' }).click();
const previewTable = page.locator('[data-tour="preview"]').locator('table');
await expect(previewTable.locator('td').filter({ hasText: '田中' })).toBeVisible();
});
テストケース
| テスト | 内容 |
|---|---|
| 氏名の分割 + トリム | 「 田中 太郎 」→ 姓:「田中」、名:「太郎」 |
| メールの正規化 | 「 TANAKA@EXAMPLE.COM 」→「tanaka@example.com」 |
| 日付の部分抽出 | 「2024-01-15」→ 年:「2024」、月:「01」、日:「15」 |
| 商品コード生成 | 「food」→「PRD-FOOD-JP」 |
| 複数カラム結合 | 姓:「田中」+ 名:「太郎」→「田中 太郎」 |
data-testid属性の活用
MUIコンポーネントはlabel属性での選択が難しいため、data-testid属性を追加しました。
<Select
multiple
value={validSourceIds}
data-testid="source-column-select"
>
{/* ... */}
</Select>
GitHub ActionsによるCI
プッシュ時に自動でテストを実行するワークフローを設定しました。
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- name: Type check
run: npm run build
- name: Run unit tests
run: npm run test:run
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npm run test:e2e
- name: Upload test results
uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
3. 複数カラム結合機能
課題
前回の記事では「今後の展望」として挙げていた機能の1つに複数カラムの結合がありました。
- 複数カラムの結合
例えば、「姓」と「名」を結合して「氏名」にしたいというユースケースです。
実装
型定義の変更
後方互換性を保ちつつ、複数カラムに対応しました。
export interface Mapping {
id: string;
sourceColumnId?: string; // 旧形式(後方互換性用)
sourceColumnIds?: string[]; // 新形式(複数カラム対応)
targetColumnId: string;
converters: ConverterConfig[];
separator?: string; // 結合時の区切り文字
}
// ヘルパー関数で統一的に扱う
export function getSourceColumnIds(mapping: Mapping): string[] {
if (mapping.sourceColumnIds && mapping.sourceColumnIds.length > 0) {
return mapping.sourceColumnIds;
}
if (mapping.sourceColumnId) {
return [mapping.sourceColumnId];
}
return [];
}
変換ロジック
mappings.forEach((mapping) => {
const sourceIds = getSourceColumnIds(mapping);
const targetColumn = targetColumns.find((c) => c.id === mapping.targetColumnId);
if (sourceIds.length > 0 && targetColumn) {
// 複数カラムの値を取得して結合
const sourceValue = sourceIds
.map((id) => {
const col = sourceColumns.find((c) => c.id === id);
return col ? (row[col.id] || '') : '';
})
.join(mapping.separator ?? '');
// コンバーターを適用
const convertedValue = applyConverters(sourceValue, converters, rowIndex);
transformedRow[targetColumn.id] = convertedValue;
}
});
UI:複数選択ドロップダウン
MUIのSelectコンポーネントでmultiple属性を使用し、選択したカラムをChipで表示します。
<FormControl size="small" sx={{ minWidth: 200 }}>
<InputLabel>変換元</InputLabel>
<Select
multiple
value={validSourceIds}
onChange={(e) => {
onUpdate({
sourceColumnIds: e.target.value as string[],
sourceColumnId: undefined,
});
}}
renderValue={(selected) => (
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
{(selected as string[]).map((id) => {
const col = sourceColumns.find((c) => c.id === id);
return <Chip key={id} label={col?.name} size="small" />;
})}
</Box>
)}
>
{sourceColumns.map((col) => (
<MenuItem key={col.id} value={col.id}>
<Checkbox checked={validSourceIds.includes(col.id)} />
<ListItemText primary={col.name} />
</MenuItem>
))}
</Select>
</FormControl>
{/* 複数選択時のみ区切り文字入力を表示 */}
{validSourceIds.length > 1 && (
<TextField
size="small"
label="区切り文字"
value={mapping.separator ?? ''}
onChange={(e) => onUpdate({ separator: e.target.value })}
/>
)}
使用例
入力CSV:
姓,名
田中,太郎
鈴木,花子
マッピング設定:
- 変換元: 姓, 名(複数選択)
- 区切り文字: スペース
- 変換先: 氏名
出力CSV:
氏名
田中 太郎
鈴木 花子
後方互換性
| データ形式 | 読み込み | 保存 |
|---|---|---|
旧形式(sourceColumnId) |
getSourceColumnIds()で配列に変換 |
新形式で保存 |
新形式(sourceColumnIds) |
そのまま使用 | そのまま保存 |
既存のプロファイルも問題なく読み込めます。
まとめ
今回は以下の機能を追加しました:
- ツアー機能: Shepherd.jsで初めてのユーザーをガイド
- E2Eテスト + ci: Playwright + GitHub Actionsで品質を担保
- 複数カラム結合: 「姓」+「名」→「氏名」のようなユースケースに対応
Discussion