257 lines
9.1 KiB
JavaScript
257 lines
9.1 KiB
JavaScript
// test_human_visual.mjs
|
|
/**
|
|
* Visual + functional test for humanize (JS).
|
|
* Red dot = cursor, yellow = mouse held.
|
|
* Trail dots show the path taken.
|
|
*/
|
|
import { launch } from '../js/dist/index.js';
|
|
|
|
const CURSOR_JS = `
|
|
(() => {
|
|
if (document.getElementById('__hc')) return;
|
|
const el = document.createElement('div');
|
|
el.id = '__hc';
|
|
el.style.cssText = 'width:14px;height:14px;background:red;border:2px solid darkred;border-radius:50%;position:fixed;z-index:2147483647;pointer-events:none;display:none;transition:background 0.05s;';
|
|
document.body.appendChild(el);
|
|
|
|
const trail = document.createElement('div');
|
|
trail.id = '__hcTrail';
|
|
trail.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:2147483646;pointer-events:none;overflow:hidden;';
|
|
document.body.appendChild(trail);
|
|
|
|
let dotCount = 0;
|
|
const maxDots = 500;
|
|
|
|
function updatePos(x, y) {
|
|
el.style.display = 'block';
|
|
el.style.left = (x - 9) + 'px';
|
|
el.style.top = (y - 9) + 'px';
|
|
if (dotCount < maxDots) {
|
|
const dot = document.createElement('div');
|
|
dot.style.cssText = 'width:3px;height:3px;background:rgba(255,0,0,0.3);border-radius:50%;position:fixed;pointer-events:none;left:'+(x-1)+'px;top:'+(y-1)+'px;';
|
|
trail.appendChild(dot);
|
|
dotCount++;
|
|
}
|
|
}
|
|
|
|
document.addEventListener('mousemove', e => updatePos(e.clientX, e.clientY));
|
|
document.addEventListener('drag', e => { if (e.clientX > 0) updatePos(e.clientX, e.clientY); });
|
|
document.addEventListener('dragover', e => { if (e.clientX > 0) updatePos(e.clientX, e.clientY); });
|
|
document.addEventListener('mousedown', () => { el.style.background = 'yellow'; });
|
|
document.addEventListener('mouseup', () => { el.style.background = 'red'; });
|
|
document.addEventListener('dragend', () => { el.style.background = 'red'; });
|
|
})();
|
|
`;
|
|
|
|
const results = [];
|
|
const delay = ms => new Promise(r => setTimeout(r, ms));
|
|
|
|
async function inject(page) {
|
|
try { await page.evaluate(CURSOR_JS); } catch {}
|
|
await delay(300);
|
|
}
|
|
|
|
function step(name) {
|
|
console.log(`\n${'='.repeat(60)}`);
|
|
console.log(` STEP: ${name}`);
|
|
console.log('='.repeat(60));
|
|
}
|
|
|
|
function check(name, passed, detail = '') {
|
|
const status = passed ? 'PASS' : 'FAIL';
|
|
let msg = ` [${status}] ${name}`;
|
|
if (detail) msg += ` — ${detail}`;
|
|
console.log(msg);
|
|
results.push({ name, status });
|
|
}
|
|
|
|
async function main() {
|
|
console.log('='.repeat(70));
|
|
console.log(' HUMAN-LIKE BEHAVIOR VISUAL TEST (JS)');
|
|
console.log(' Watch the red dot — it should move smoothly like a real cursor');
|
|
console.log('='.repeat(70));
|
|
|
|
const browser = await launch({
|
|
headless: false,
|
|
humanize: true,
|
|
});
|
|
const page = await browser.newPage();
|
|
|
|
// ============================================================
|
|
// SCENARIO 1: Wikipedia search
|
|
// ============================================================
|
|
step('Wikipedia — navigate and search');
|
|
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
|
|
await delay(2000);
|
|
await inject(page);
|
|
await delay(1000);
|
|
|
|
console.log(' Watch: cursor moves to search box (Bezier curve)');
|
|
let t0 = Date.now();
|
|
await page.locator('#searchInput').click();
|
|
let ms = Date.now() - t0;
|
|
check('click on search input', ms > 200, `${ms} ms`);
|
|
await delay(500);
|
|
|
|
console.log(' Watch: characters appear one by one');
|
|
t0 = Date.now();
|
|
await page.locator('#searchInput').fill('Python programming language');
|
|
ms = Date.now() - t0;
|
|
let val = await page.locator('#searchInput').inputValue();
|
|
check('fill search box', val === 'Python programming language' && ms > 2000, `${ms} ms, value='${val}'`);
|
|
await delay(500);
|
|
|
|
console.log(' Watch: double click selects word');
|
|
t0 = Date.now();
|
|
await page.locator('#searchInput').dblclick();
|
|
ms = Date.now() - t0;
|
|
let sel = await page.evaluate(() => window.getSelection().toString().trim());
|
|
check('dblclick selects word', sel.length > 0 && ms > 200, `${ms} ms, selected='${sel}'`);
|
|
await delay(500);
|
|
|
|
console.log(' Watch: old text replaced');
|
|
t0 = Date.now();
|
|
await page.locator('#searchInput').fill('Artificial intelligence');
|
|
ms = Date.now() - t0;
|
|
val = await page.locator('#searchInput').inputValue();
|
|
check('fill replaces text', val === 'Artificial intelligence' && ms > 1500, `${ms} ms, value='${val}'`);
|
|
await delay(500);
|
|
|
|
console.log(' Watch: cursor hovers button without clicking');
|
|
t0 = Date.now();
|
|
await page.locator('button[type="submit"]').hover();
|
|
ms = Date.now() - t0;
|
|
check('hover search button', ms > 100, `${ms} ms`);
|
|
await delay(1000);
|
|
|
|
// ============================================================
|
|
// SCENARIO 2: Checkboxes
|
|
// ============================================================
|
|
step('Checkboxes — check and uncheck');
|
|
await page.goto('https://the-internet.herokuapp.com/checkboxes', { waitUntil: 'domcontentloaded' });
|
|
await delay(2000);
|
|
await inject(page);
|
|
await delay(1000);
|
|
|
|
const cb1 = page.locator('input[type="checkbox"]').nth(0);
|
|
const cb2 = page.locator('input[type="checkbox"]').nth(1);
|
|
|
|
if (await cb1.isChecked()) { await cb1.uncheck(); await delay(500); }
|
|
|
|
console.log(' Watch: cursor moves to checkbox, clicks');
|
|
t0 = Date.now();
|
|
await cb1.check();
|
|
ms = Date.now() - t0;
|
|
check('check checkbox 1', await cb1.isChecked() && ms > 200, `${ms} ms`);
|
|
await delay(500);
|
|
|
|
if (!(await cb2.isChecked())) { await cb2.check(); await delay(500); }
|
|
|
|
t0 = Date.now();
|
|
await cb2.uncheck();
|
|
ms = Date.now() - t0;
|
|
check('uncheck checkbox 2', !(await cb2.isChecked()) && ms > 200, `${ms} ms`);
|
|
await delay(1000);
|
|
|
|
// ============================================================
|
|
// SCENARIO 3: Dropdown
|
|
// ============================================================
|
|
step('Dropdown — select option');
|
|
await page.goto('https://the-internet.herokuapp.com/dropdown', { waitUntil: 'domcontentloaded' });
|
|
await delay(2000);
|
|
await inject(page);
|
|
await delay(1000);
|
|
|
|
console.log(' Watch: cursor hovers dropdown, option selected');
|
|
t0 = Date.now();
|
|
await page.locator('#dropdown').selectOption('2');
|
|
ms = Date.now() - t0;
|
|
val = await page.locator('#dropdown').inputValue();
|
|
check('select option', val === '2' && ms > 100, `${ms} ms, value='${val}'`);
|
|
await delay(1000);
|
|
|
|
// ============================================================
|
|
// SCENARIO 4: Drag and Drop
|
|
// ============================================================
|
|
step('Drag and Drop');
|
|
await page.goto('https://the-internet.herokuapp.com/drag_and_drop', { waitUntil: 'domcontentloaded' });
|
|
await delay(2000);
|
|
await inject(page);
|
|
await delay(1000);
|
|
|
|
const beforeA = (await page.locator('#column-a header').textContent()).trim();
|
|
console.log(` Before: A='${beforeA}'`);
|
|
console.log(' Watch: cursor to A, yellow (held), moves to B, releases');
|
|
|
|
t0 = Date.now();
|
|
await page.locator('#column-a').dragTo(page.locator('#column-b'));
|
|
ms = Date.now() - t0;
|
|
await delay(1000);
|
|
|
|
const afterA = (await page.locator('#column-a header').textContent()).trim();
|
|
const swapped = beforeA !== afterA;
|
|
check('drag A to B', swapped && ms > 300, `${ms} ms, swapped=${swapped}`);
|
|
await delay(1000);
|
|
|
|
// ============================================================
|
|
// SCENARIO 5: Text editing
|
|
// ============================================================
|
|
step('Text editing — type, press, clear');
|
|
await page.goto('https://www.wikipedia.org', { waitUntil: 'domcontentloaded' });
|
|
await delay(2000);
|
|
await inject(page);
|
|
await delay(1000);
|
|
|
|
console.log(' Watch: types character by character');
|
|
t0 = Date.now();
|
|
await page.locator('#searchInput').type('Hello World');
|
|
ms = Date.now() - t0;
|
|
val = await page.locator('#searchInput').inputValue();
|
|
check("type 'Hello World'", val === 'Hello World' && ms > 1000, `${ms} ms`);
|
|
await delay(500);
|
|
|
|
console.log(' Watch: field cleared');
|
|
t0 = Date.now();
|
|
await page.locator('#searchInput').clear();
|
|
ms = Date.now() - t0;
|
|
val = await page.locator('#searchInput').inputValue();
|
|
check('clear field', val === '' && ms > 100, `${ms} ms`);
|
|
await delay(500);
|
|
|
|
console.log(' Watch: mouse moves in Bezier curve');
|
|
t0 = Date.now();
|
|
await page.mouse.move(600, 400);
|
|
ms = Date.now() - t0;
|
|
check('mouse.move', ms > 100, `${ms} ms`);
|
|
await delay(500);
|
|
|
|
t0 = Date.now();
|
|
await page.mouse.click(300, 300);
|
|
ms = Date.now() - t0;
|
|
check('mouse.click', ms > 100, `${ms} ms`);
|
|
await delay(1000);
|
|
|
|
// ============================================================
|
|
// SUMMARY
|
|
// ============================================================
|
|
console.log('\n' + '='.repeat(70));
|
|
console.log(' SUMMARY');
|
|
console.log('='.repeat(70));
|
|
|
|
const passed = results.filter(r => r.status === 'PASS').length;
|
|
const failed = results.filter(r => r.status === 'FAIL').length;
|
|
|
|
for (const r of results) {
|
|
const icon = r.status === 'PASS' ? 'OK' : 'XX';
|
|
console.log(` [${icon}] ${r.name}`);
|
|
}
|
|
|
|
console.log(`\n ${passed}/${results.length} passed, ${failed} failed`);
|
|
if (failed === 0) console.log(' *** ALL TESTS PASSED ***');
|
|
console.log('='.repeat(70));
|
|
|
|
await browser.close();
|
|
}
|
|
|
|
main().catch(console.error);
|