bone_2026/tests/browser/ui_labeling.js

165 lines
8.0 KiB
JavaScript
Raw Permalink 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.

/**
* Проверка интерфейса ручной разметки в реальном браузере.
*
* Сценарий повторяет работу специалиста: открыть /label, выбрать снимок из
* списка, посмотреть изображение, отметить вердикт и сохранить его. Проверяется,
* что вердикт действительно дошёл до сервера (счётчик разобранных растёт и
* остаётся после перезагрузки) и что страница не обращается к внешним хостам.
*
* Требуется запущенный сервер с временным файлом вердиктов, чтобы не портить
* рабочий labels/manual_labels.csv:
* DXA_MANUAL_LABELS=/tmp/manual_ui.csv python -m uvicorn src.main:app --port 8123
* node tests/browser/ui_labeling.js
*
* Переменные окружения:
* BASE_URL адрес сервера (по умолчанию http://127.0.0.1:8123)
* 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 BASE = process.env.BASE_URL || 'http://127.0.0.1:8123';
function assert(condition, message) {
if (!condition) throw new Error(message);
}
(async () => {
const userDataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dxa-label-'));
const context = await chromium.launchPersistentContext(userDataDir, {
channel: 'chrome',
headless: true,
});
const page = context.pages()[0] || await context.newPage();
await page.setViewportSize({ width: 1500, height: 1100 });
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(BASE) && !url.startsWith('data:')) external.push(url);
});
await page.goto(`${BASE}/label`, { waitUntil: 'domcontentloaded' });
console.log('title:', await page.title());
await page.waitForFunction(
() => document.querySelectorAll('#list .item-card').length > 0,
{ timeout: 90000 }
);
const summary = await page.evaluate(() => ({
progress: document.getElementById('progressText').innerText.trim(),
conflicts: document.getElementById('conflictText').innerText.trim(),
byRegion: document.getElementById('byRegion').innerText.trim(),
items: document.querySelectorAll('#list .item-card').length,
}));
console.log('summary:', summary);
assert(summary.items > 0, 'список снимков пуст');
assert(/\d+ из \d+/.test(summary.progress), `счётчик разобранных не найден: ${summary.progress}`);
const total = Number(summary.progress.match(/из (\d+)/)[1]);
assert(total > 100, `ожидался весь датасет, получено ${total}`);
// Первым идёт расхождение разметки с пометкой в имени файла.
const first = await page.evaluate(() => ({
file: document.querySelector('#list .item-card .truncate').innerText.trim(),
conflict: /расхождение/.test(document.querySelector('#list .item-card').innerText),
}));
console.log('первый снимок:', first);
assert(first.conflict, 'расхождения должны идти первыми');
// Разбор снимка: изображение должно реально загрузиться с сервера.
await page.locator('#list .item-card').first().click();
await page.waitForSelector('#detail #image', { timeout: 20000 });
await page.waitForFunction(
() => { const img = document.getElementById('image'); return img && img.naturalWidth > 0; },
{ timeout: 30000 }
);
const image = await page.evaluate(() => {
const img = document.getElementById('image');
return { w: img.naturalWidth, h: img.naturalHeight, src: img.getAttribute('src').slice(0, 40) };
});
console.log('изображение:', image);
assert(image.w > 50 && image.h > 50, `снимок не отрисован: ${JSON.stringify(image)}`);
// Вердикт: «нарушение» требует типа, «годное» его снимает.
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('#markGood').click();
await page.locator('#saveVerdict').click();
const reviewedBefore = Number(summary.progress.match(/Разобрано (\d+)/)[1]);
const expected = reviewedBefore + 1;
await page.waitForFunction(
(want) => new RegExp(`Разобрано ${want} `).test(document.getElementById('progressText').innerText),
expected,
{ timeout: 20000 }
);
console.log('после сохранения:', (await page.locator('#progressText').innerText()).trim());
// Вердикт должен сохраниться на сервере, а не только в состоянии страницы.
await page.reload({ waitUntil: 'domcontentloaded' });
await page.waitForFunction(
(want) => new RegExp(`Разобрано ${want} `).test(document.getElementById('progressText').innerText),
expected,
{ timeout: 90000 }
);
console.log('после перезагрузки:', (await page.locator('#progressText').innerText()).trim());
// Фильтр «только расхождения» должен оставлять только расхождения.
await page.locator('#filterConflicts').check();
await page.waitForFunction(
() => document.querySelectorAll('#list .item-card').length > 0,
{ timeout: 20000 }
);
const filtered = await page.evaluate(() => {
const cards = Array.from(document.querySelectorAll('#list .item-card'));
return {
items: cards.length,
withoutConflict: cards.filter((c) => !/расхождение/.test(c.innerText)).length,
};
});
console.log('фильтр расхождений:', filtered);
assert(filtered.withoutConflict === 0, 'в фильтре «только расхождения» есть лишние снимки');
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);
});