#!/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'),
settingsRefreshBtn: !!document.querySelector('#settings-refresh-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(' #settings-refresh-btn:', check.settingsRefreshBtn);
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();