146 lines
4.7 KiB
JavaScript
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);
|
|
}
|