bone_2026/tests/browser/ui_check.js

164 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Проверка панели деталей веб-интерфейса в реальном браузере.
*
* Загружает несколько 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); });