🏆【検証中】 Puppeteer を使用した YAML ベースの自動テストフレームワーク

## 🏆 Puppeteer を使用した YAML ベースの自動テストフレームワーク（JSON 洗替・アラート描画機能付き）

こんにちは！今回は Puppeteer を使用して、YAML で管理できる自動テストフレームワーク を作成しました。
Webページの操作や検証 (assert)、エラーハンドリング、スクリーンショット撮影などの機能をサポートし、テスト結果を CSV に記録します。
Puppeteer を使って効率的に Web UI テストを自動化したい方におすすめです！💡

**現時点検証中です!!**

## 前提条件

サービスチェックするWEBページはjquery必須です。
その場合は下記のように別途読み込ませたりなんかすれば使えると思います。
よしなに。

```javascript
await page.addScriptTag({ url: JQUERY_URL });
```

## 🎯 実装する機能
- ✅ Puppeteer での Web UI 自動テスト
- ✅ alert や confirm が出たときに、画面に描画しスクリーンショットを撮影
- ✅ SessionStorage の内容を画面に描画しスクリーンショットを撮影
- ✅ JSON 洗替機能（スタブ設定とかテスト中に書き換えちゃったり）
- ✅ YAML でテストケースを管理（テストケースいっぱい作るんだから極力簡略化する）
- ✅ Puppeteer で画面操作 (navigate, input, click, wait, assert, screenshot, Session)
- ✅ assert の種類 (exists, count, text, isShow)
- ✅ エラーハンドリング
  - assert 失敗時 → CSV にエラーを記録 & スクショ撮影
  - wait の30秒タイムアウト → CSV に記録 & スクショ撮影
  - navigate 失敗時 → CSV に記録 & スクショ撮影
  - 致命的エラー発生時 → スクショ撮影 & browser.close() でテスト強制終了
- ✅ スクリーンショット撮影
  - 要素指定がなければ 全画面
  - 要素指定があれば 赤枠をつけて、その周辺100px を含めて撮影 → 赤枠削除
- ✅ SessionStorage の表示
  - alert、confirm と同様に SessionStorage の内容を画面に描画 → スクショ撮影 → 削除
- ✅ テスト結果を CSV に記録 (test_results.csv)

## テスト名, 成功/失敗, エラー内容, スクリーンショットパス
### 📌 環境構築
まず、必要なパッケージをインストールします。

```sh
npm init -y
npm install --ignore-scripts puppeteer
npm install yaml fs-extra csv-writer
```

### 📌 テストフレームワークのコード
#### 1️⃣ YAML のパースとテスト実行 (testRunner.js)
```javascript
const puppeteer = require('puppeteer');
const fs = require('fs-extra');
const yaml = require('yaml');
const path = require('path');
const { createObjectCsvWriter } = require('csv-writer');

const testResults = [];
const screenshotsDir = path.join(__dirname, 'screenshots');
fs.ensureDirSync(screenshotsDir);

async function runTest(filePath) {
    const browser = await puppeteer.launch({ headless: false });
    const page = await browser.newPage();
    await page.setViewport({ width: 1280, height: 720 });

    // Alert & Confirm のハンドリング
    page.on('dialog', async dialog => {
        const message = dialog.message();
        await drawMessage(page, message);
        await takeScreenshot(page, `alert_${Date.now()}.png`);
        await dialog.dismiss();
        await removeMessage(page);
    });

    const file = fs.readFileSync(filePath, 'utf8');
    const testCases = yaml.parse(file);

    for (const testCase of testCases.tests) {
        console.log(`Running: ${testCase.name}`);
        let success = true;
        let errorMsg = '';

        try {
            for (const step of testCase.steps) {
                await executeStep(page, step);
            }
        } catch (error) {
            success = false;
            errorMsg = error.message;
            await takeScreenshot(page, `error_${testCase.name}.png`);
        }

        testResults.push({
            test_name: testCase.name,
            status: success ? 'Success' : 'Failed',
            error: errorMsg,
            screenshot: success ? '' : `screenshots/error_${testCase.name}.png`,
        });
    }

    await browser.close();
    await saveResults();
}

async function executeStep(page, step) {
    switch (step.action) {
        case 'navigate':
            await page.goto(step.url);
            break;
        case 'input':
            await page.type(step.selector, step.text);
            break;
        case 'click':
            await page.click(step.selector);
            break;
        case 'wait':
            await page.waitForSelector(step.selector, { timeout: 30000 });
            break;
        case 'assert':
            await assertCondition(page, step);
            break;
        case 'screenshot':
            await takeScreenshot(page, step.filename, step.selector);
            break;
        case 'Session':
            await handleSessionStorage(page);
            break;
        case 'jsonOverwrite':
            await overwriteJson(step.filePath, step.data);
            break;
        default:
            throw new Error(`Unknown action: ${step.action}`);
    }
}

async function assertCondition(page, step) {
    switch (step.type) {
        case 'exists':
            if (!(await page.$(step.selector))) throw new Error(`Assertion failed: ${step.selector} does not exist.`);
            break;
        case 'count':
            if ((await page.$$(step.selector)).length !== step.value) throw new Error(`Assertion failed: Count mismatch.`);
            break;
        case 'text':
            if ((await page.$eval(step.selector, el => el.textContent)) !== step.value) throw new Error(`Assertion failed: Text mismatch.`);
            break;
        case 'isShow':
            if (!(await page.$(step.selector))) throw new Error(`Assertion failed: ${step.selector} is not visible.`);
            break;
        default:
            throw new Error(`Unknown assert type: ${step.type}`);
    }
}

async function takeScreenshot(page, filename, selector = null) {
    const filePath = path.join(screenshotsDir, filename);
    if (selector) {
        await page.evaluate(s => { document.querySelector(s).style.border = '2px solid red'; }, selector);
        await page.screenshot({ path: filePath, clip: await page.$eval(selector, el => el.getBoundingClientRect()) });
        await page.evaluate(s => { document.querySelector(s).style.border = ''; }, selector);
    } else {
        await page.screenshot({ path: filePath });
    }
}

async function handleSessionStorage(page) {
    const sessionData = await page.evaluate(() => JSON.stringify(sessionStorage));
    await drawMessage(page, `SessionStorage: ${sessionData}`);
    await takeScreenshot(page, 'session_storage.png');
    await removeMessage(page);
}

async function overwriteJson(filePath, newData) {
    try {
        await fs.writeJson(filePath, newData, { spaces: 2 });
        console.log(`JSON overwritten: ${filePath}`);
    } catch (error) {
        throw new Error(`Failed to overwrite JSON: ${filePath}`);
    }
}

// メッセージを画面に描画
async function drawMessage(page, message) {
    await page.evaluate(msg => {
        const div = document.createElement('div');
        div.id = 'test-message';
        div.style.position = 'fixed';
        div.style.top = '10px';
        div.style.left = '50%';
        div.style.transform = 'translateX(-50%)';
        div.style.backgroundColor = 'black';
        div.style.color = 'white';
        div.style.padding = '10px';
        div.style.borderRadius = '5px';
        div.style.fontSize = '16px';
        div.style.zIndex = '9999';
        div.textContent = msg;
        document.body.appendChild(div);
    }, message);
}

// メッセージを削除
async function removeMessage(page) {
    await page.evaluate(() => {
        const msg = document.getElementById('test-message');
        if (msg) msg.remove();
    });
}

async function saveResults() {
    const csvWriter = createObjectCsvWriter({
        path: 'test_results.csv',
        header: [
            { id: 'test_name', title: 'Test Name' },
            { id: 'status', title: 'Status' },
            { id: 'error', title: 'Error Message' },
            { id: 'screenshot', title: 'Screenshot Path' },
        ],
    });
    await csvWriter.writeRecords(testResults);
    console.log('Test results saved to test_results.csv');
}

const yamlFilePath = process.argv[2] || 'test.yaml';
runTest(yamlFilePath);
```

#### 📌 2️⃣ サンプル YAML (test.yaml)
```yaml
tests:
  - name: "Google検索テスト"
    steps:
      - action: "jsonOverwrite"
        filePath: "./data.json"
        data:
          key1: "value1"
          key2: "value2"
      - action: "navigate"
        url: "https://www.google.com"
      - action: "input"
        selector: "input[name='q']"
        text: "Puppeteer"
      - action: "click"
        selector: "input[name='btnK']"
      - action: "wait"
        selector: "#search"
      - action: "screenshot"
        filename: "google_search_result.png"
```
#### 🚀 実行方法

```sh
node testRunner.js test.yaml
```

## 🎉 まとめ
Puppeteer を使用して YAML ベースの自動テストフレームワークを実装！
assert, SessionStorage, スクリーンショット, エラーハンドリング も対応！
テスト結果を CSV に記録！

ぜひカスタマイズして活用してください！🚀