Tujuan
OTP (Oprex Test Protocol) v1 adalah protokol JSON di atas WebSocket antara Oprex dan *executor* (extension Chrome, runner Playwright, atau agen lain) untuk menjalankan dan merekam pengujian antarmuka per objek. Spesifikasi lengkap ada di Oprex sebagai Spec #108; halaman ini adalah ringkasan yang cukup untuk menulis executor sendiri.
Transport & autentikasi
wss://api.oprex.id/otp— headerAuthorization: Bearer <oprex_pk_… (scope write) | JWT>atau?token=.- Pesan JSON UTF-8, satu pesan per frame, maks 1 MB. Bukti (screenshot) tidak lewat WS — unggah multipart ke
POST /api/v1/otp/evidence. GET /api/v1/otp/protocol→ versi & batas (keepaliveSec 25,maxSteps 500,stepTimeoutMs 30000).
Envelope
{ "otp": "1.0", "id": "msg_…", "ts": "2026-09-18T09:00:00Z",
"type": "step.result", "sessionId": "ots_…", "runId": "otr_…", "ref": "msg_…", "payload": { } }
Type: hello · welcome · ack · error · ping · pong · run.start · run.cancel · run.finished · step.exec · step.result · record.start · record.stop · event.recorded. Major versi berbeda → E_VERSION dan koneksi ditutup.
Kode error: E_AUTH · E_VERSION · E_CAPABILITY · E_LOCATOR_NOT_FOUND · E_TIMEOUT · E_ASSERT_FAILED · E_NAVIGATION_BLOCKED · E_CANCELLED · E_INTERNAL.
Handshake
Executor → hello:
{ "executor": "runner", "name": "my-runner", "version": "1.0.0",
"capabilities": { "record": false, "replay": true, "screenshot": true, "upload": true,
"crossOrigin": true, "eval": false, "maxParallel": 1 }, "projectId": "prj_…" }
Oprex → welcome { sessionId, serverTime, keepaliveSec, limits }. Langkah yang butuh kapabilitas false dilewati (skip + E_CAPABILITY), bukan gagal. Oprex mengirim ping tiap 25 s; jawab pong.
Locator
{ "by": "testid|role|label|placeholder|text|alt|title|css|xpath", "value": "…",
"name": "Simpan", "exact": false, "nth": 0, "within": { … }, "frame": "iframe#editor" }
Atau berlapis: { "candidates": [ {…}, {…} ] } — dicoba bergiliran; laporkan yang berhasil di usedLocator.
Aksi (step.exec)
{ "stepIndex": 3, "timeoutMs": 30000, "continueOnFail": false, "evidence": "onFail",
"label": "Login — 4. Klik Masuk",
"step": { "action": "click", "locator": { "by": "role", "value": "button", "name": "Masuk" } } }
navigate{url,waitUntil} · click{locator,modifiers} · dblclick · rightClick · type{locator,text,delayMs,clear} · press{key,locator?} · select{locator,by:value|label|index,value} · check · uncheck · hover · scroll{locator?,dx,dy,into?} · upload{locator,assetId} · wait{ms|locator+state|networkidle} · screenshot{fullPage} · assert · eval{script} (runner saja)
Teks boleh memuat variabel {{nama}} dari run.start.vars (rekaman menyimpan nilai sensitif sebagai {{secret_N}}).
Assert
{ "action": "assert", "kind": "text", "locator": {…}, "op": "contains", "expected": "Tersimpan" }
kind: visible|hidden|enabled|disabled|checked|text|value|count|url|title|attribute · op: equals|contains|matches|gt|lt.
Hasil (step.result)
{ "stepIndex": 3, "status": "pass|fail|skip|error", "durationMs": 812,
"error": { "code": "E_LOCATOR_NOT_FOUND", "message": "…", "triedLocators": [ … ] },
"evidence": { "screenshotAssetId": "ast_…", "consoleErrors": [ "…" ] },
"actual": "…", "usedLocator": { … } }
Setelah semua langkah, Oprex mengirim run.finished { runId, status, passed, failed, skipped, durationMs } dan, bila diminta, mencatat record_test_result per test case.
Keamanan
Sesi terikat tenant + project dari kunci; run.start.allowedOrigins harus ditegakkan executor (E_NAVIGATION_BLOCKED); eval hanya bila kapabilitas eval:true; bukti disimpan di Asset Bucket tenant (signed URL 15 menit).