Real-World Flows (E2E) — no heavy tooling
Run realistic journeys from your admin page: navigate → wait → type → click → verify.
Runs locally in the browser · canonical RTO_REQUEST/RTO_RESPONSE · respects the Allow-list
v7.10.1
Allow-list required: open the extension popup and add
this-host.
Then click .
The extension’s built-in banner will also resume the pending action after consent.
1) Minimal request helper (canonical envelopes)
One tiny function you can reuse in your pages. It posts RTO_REQUEST with action/args and resolves the paired RTO_RESPONSE (ok / data / error). The askBackoff helper retries transient failures.
<script>
const ADMIN_ORIGIN='admin', EXT_ORIGIN='extension';
let _seq=0, _waiters=new Map();
addEventListener('message', (e) => {
const d=e.data; if(!d||typeof d!=='object') return;
if(d.origin!==EXT_ORIGIN) return;
if(d.requestId && _waiters.has(d.requestId)) { _waiters.get(d.requestId)(d); _waiters.delete(d.requestId); }
});
function ask(action, args={}, timeoutMs=12000){
const requestId='r'+(++_seq);
postMessage({ origin:ADMIN_ORIGIN, type:'RTO_REQUEST', requestId, action, args }, '*');
return new Promise((resolve,reject)=>{
const t=setTimeout(()=>{ _waiters.delete(requestId); reject(Object.assign(new Error('timeout'),{ code:'TIMEOUT' })); }, timeoutMs);
_waiters.set(requestId, (resp)=>{ clearTimeout(t); resolve(resp); });
});
}
async function askBackoff(action, args={}, tries=3, base=300){
let n=0;
while(true){
try{
const r = await ask(action, args, 12000);
if(!r.ok) throw Object.assign(new Error(r.error?.message||'error'), r.error||{});
return r;
}catch(e){
if(++n>=tries) throw e;
await new Promise(r=>setTimeout(r, base*Math.pow(2,n-1)));
}
}
}
// Optional: mark this admin tab as "master" so the extension can refocus it via focusMaster
try { postMessage({ origin:ADMIN_ORIGIN, type:'RTO_REQUEST', requestId:'r0', action:'setMasterTab', args:{} }, '*'); } catch(_){}
</script>
2) What does “E2E” mean in RTO?
Plain meaning: run a user path in a real tab that you control through the extension — from your own admin page.
- Scope: open/navigate/focus → type/click/select/submit → read URL/title → optional small extraction via
runJs. - Local: everything runs in your browser (no grid, no remote server, no extra backend).
- Safety: actions run on allow-listed hosts; sensitive fields are guarded.
- Not a test framework: you add your own assertions; RTO is the automation bridge.
Stable errors: DOMAIN_NOT_ALLOWED, NO_CONTROLLED_TAB, ELEMENT_NOT_FOUND, TIMEOUT, EXECUTION_ERROR, FORBIDDEN, EXT_NOT_DETECTED.
3) Prerequisites — “why does nothing happen?”
- The extension answers a detection ping via this page’s helper.
- A remote tab is under control and
getCurrentUrl/getTitleshow a validhttps://…URL. - The target host is in the Allow-list (otherwise you’ll see
DOMAIN_NOT_ALLOWEDand a banner).
4) Message contract (canonical)
Request (Admin → Extension)
{
"origin": "admin",
"type": "RTO_REQUEST",
"requestId": "r123",
"action": "navigate",
"args": { "url": "https://example.org", "focus": true }
}
Response (ok)
{
"origin": "extension",
"type": "RTO_RESPONSE",
"requestId": "r123",
"ok": true,
"data": { "tabId": 42, "url": "https://example.org" }
}
Response (error)
{
"origin": "extension",
"type": "RTO_RESPONSE",
"requestId": "r123",
"ok": false,
"error": { "code": "DOMAIN_NOT_ALLOWED", "message": "Host not in allow-list" }
}
See Diagnostics and API pages for the error cookbook and return shapes.
5) Copy-paste flows (navigate → wait → type → click → verify)
A. Minimal journey
<script type="module">
(async () => {
// Warm the message path (or use a detect call if available)
await askBackoff('runJs', { code:'true' }, 1, 1);
await ask('navigate', { url:'https://example.org', focus:true });
await ask('waitFor', { selector:'#q', timeoutMs:20000 });
await ask('domType', { selector:'#q', value:'rto demo' });
await ask('domClick', { selector:'form button[type=submit]' });
const r = await ask('getCurrentUrl');
console.log('Now at:', r.ok ? r.data?.url : '(unknown)');
})();
</script>
B. Public “login” form with resilient waits
<script type="module">
(async () => {
await askBackoff('navigate', { url:'https://site.example/login', focus:true }, 2, 250);
await ask('waitFor', { selector:'#email', timeoutMs:20000 });
await ask('domType', { selector:'#email', value:'user@example.org' });
await ask('domType', { selector:'#password', value:'s3cret' });
await ask('domClick', { selector:'form button[type=submit]' });
// Wait for a dashboard marker with backoff
await askBackoff('waitFor', { selector:'.dashboard', timeoutMs:25000 }, 3, 300);
})();
</script>
C. Multi-tab & focus strategy
<script type="module">
(async () => {
await ask('openTab', { url:'https://site-a.example', focus:true, newTab:true, tabKey:'a' });
await ask('openTab', { url:'https://site-b.example', focus:false, newTab:true, tabKey:'b' });
await ask('focus', { tabKey:'b' }); // bring tab 'b' to front
// Or go back to this admin page:
await ask('focusMaster');
})();
</script>
D. Extract text/HTML via runJs
<script type="module">
(async () => {
await ask('navigate', { url:'https://blog.example.org/post/123' });
const title = await ask('runJs', { code:'(document.querySelector("h1")||{}).textContent||""' });
const html = await ask('runJs', { code:'(document.querySelector(".content")||{}).innerHTML||""' });
console.log('Title:', title.ok ? title.data : '');
console.log('HTML :', html.ok ? html.data : '');
})();
</script>
6) Simple assertions (optional)
Keep assertions in your admin page. Mix
getCurrentUrl/getTitle with targeted waitFor and bounded timeouts.<script type="module">
async function expectExists(selector, timeoutMs = 12000) {
const t0 = Date.now();
while (Date.now() - t0 < timeoutMs) {
const r = await ask('runJs', { code:`!!document.querySelector(${JSON.stringify(selector)})` });
if (r.ok && r.data) return true;
await new Promise(r => setTimeout(r, 300));
}
const err = new Error('ELEMENT_NOT_FOUND');
err.code='ELEMENT_NOT_FOUND';
err.selector=selector;
throw err;
}
</script>
7) Best practices (stability & safety)
- Deny-by-default: add only the hosts you need in the Allow-list.
- Assert before act: check
getCurrentUrlandwaitFora concrete marker; avoid arbitrary sleeps. - Timeouts: 12–30s for heavy SPAs; wrap critical steps with
askBackoff. - Selectors: prefer stable (
#id,[data-testid],aria-*). - Focus hygiene: use
suppressNextFocusfor one-shot silent navigations;focusMasterto come back here. - Sensitive data: password-like inputs are guarded; don’t try to exfiltrate secrets.
8) Troubleshooting & error handling
Error cookbook (quick map)
switch (err.code) {
case 'DOMAIN_NOT_ALLOWED': /* add host to Allow-list (popup) */ break;
case 'NO_CONTROLLED_TAB' : /* call openTab/navigate first */ break;
case 'ELEMENT_NOT_FOUND' : /* fix selector or waitFor */ break;
case 'TIMEOUT' : /* increase timeout or add backoff */ break;
case 'EXECUTION_ERROR' : /* check page console; guard nulls */ break;
case 'FORBIDDEN' : /* sensitive field blocked; redesign the step */ break;
case 'EXT_NOT_DETECTED' : /* install/enable the extension */ break;
}
Use the Diagnostics page’s inline console to see live events & payloads.
Allow-list banner pattern
If an action yields DOMAIN_NOT_ALLOWED, show a friendly hint and provide a “Try again”.
function handleActionError(e, url){
const code = e?.code || e?.message || '';
if (String(code).includes('DOMAIN_NOT_ALLOWED')){
const host = (()=>{ try { return new URL(url).host; } catch(_) { return ''; } })();
const box = document.getElementById('allow-box');
if (box){ box.classList.remove('d-none'); box.querySelector('.domain').textContent = host; }
} else {
console.warn('Action failed:', e);
}
}
Next pages
| Page | Description |
|---|---|
| Start | Overview, how it works, setup, quick start, and links to all sections. |
| Detect the Extension | Detection contract (ping/pong), install prompts, and graceful fallbacks when the extension isn’t present. |
| Allow-list & Permissions | Deny-by-default model, domain allow-list via popup, and suggested UX to guide users to enable hosts. |
| Remote Tab Control | Open/navigate/focus the controlled tab, read URL/title, select an existing tab, lifecycle & restore patterns. |
| DOM & Automation | Action catalog (domType, domSetValue, domClick, selectSetValue, submitForm, waitFor) and safe interaction tips. |
| Recipes & Flows | Copy-paste flows: resilient login, dashboard checks, filters, playlists, and allow-list banners. |
| Events | Event stream reference, subscription helpers, live logger widget, and basic metrics/durations collection. |
| Diagnostics | Inline console, error cookbook, QA self-check, and performance tips for troubleshooting flows quickly. |
| Full API Reference | Canonical message envelopes, request/response schemas, error codes, and return shapes for every action. |
| Compatibility | Supported browsers/versions, permissions nuances, CSP/iframe notes, and known limitations or workarounds. |
| Advanced Patterns | Idempotent flows, retries/backoff, state restore after reload, playlist strategies, and robust selectors. |
| Favorites | Star and group URLs, quick actions (open/focus/navigate), export/import sets, and playlist runs from favorites. |
| Real-World Flows (E2E) | Run end-to-end flows from your admin page with safe, stable message contracts and resilient waits. |