bone_2026/tests/browser/review_pack.js

154 lines
8.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.

/**
* Проверка автономного HTML-пакета для ручной разметки (файл для врача).
*
* Пакет открывается напрямую с диска (`file://`), без сервера и без сети —
* именно так его откроет врач. Сценарий повторяет его работу: выбрать снимок,
* посмотреть, отметить вердикт, убедиться, что он пережил перезагрузку, выгрузить
* CSV, а затем загрузить его обратно (восстановление прогресса на другой машине).
*
* Пакет нужно собрать заранее:
* python -m src.dxa.review_pack --out /tmp/review_small.html --limit 8
* PACK=/tmp/review_small.html node tests/browser/review_pack.js
*
* Переменные окружения:
* PACK путь к HTML-пакету (обязателен)
* PLAYWRIGHT путь к playwright-core (по умолчанию bundled Browser Use)
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
function resolvePlaywright() {
if (process.env.PLAYWRIGHT) return process.env.PLAYWRIGHT;
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 PACK = process.env.PACK;
if (!PACK || !fs.existsSync(PACK)) {
console.error('FAILED: укажите PACK=путь/к/пакету.html (соберите его через src.dxa.review_pack)');
process.exit(1);
}
function assert(condition, message) {
if (!condition) throw new Error(message);
}
(async () => {
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dxa-pack-'));
const context = await chromium.launchPersistentContext(userDataDir, {
channel: 'chrome',
headless: true,
acceptDownloads: true,
});
const page = context.pages()[0] || await context.newPage();
await page.setViewportSize({ width: 1500, height: 1000 });
// beforeunload предупреждает о невыгруженной разметке — для проверки это ожидаемо.
page.on('dialog', (dialog) => dialog.accept());
const consoleErrors = [];
const external = [];
page.on('console', (m) => { if (m.type() === 'error') consoleErrors.push(m.text()); });
page.on('pageerror', (e) => consoleErrors.push('PAGEERROR ' + e.message));
page.on('request', (request) => {
const url = request.url();
if (!url.startsWith('file:') && !url.startsWith('data:') && !url.startsWith('blob:')) {
external.push(url);
}
});
await page.goto('file://' + path.resolve(PACK), { waitUntil: 'domcontentloaded' });
console.log('title:', await page.title());
await page.waitForFunction(() => document.querySelectorAll('.item').length > 0, { timeout: 30000 });
const total = await page.evaluate(() => document.querySelectorAll('.item').length);
console.log('снимков в списке:', total);
console.log('прогресс:', (await page.locator('#progressText').innerText()).trim());
console.log('отпечаток пакета:', (await page.locator('#packId').innerText()).trim());
assert(total > 1, 'в пакете должно быть несколько снимков');
// Разбор первого снимка: изображение встроено как data-URI и рисуется.
await page.locator('.item').first().click();
await page.waitForFunction(() => {
const img = document.getElementById('image');
return img && img.naturalWidth > 0;
}, { timeout: 20000 });
const image = await page.evaluate(() => {
const img = document.getElementById('image');
return { w: img.naturalWidth, h: img.naturalHeight, data: img.getAttribute('src').startsWith('data:image/png;base64,') };
});
console.log('снимок:', image);
assert(image.w > 50 && image.h > 50, 'снимок не отрисован');
assert(image.data, 'снимок должен быть встроен как data-URI');
// Построенная разметка и её источник видны сразу (режим подтверждения).
const detail = await page.locator('#detail').innerText();
assert(/Построенная разметка/.test(detail), 'не показана построенная разметка');
assert(/источник:/.test(detail), 'не показан источник метки');
console.log('подсказка о расхождении:', /не совпадает с пометкой/.test(detail) ? 'есть' : 'нет в этом снимке');
// Нарушение: тип подставляется по области; годное — снимает тип.
await page.locator('#markBad').click();
const chosen = await page.locator('#violation').inputValue();
console.log('тип при отметке «нарушение»:', chosen || '(пусто)');
assert(chosen, 'тип нарушения не подставился');
await page.locator('#markGood').click();
assert((await page.locator('#violation').inputValue()) === '', 'тип должен сниматься при «годное»');
await page.locator('#reviewer').fill('Тестова А. А.');
await page.locator('#save').click();
await page.waitForFunction(() => /Разобрано 1 /.test(document.getElementById('progressText').innerText), { timeout: 15000 });
console.log('после сохранения:', (await page.locator('#progressText').innerText()).trim());
// Вердикт должен пережить перезагрузку страницы (localStorage).
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => /Разобрано 1 /.test(document.getElementById('progressText').innerText), { timeout: 20000 });
console.log('после перезагрузки:', (await page.locator('#progressText').innerText()).trim());
// Выгрузка CSV.
const [download] = await Promise.all([
page.waitForEvent('download', { timeout: 20000 }),
page.locator('#export').click(),
]);
const csvPath = path.join(userDataDir, 'export.csv');
await download.saveAs(csvPath);
const csvText = fs.readFileSync(csvPath, 'utf-8');
const lines = csvText.replace(/^\ufeff/, '').trim().split(/\r?\n/);
const header = lines[0].split(',');
console.log('CSV: строк', lines.length - 1, '| колонки:', header.join(','));
assert(/path_to_image/.test(header[0]), 'в CSV нет колонки path_to_image');
assert(header.includes('quality_class') && header.includes('pack_id'), 'в CSV нет обязательных колонок');
assert(lines.length === 2, `в CSV должна быть одна строка с вердиктом, получено ${lines.length - 1}`);
assert(/,0,/.test(lines[1]), 'вердикт «годное» должен записаться как quality_class=0');
// Восстановление прогресса из файла: чистим хранилище и загружаем CSV обратно.
await page.evaluate(() => window.localStorage.clear());
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => /Разобрано 0 /.test(document.getElementById('progressText').innerText), { timeout: 20000 });
await page.locator('#import').setInputFiles(csvPath);
await page.waitForFunction(() => /Разобрано 1 /.test(document.getElementById('progressText').innerText), { timeout: 20000 });
console.log('после загрузки CSV:', (await page.locator('#progressText').innerText()).trim());
console.log('внешние запросы:', external.length ? external : 'нет');
assert(external.length === 0, `пакет обратился к сети: ${external.join(', ')}`);
console.log('ошибки консоли:', consoleErrors.length ? consoleErrors : 'нет');
assert(consoleErrors.length === 0, `ошибки в консоли: ${consoleErrors.join(' | ')}`);
await context.close();
console.log('OK: пакет разметки работает автономно (вердикт, перезагрузка, выгрузка, загрузка)');
})().catch((error) => {
console.error('FAILED:', error.message);
process.exit(1);
});