364 lines
14 KiB
Python
364 lines
14 KiB
Python
"""
|
|
Visual + functional test for humanize.
|
|
Red dot = cursor, yellow = mouse held.
|
|
"""
|
|
import pytest
|
|
pytestmark = pytest.mark.slow
|
|
|
|
if __name__ == "__main__":
|
|
from cloakbrowser import launch
|
|
import time
|
|
|
|
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'; });
|
|
}
|
|
"""
|
|
|
|
def inject(page):
|
|
try:
|
|
page.evaluate(CURSOR_JS)
|
|
except:
|
|
pass
|
|
time.sleep(0.3)
|
|
|
|
results = []
|
|
|
|
def step(name):
|
|
print(f"\n{'='*60}")
|
|
print(f" STEP: {name}")
|
|
print(f"{'='*60}")
|
|
|
|
def check(name, passed, detail=""):
|
|
status = "PASS" if passed else "FAIL"
|
|
msg = f" [{status}] {name}"
|
|
if detail:
|
|
msg += f" — {detail}"
|
|
print(msg)
|
|
results.append((name, status))
|
|
|
|
print("=" * 70)
|
|
print(" HUMAN-LIKE BEHAVIOR VISUAL TEST")
|
|
print(" Watch the red dot — it should move smoothly like a real cursor")
|
|
print(" Yellow = mouse button held")
|
|
print(" Red trail dots = path taken")
|
|
print("=" * 70)
|
|
|
|
browser = launch(headless=False, humanize=True)
|
|
page = browser.new_page()
|
|
|
|
# ============================================================
|
|
# SCENARIO 1: Wikipedia search
|
|
# ============================================================
|
|
step("Wikipedia — navigate and search")
|
|
page.goto('https://www.wikipedia.org', wait_until='domcontentloaded')
|
|
time.sleep(2)
|
|
inject(page)
|
|
time.sleep(1)
|
|
|
|
print(" Watch: cursor moves to search box (Bezier curve)")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').click()
|
|
click_ms = int((time.time() - t0) * 1000)
|
|
check("click on search input", click_ms > 200, f"{click_ms} ms")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: characters appear one by one with varying speed")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').fill('Python programming language')
|
|
fill_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#searchInput').input_value()
|
|
check("fill search box", val == 'Python programming language' and fill_ms > 2000, f"{fill_ms} ms, value='{val}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: cursor moves to search box, double yellow flash, word selected")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').dblclick()
|
|
dbl_ms = int((time.time() - t0) * 1000)
|
|
sel = page.evaluate('() => window.getSelection().toString().trim()')
|
|
check("dblclick selects word", len(sel) > 0 and dbl_ms > 200, f"{dbl_ms} ms, selected='{sel}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: old text cleared, new text typed")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').fill('Artificial intelligence')
|
|
fill2_ms = int((time.time() - t0) * 1000)
|
|
val2 = page.locator('#searchInput').input_value()
|
|
check("fill replaces text", val2 == 'Artificial intelligence' and fill2_ms > 1500, f"{fill2_ms} ms, value='{val2}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: cursor moves to button without clicking")
|
|
t0 = time.time()
|
|
page.locator('button[type="submit"]').hover()
|
|
hover_ms = int((time.time() - t0) * 1000)
|
|
check("hover search button", hover_ms > 100, f"{hover_ms} ms")
|
|
time.sleep(1)
|
|
|
|
# ============================================================
|
|
# SCENARIO 2: Form interaction — checkboxes
|
|
# ============================================================
|
|
step("Checkboxes — check and uncheck")
|
|
page.goto('https://the-internet.herokuapp.com/checkboxes', wait_until='domcontentloaded')
|
|
time.sleep(2)
|
|
inject(page)
|
|
time.sleep(1)
|
|
|
|
cb1 = page.locator('input[type="checkbox"]').nth(0)
|
|
cb2 = page.locator('input[type="checkbox"]').nth(1)
|
|
|
|
print(" Watch: cursor moves to first checkbox, clicks")
|
|
if cb1.is_checked():
|
|
cb1.uncheck()
|
|
time.sleep(0.5)
|
|
|
|
t0 = time.time()
|
|
cb1.check()
|
|
check_ms = int((time.time() - t0) * 1000)
|
|
check("check checkbox 1", cb1.is_checked() and check_ms > 200, f"{check_ms} ms, checked={cb1.is_checked()}")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: cursor moves to second checkbox, clicks to uncheck")
|
|
if not cb2.is_checked():
|
|
cb2.check()
|
|
time.sleep(0.5)
|
|
|
|
t0 = time.time()
|
|
cb2.uncheck()
|
|
uncheck_ms = int((time.time() - t0) * 1000)
|
|
check("uncheck checkbox 2", not cb2.is_checked() and uncheck_ms > 200, f"{uncheck_ms} ms, checked={cb2.is_checked()}")
|
|
time.sleep(1)
|
|
|
|
# ============================================================
|
|
# SCENARIO 3: Dropdown
|
|
# ============================================================
|
|
step("Dropdown — select option")
|
|
page.goto('https://the-internet.herokuapp.com/dropdown', wait_until='domcontentloaded')
|
|
time.sleep(2)
|
|
inject(page)
|
|
time.sleep(1)
|
|
|
|
print(" Watch: cursor moves to dropdown, hovers, option selected")
|
|
t0 = time.time()
|
|
page.locator('#dropdown').select_option('1')
|
|
sel_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#dropdown').input_value()
|
|
check("select option 1", val == '1' and sel_ms > 100, f"{sel_ms} ms, value='{val}'")
|
|
time.sleep(0.5)
|
|
|
|
t0 = time.time()
|
|
page.locator('#dropdown').select_option('2')
|
|
sel2_ms = int((time.time() - t0) * 1000)
|
|
val2 = page.locator('#dropdown').input_value()
|
|
check("select option 2", val2 == '2' and sel2_ms > 100, f"{sel2_ms} ms, value='{val2}'")
|
|
time.sleep(1)
|
|
|
|
# ============================================================
|
|
# SCENARIO 4: Drag and drop
|
|
# ============================================================
|
|
step("Drag and Drop — move column A to B")
|
|
page.goto('https://the-internet.herokuapp.com/drag_and_drop', wait_until='domcontentloaded')
|
|
time.sleep(2)
|
|
inject(page)
|
|
time.sleep(1)
|
|
|
|
before_a = page.locator('#column-a header').text_content().strip()
|
|
before_b = page.locator('#column-b header').text_content().strip()
|
|
print(f" Before: A='{before_a}', B='{before_b}'")
|
|
|
|
print(" Watch: cursor moves to A, turns yellow (held), moves to B, releases")
|
|
t0 = time.time()
|
|
page.locator('#column-a').drag_to(page.locator('#column-b'))
|
|
drag_ms = int((time.time() - t0) * 1000)
|
|
time.sleep(1)
|
|
|
|
after_a = page.locator('#column-a header').text_content().strip()
|
|
after_b = page.locator('#column-b header').text_content().strip()
|
|
swapped = before_a != after_a
|
|
print(f" After: A='{after_a}', B='{after_b}'")
|
|
check("drag A to B", swapped and drag_ms > 300, f"{drag_ms} ms, swapped={swapped}")
|
|
time.sleep(1)
|
|
|
|
# ============================================================
|
|
# SCENARIO 5: Text editing
|
|
# ============================================================
|
|
step("Text editing — type, press keys, clear")
|
|
page.goto('https://www.wikipedia.org', wait_until='domcontentloaded')
|
|
time.sleep(2)
|
|
inject(page)
|
|
time.sleep(1)
|
|
|
|
print(" Watch: cursor clicks input, types character by character")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').type('Hello World')
|
|
type_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#searchInput').input_value()
|
|
check("type 'Hello World'", val == 'Hello World' and type_ms > 1000, f"{type_ms} ms, value='{val}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: cursor clicks, presses single key")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').press('End')
|
|
page.locator('#searchInput').press('!')
|
|
press_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#searchInput').input_value()
|
|
check("press '!' at end", '!' in val and press_ms > 100, f"{press_ms} ms, value='{val}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: field gets cleared (Ctrl+A, Backspace)")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').clear()
|
|
clear_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#searchInput').input_value()
|
|
check("clear field", val == '' and clear_ms > 100, f"{clear_ms} ms, value='{repr(val)}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: press_sequentially types each key individually")
|
|
t0 = time.time()
|
|
page.locator('#searchInput').press_sequentially('Sequential')
|
|
pseq_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#searchInput').input_value()
|
|
check("press_sequentially", val == 'Sequential' and pseq_ms > 500, f"{pseq_ms} ms, value='{val}'")
|
|
time.sleep(1)
|
|
|
|
# ============================================================
|
|
# SCENARIO 6: Mouse precision
|
|
# ============================================================
|
|
step("Mouse precision — move to coordinates")
|
|
print(" Watch: cursor moves in a Bezier curve to (600, 400)")
|
|
t0 = time.time()
|
|
page.mouse.move(600, 400)
|
|
move_ms = int((time.time() - t0) * 1000)
|
|
check("mouse.move to (600,400)", move_ms > 100, f"{move_ms} ms")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: cursor moves to (200, 200), clicks")
|
|
t0 = time.time()
|
|
page.mouse.click(200, 200)
|
|
mclick_ms = int((time.time() - t0) * 1000)
|
|
check("mouse.click at (200,200)", mclick_ms > 100, f"{mclick_ms} ms")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: keyboard types directly (no click needed)")
|
|
page.locator('#searchInput').click()
|
|
time.sleep(0.3)
|
|
t0 = time.time()
|
|
page.keyboard.type('Direct keyboard')
|
|
kb_ms = int((time.time() - t0) * 1000)
|
|
check("keyboard.type", kb_ms > 500, f"{kb_ms} ms")
|
|
time.sleep(1)
|
|
|
|
# ============================================================
|
|
# SCENARIO 7: ElementHandle — query_selector interactions
|
|
# ============================================================
|
|
step("ElementHandle — query_selector click, type, fill, hover")
|
|
page.goto('https://www.wikipedia.org', wait_until='domcontentloaded')
|
|
time.sleep(2)
|
|
inject(page)
|
|
time.sleep(1)
|
|
|
|
print(" Watch: get element via query_selector, cursor moves smoothly")
|
|
el = page.query_selector('#searchInput')
|
|
assert el is not None, "query_selector returned None"
|
|
assert getattr(el, '_human_patched', False), "ElementHandle not patched!"
|
|
|
|
t0 = time.time()
|
|
el.click()
|
|
eh_click_ms = int((time.time() - t0) * 1000)
|
|
check("ElementHandle click", eh_click_ms > 100, f"{eh_click_ms} ms")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: ElementHandle type — characters appear one by one")
|
|
t0 = time.time()
|
|
el.type('ElementHandle typing')
|
|
eh_type_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#searchInput').input_value()
|
|
check("ElementHandle type", val == 'ElementHandle typing' and eh_type_ms > 1500, f"{eh_type_ms} ms, value='{val}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: ElementHandle fill — clears then types")
|
|
t0 = time.time()
|
|
el.fill('Filled via EH')
|
|
eh_fill_ms = int((time.time() - t0) * 1000)
|
|
val = page.locator('#searchInput').input_value()
|
|
check("ElementHandle fill", val == 'Filled via EH' and eh_fill_ms > 1000, f"{eh_fill_ms} ms, value='{val}'")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: ElementHandle hover — cursor moves without clicking")
|
|
btn_el = page.query_selector('button[type="submit"]')
|
|
t0 = time.time()
|
|
btn_el.hover()
|
|
eh_hover_ms = int((time.time() - t0) * 1000)
|
|
check("ElementHandle hover", eh_hover_ms > 50, f"{eh_hover_ms} ms")
|
|
time.sleep(0.5)
|
|
|
|
print(" Watch: query_selector_all returns patched handles")
|
|
page.goto('https://the-internet.herokuapp.com/checkboxes', wait_until='domcontentloaded')
|
|
time.sleep(2)
|
|
inject(page)
|
|
time.sleep(1)
|
|
els = page.query_selector_all('input[type="checkbox"]')
|
|
all_patched = all(getattr(e, '_human_patched', False) for e in els)
|
|
check("query_selector_all all patched", all_patched and len(els) >= 2, f"{len(els)} elements, all_patched={all_patched}")
|
|
|
|
if els:
|
|
print(" Watch: click checkbox via ElementHandle")
|
|
t0 = time.time()
|
|
els[0].click()
|
|
cb_click_ms = int((time.time() - t0) * 1000)
|
|
check("ElementHandle checkbox click", cb_click_ms > 100, f"{cb_click_ms} ms")
|
|
time.sleep(1)
|
|
|
|
# ============================================================
|
|
# SUMMARY
|
|
# ============================================================
|
|
print("\n" + "=" * 70)
|
|
print(" SUMMARY")
|
|
print("=" * 70)
|
|
passed = sum(1 for _, s in results if s == "PASS")
|
|
failed = sum(1 for _, s in results if s == "FAIL")
|
|
total = len(results)
|
|
|
|
for name, status in results:
|
|
icon = "OK" if status == "PASS" else "XX"
|
|
print(f" [{icon}] {name}")
|
|
|
|
print(f"\n {passed}/{total} passed, {failed} failed")
|
|
if failed == 0:
|
|
print(" *** ALL TESTS PASSED ***")
|
|
print("=" * 70)
|
|
|
|
input("\nPress Enter to close browser...")
|
|
browser.close()
|