/** * Проверка панели деталей веб-интерфейса в реальном браузере. * * Загружает несколько DICOM-файлов, кликает по строкам таблицы и снимает * значения из панели деталей, чтобы убедиться, что они меняются при * переключении строк (это был основной симптом бага: панель показывала одни * и те же значения). * * Требуется запущенный сервер: * python -m uvicorn src.main:app --port 8123 * node tests/browser/ui_check.js * * Переменные окружения: * BASE_URL адрес сервера (по умолчанию http://127.0.0.1:8123) * PLAYWRIGHT путь к playwright-core (по умолчанию берётся из bundled * Browser Use в установке Qwen Code) * * Используется временный профиль Chrome через launchPersistentContext, * профиль пользователя не затрагивается. */ const fs = require('fs'); const path = require('path'); const os = require('os'); function resolvePlaywright() { if (process.env.PLAYWRIGHT) return process.env.PLAYWRIGHT; // Ищем playwright-core в bundled Browser Use установки Qwen Code. const base = path.join(os.homedir(), '.qwen', 'updates', 'npm'); if (!fs.existsSync(base)) return 'playwright-core'; const versions = fs.readdirSync(path.join(base, fs.readdirSync(base)[0], 'versions')).sort(); for (const v of versions.reverse()) { const candidate = path.join( base, fs.readdirSync(base)[0], 'versions', v, 'node_modules/@qwen-code/qwen-code/bundled/browser-use/runtime/node_modules/playwright-core' ); if (fs.existsSync(candidate)) return candidate; } return 'playwright-core'; } const { chromium } = require(resolvePlaywright()); const BASE = process.env.BASE_URL || 'http://127.0.0.1:8123'; const REPO = path.resolve(__dirname, '..', '..'); const FILES = [ path.join(REPO, 'dataset_hack/Для теста/spine.dcm'), path.join(REPO, 'dataset_hack/Для теста/l_hip.dcm'), path.join(REPO, 'dataset_hack/Для теста/r_hip.dcm'), ]; async function panelState(page) { return page.evaluate(() => { const txt = (id) => { const el = document.getElementById(id); return el ? el.innerText.replace(/\s+/g, ' ').trim() : null; }; const cards = (id) => { const el = document.getElementById(id); if (!el) return null; return Array.from(el.children).map((c) => c.innerText.replace(/\s+/g, ' ').trim()); }; const imgs = Array.from(document.querySelectorAll('#detailImage img, #detailMask img')) .map((i) => ({ src: (i.getAttribute('src') || '').slice(0, 22), w: i.naturalWidth, h: i.naturalHeight })); return { detailVisible: !document.getElementById('detailSection').classList.contains('hidden'), badge: txt('detailBadge'), basic: txt('detailBasic'), quality: txt('detailQuality'), measurements: cards('detailMotion'), roiCards: cards('detailArtifacts'), spineHidden: document.getElementById('spineSection').classList.contains('hidden'), spine: txt('detailSpine'), hipHidden: document.getElementById('hipSection').classList.contains('hidden'), hip: txt('detailHip'), reason: txt('detailReason'), note: txt('detailMetricsNote'), images: imgs, }; }); } (async () => { const userDataDir = fs.mkdtempSync(path.join(require('os').tmpdir(), 'dxa-ui-')); // launchPersistentContext с временным профилем: профиль пользователя не затрагивается. const context = await chromium.launchPersistentContext(userDataDir, { channel: 'chrome', headless: true }); const browser = context; const page = context.pages()[0] || await context.newPage(); await page.setViewportSize({ width: 1400, height: 1100 }); const consoleErrors = []; page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()); }); page.on('pageerror', (e) => consoleErrors.push('PAGEERROR ' + e.message)); await page.goto(BASE, { waitUntil: 'domcontentloaded' }); console.log('title:', await page.title()); console.log('status banner:', (await page.locator('#statusText').innerText()).trim()); // Загружаем файлы через input (drag-and-drop эмулировать не нужно). await page.setInputFiles('#fileInput', FILES); await page.waitForFunction( () => document.querySelectorAll('#resultsTable tr').length === 3, { timeout: 60000 } ); console.log('rows rendered:', await page.locator('#resultsTable tr').count()); console.log('stats:', await page.evaluate(() => ({ total: document.getElementById('totalCount').innerText, ok: document.getElementById('okCount').innerText, violation: document.getElementById('violationCount').innerText, avg: document.getElementById('accuracy').innerText, }))); const shots = []; const states = []; for (const idx of [0, 1, 2]) { await page.locator('#resultsTable tr').nth(idx).click(); await page.waitForTimeout(1200); const state = await panelState(page); states.push(state); const file = path.join(os.tmpdir(), `dxa_panel_row${idx}.png`); await page.screenshot({ path: file, fullPage: false }); shots.push({ idx, file, measurements: state.measurements, region: state.basic }); // Прокручиваем к панели, чтобы она попала в кадр await page.locator('#detailSection').scrollIntoViewIfNeeded(); await page.screenshot({ path: path.join(os.tmpdir(), `dxa_detail_row${idx}.png`) }); } console.log('\n=== ПАНЕЛЬ ДЕТАЛЕЙ ПО СТРОКАМ ==='); for (const s of shots) { console.log(`\nстрока ${s.idx}:`); console.log(' basic:', s.region); console.log(' измерения:', s.measurements); } console.log('\n=== СЕКЦИИ / ВИЗУАЛИЗАЦИЯ / ЗАКЛЮЧЕНИЕ ==='); states.forEach((s, i) => { console.log(`\nстрока ${i}: visible=${s.detailVisible}`); console.log(' badge:', s.badge); console.log(' quality:', s.quality); console.log(' roi:', s.roiCards); console.log(' spine hidden:', s.spineHidden, '| hip hidden:', s.hipHidden); if (!s.spineHidden) console.log(' spine:', s.spine); if (!s.hipHidden) console.log(' hip:', s.hip); console.log(' note:', (s.note || '').slice(0, 80)); console.log(' images:', s.images); }); // Проверка, что значения действительно различаются между строками const uniq = (arr) => new Set(arr.filter(Boolean)).size; console.log('\n=== РАЗЛИЧИМОСТЬ ЗНАЧЕНИЙ МЕЖДУ СТРОКАМИ ==='); console.log('разных basic:', uniq(states.map((s) => s.basic))); console.log('разных measurements:', uniq(states.map((s) => JSON.stringify(s.measurements)))); console.log('разных quality:', uniq(states.map((s) => s.quality))); console.log('разных roi:', uniq(states.map((s) => JSON.stringify(s.roiCards)))); console.log('разных reason:', uniq(states.map((s) => s.reason))); console.log('\nconsole errors:', consoleErrors.length ? consoleErrors : 'none'); fs.writeFileSync(path.join(os.tmpdir(), 'dxa_ui_states.json'), JSON.stringify(states, null, 2)); console.log('states saved to', path.join(os.tmpdir(), 'dxa_ui_states.json')); console.log('screenshots:', shots.map((s) => s.file).join(', ')); await browser.close(); fs.rmSync(userDataDir, { recursive: true, force: true }); })().catch((e) => { console.error('FAILED:', e.message); process.exit(1); });