Files
2026-06-29 22:25:58 +07:00

146 lines
4.7 KiB
JavaScript

/**
* Helper untuk interaksi browser ala manusia.
*
* Tujuan: menghindari pola bot yang kentara (paste instan via .fill(),
* klik tanpa hover/jeda, dsb). Bukan stealth penuh — cuma menambah
* jitter di layer event yang biasa diukur skrip antifraud
* (waktu antar keystroke, klik tanpa pointermove, fokus tanpa klik).
*
* Pakai begini:
* import { humanType, humanClick, humanDelay, humanFill } from './human.js';
* await humanFill(page, 'input[name="email"]', email);
* await humanClick(page, 'button[type="submit"]');
*/
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function randInt(min, max) {
return Math.floor(rand(min, max + 1));
}
export function humanDelay(min = 80, max = 220) {
return new Promise((resolve) => setTimeout(resolve, randInt(min, max)));
}
/**
* Resolve target ke ElementHandle/Locator yang punya method click/fill/type.
* - String → page.$(selector) (ElementHandle)
* - Locator → return apa adanya
* - ElementHandle → return apa adanya
*/
async function resolveTarget(pageOrFrame, target) {
if (typeof target === 'string') {
const handle = await pageOrFrame.$(target);
if (!handle) throw new Error(`humanFill: selector tidak ketemu: ${target}`);
return handle;
}
return target;
}
/**
* Klik element dengan sedikit hover dulu. Hover memicu pointermove,
* yang sering dipakai sebagai sinyal "user benerian" oleh script antibot.
*/
export async function humanClick(pageOrFrame, target, options = {}) {
const el = await resolveTarget(pageOrFrame, target);
try {
// Move pointer ke element pelan-pelan (Playwright hover sudah
// terjemahkan jadi serangkaian pointermove)
await el.hover({ timeout: options.hoverTimeout || 4000 }).catch(() => {});
} catch (e) {}
await humanDelay(60, 180);
await el.click({ ...options, delay: randInt(40, 120) });
}
/**
* Ketik teks ala manusia: per karakter dengan delay acak,
* sesekali jeda 200-500ms seolah-olah "mikir".
*/
export async function humanType(pageOrFrame, text, options = {}) {
const minDelay = options.minDelay ?? 60;
const maxDelay = options.maxDelay ?? 180;
const thinkProb = options.thinkProb ?? 0.07; // 7% chance jeda mikir per char
const thinkMin = options.thinkMin ?? 220;
const thinkMax = options.thinkMax ?? 520;
const keyboard = pageOrFrame.keyboard;
for (const ch of text) {
await keyboard.type(ch, { delay: randInt(minDelay, maxDelay) });
if (Math.random() < thinkProb) {
await humanDelay(thinkMin, thinkMax);
}
}
}
/**
* Fokus ke field, bersihkan isi lama secara natural, lalu ketik nilai baru
* karakter-per-karakter. Pengganti drop-in untuk .fill() yang lebih meyakinkan.
*
* options:
* - clear: 'select-all' (default) atau 'backspace' atau 'none'
* - clickFirst: true (default) — klik field dulu sebelum ngetik
*/
export async function humanFill(pageOrFrame, target, value, options = {}) {
const el = await resolveTarget(pageOrFrame, target);
const clearMode = options.clear ?? 'select-all';
if (options.clickFirst !== false) {
try { await el.hover({ timeout: 3000 }); } catch (e) {}
await humanDelay(80, 200);
try {
await el.click({ delay: randInt(40, 120) });
} catch (e) {
// beberapa input ke-overlay; coba focus saja
try { await el.focus(); } catch (e2) {}
}
} else {
try { await el.focus(); } catch (e) {}
}
await humanDelay(80, 200);
// Bersihkan isi lama
if (clearMode === 'select-all') {
await pageOrFrame.keyboard.press('Control+A');
await humanDelay(40, 100);
await pageOrFrame.keyboard.press('Backspace');
} else if (clearMode === 'backspace') {
// Hapus per karakter, max 100 (jaga-jaga)
for (let i = 0; i < 100; i++) {
const v = await el.evaluate((node) => node.value || '').catch(() => '');
if (!v) break;
await pageOrFrame.keyboard.press('Backspace');
await humanDelay(20, 60);
}
}
await humanDelay(60, 160);
await humanType(pageOrFrame, String(value), options);
await humanDelay(80, 200);
}
/**
* Versi humanFill khusus Locator API (page.locator(...)) — kadang lebih
* stabil daripada selector string karena Locator auto-wait.
*/
export async function humanFillLocator(page, locator, value, options = {}) {
await locator.scrollIntoViewIfNeeded().catch(() => {});
await locator.hover({ timeout: 3000 }).catch(() => {});
await humanDelay(80, 200);
await locator.click({ delay: randInt(40, 120) }).catch(async () => {
// fallback: focus aja
await locator.focus().catch(() => {});
});
await humanDelay(80, 200);
await page.keyboard.press('Control+A');
await humanDelay(40, 100);
await page.keyboard.press('Backspace');
await humanDelay(60, 160);
await humanType(page, String(value), options);
await humanDelay(80, 200);
}