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