Newer
Older
TelosDB / tools / debug-ui-puppeteer.mjs
#!/usr/bin/env node
/**
 * 開発サーバー (8474) を Puppeteer で開き、コンソールログとスクリーンショットを取得する。
 * 使い方:
 *   1. 先に npm run dev または dev:fast / dev:fast:pro で 8474 を起動する
 *   2. 別ターミナルで npm run debug-ui(または node tools/debug-ui-puppeteer.mjs)
 * 出力: tmp/debug-ui-console.log(コンソール), tmp/debug-ui-screenshot.png
 * 注意: ブラウザでは Tauri API がないため、設定読み込み等は 3001 の fetch にフォールバックする。
 */
import puppeteer from 'puppeteer';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const root = path.resolve(__dirname, '..');
const tmpDir = path.join(root, 'tmp');
const args = process.argv.slice(2);
const isPro = args.includes('--pro') || process.env.DEBUG_UI_PRO === '1';
const urlArg = args.filter((a) => a !== '--pro')[0];
const url = urlArg || process.env.DEBUG_UI_URL || 'http://127.0.0.1:8474';
const targetUrl = url.startsWith('file:') || url === 'file' || url === '--file'
  ? `file:///${path.join(root, 'src', 'frontend', 'index.html').replace(/\\/g, '/')}` : url;

if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });

const suffix = isPro ? '-pro' : '';
const logPath = path.join(tmpDir, `debug-ui-console${suffix}.log`);
const screenshotPath = path.join(tmpDir, `debug-ui-screenshot${suffix}.png`);
const API_BASE = 'http://127.0.0.1:3001';

const logs = [];

async function main() {
  let browser;
  try {
    browser = await puppeteer.launch({
      headless: false,
      defaultViewport: { width: 800, height: 600 },
      args: ['--no-sandbox'],
    });
    const page = await browser.newPage();
    await page.setCacheEnabled(false);

    page.on('console', (msg) => {
      const type = msg.type();
      const text = msg.text();
      const line = `[${type}] ${text}`;
      logs.push(line);
      console.log(line);
    });
    page.on('requestfailed', (req) => {
      const url = req.url();
      const line = `[requestfailed] ${url}`;
      logs.push(line);
      console.log(line);
    });
    page.on('pageerror', (err) => {
      const line = `[pageerror] ${err.message}`;
      logs.push(line);
      console.log(line);
    });
    page.on('response', (res) => {
      const status = res.status();
      const url = res.url();
      if (status === 404) {
        const line = `[404] ${url}`;
        logs.push(line);
        console.log(line);
      }
      if (url.includes('main-panel')) {
        const line = `[response] main-panel ${status} ${res.request().resourceType()}`;
        logs.push(line);
        console.log(line);
      }
    });

    if (isPro) {
      console.log('--- Pro 版チェック(有料版を優先) ---');
      console.log('バックエンド /edition を確認します(Pro 起動: npm run dev:fast:pro)');
    }

    // サーバー未起動なら即失敗。先に npm run dev:fast を起動してから本スクリプトを実行する運用。
    const waitForUrl = targetUrl.startsWith('http') ? targetUrl : 'http://127.0.0.1:8474';
    if (waitForUrl.startsWith('http')) {
      let ok = false;
      try {
        const ctrl = new AbortController();
        setTimeout(() => ctrl.abort(), 3000);
        const res = await fetch(waitForUrl, { method: 'HEAD', signal: ctrl.signal });
        ok = res.ok;
      } catch (_) {}
      if (!ok) {
        console.error('8474 に接続できません。先に npm run dev:fast' + (isPro ? ':pro' : '') + ' を起動してから npm run debug-ui を実行してください。');
        throw new Error('Server not ready');
      }
    }

    console.log('Navigating to', targetUrl, '...');
    await page.goto(targetUrl, { waitUntil: 'load', timeout: 20000 }).catch((e) => {
      console.error('Failed to open', targetUrl, '- is dev running? (npm run dev or dev:fast' + (isPro ? ':pro' : '') + ')');
      throw e;
    });

    // 起動直後: 読み込み中だけ見えているか(goto直後わずかに待って撮影)
    await new Promise((r) => setTimeout(r, 5));
    const loadingScreenshotPath = path.join(tmpDir, 'debug-ui-screenshot-loading.png');
    await page.screenshot({ path: loadingScreenshotPath, fullPage: false });
    console.log('Loading state screenshot:', loadingScreenshotPath);

    await new Promise((r) => setTimeout(r, 3000));

    /* 設定パネルを開いてトグルを確認(つまみが端まで動く・幅が適切か) */
    const settingsBtn = await page.$('.sidebar-nav .nav-item[data-panel="settings"]');
    if (settingsBtn) {
      await settingsBtn.click();
      await new Promise((r) => setTimeout(r, 500));
      const toggleCheck = await page.evaluate(() => {
        const wraps = document.querySelectorAll('.setting-toggle-wrap');
        const toggles = [];
        wraps.forEach((el, i) => {
          const r = el.getBoundingClientRect();
          const slider = el.querySelector('.setting-toggle-slider');
          const input = el.querySelector('.setting-toggle');
          toggles.push({
            index: i,
            width: Math.round(r.width),
            height: Math.round(r.height),
            checked: input ? input.checked : false,
          });
        });
        return { count: wraps.length, toggles, panelSettingsVisible: !!document.querySelector('#panel-settings:not(.hidden)') };
      });
      console.log('--- トグル確認(設定パネル) ---');
      console.log('  設定パネル表示:', toggleCheck.panelSettingsVisible);
      console.log('  トグル数:', toggleCheck.count);
      toggleCheck.toggles.forEach((t) => {
        console.log(`  トグル${t.index + 1}: 幅=${t.width}px 高さ=${t.height}px checked=${t.checked}`);
      });
      if (toggleCheck.count >= 2 && toggleCheck.toggles[0].width > 0) {
        const w = toggleCheck.toggles[0].width;
        const h = toggleCheck.toggles[0].height;
        if (w >= 28 && w <= 40 && h >= 14 && h <= 22) {
          console.log('  => トグル幅・高さ OK (2rem×1rem 想定)');
        } else if (w > 45) {
          console.log('  => トグル幅が広すぎ (2rem=約32px 想定)');
          process.exitCode = 1;
        } else {
          console.log('  => トグル寸法要確認');
        }
      }
      /* 標準フォルダONで一覧に4行出るか */
      const standardToggle = await page.$('#setting-standard-folders-enabled');
      if (standardToggle) {
        const wasChecked = await page.evaluate((el) => el.checked, standardToggle);
        if (!wasChecked) {
          await standardToggle.click();
          await new Promise((r) => setTimeout(r, 300));
        }
        const tableCheck = await page.evaluate(() => {
          const standardTbody = document.querySelector('#settings-standard-paths-tbody');
          const customTbody = document.querySelector('#settings-custom-paths-tbody');
          const standardRows = standardTbody ? standardTbody.querySelectorAll('tr') : [];
          const customRows = customTbody ? customTbody.querySelectorAll('tr') : [];
          return {
            standardRowCount: standardRows.length,
            customRowCount: customRows.length,
            firstCellText: standardRows[0]?.cells[0]?.textContent?.trim() || '',
          };
        });
        console.log('--- 標準フォルダON時 ---');
        console.log('  標準フォルダ表の行数:', tableCheck.standardRowCount, tableCheck.standardRowCount === 4 ? '(OK)' : '(期待: 4)');
        console.log('  監視フォルダ表の行数:', tableCheck.customRowCount);
        if (tableCheck.standardRowCount > 0) console.log('  先頭セル例:', tableCheck.firstCellText);
        if (tableCheck.standardRowCount !== 4) process.exitCode = 1;
      }
      const settingsScreenshotPath = path.join(tmpDir, 'debug-ui-screenshot-settings.png');
      await page.screenshot({ path: settingsScreenshotPath, fullPage: false });
      console.log('Settings screenshot:', settingsScreenshotPath);
    }

    let editionFromApi = null;
    if (isPro) {
      try {
        const res = await fetch(`${API_BASE}/edition`);
        const data = await res.json();
        editionFromApi = data?.edition || null;
        console.log('  /edition:', editionFromApi);
        if (editionFromApi !== 'pro') {
          console.error('  => Pro バックエンドが起動していません。npm run dev:fast:pro で起動してから再実行してください。');
          process.exitCode = 1;
        }
      } catch (e) {
        console.error('  => /edition 取得失敗:', e.message);
        console.error('  => Pro バックエンドが起動していません。npm run dev:fast:pro で起動してから再実行してください。');
        process.exitCode = 1;
      }
    }

    const check = await page.evaluate(() => ({
      panelSearch: !!document.querySelector('#panel-search'),
      panelSettings: !!document.querySelector('#panel-settings'),
      settingsSaveBtn: !!document.querySelector('#settings-save-btn'),
      searchInput: !!document.querySelector('#query'),
      mainPanelLoading: !!document.querySelector('.main-panel-loading'),
      mainPanelDefined: typeof customElements !== 'undefined' && !!customElements.get('main-panel'),
      scriptTags: Array.from(document.querySelectorAll('script[src]')).map(s => s.src),
      searchPanelVisible: (() => {
        const el = document.querySelector('#panel-search');
        return el && !el.classList.contains('hidden');
      })(),
      editionBadgeText: (() => {
        const el = document.getElementById('edition-badge');
        return el ? el.textContent.trim() : null;
      })(),
    }));
    console.log('--- UI check ---');
    if (isPro) {
      console.log('  エディションバッジ:', check.editionBadgeText);
      if (check.editionBadgeText !== 'Pro') {
        console.error('  => UI 上のバッジが Pro ではありません。バックエンドを Pro で起動してください。');
        process.exitCode = 1;
      }
    }
    console.log('  customElements main-panel 登録:', check.mainPanelDefined);
    console.log('  script[src] 一覧:', check.scriptTags?.length ? check.scriptTags.join(', ') : 'none');
    console.log('  #panel-search:', check.panelSearch);
    console.log('  #panel-settings:', check.panelSettings);
    console.log('  #settings-save-btn:', check.settingsSaveBtn);
    console.log('  #query (検索入力):', check.searchInput);
    console.log('  .main-panel-loading (読み込み中):', check.mainPanelLoading);
    console.log('  検索パネル表示中:', check.searchPanelVisible);
    if (check.settingsSaveBtn && check.searchInput) {
      const proOk = isPro && editionFromApi === 'pro' && check.editionBadgeText === 'Pro';
      console.log('  => パネルは正しく表示されています' + (isPro ? (proOk ? ' (Pro 接続済)' : '(Pro バックエンド未起動)') : ''));
    } else if (check.mainPanelLoading) {
      console.log('  => 読み込み中のままです(スクリプト未実行 or 未完了)');
    } else {
      console.log('  => 一部のみ表示の可能性');
    }

    await page.screenshot({ path: screenshotPath, fullPage: true });
    console.log('Screenshot saved to', screenshotPath);

    fs.writeFileSync(logPath, logs.join('\n'), 'utf8');
    console.log('Console log saved to', logPath);
  } catch (e) {
    console.error(e);
    if (logs.length) fs.writeFileSync(logPath, logs.join('\n'), 'utf8');
    process.exit(1);
  } finally {
    if (browser) await browser.close();
  }
}

main();