use std::collections::HashMap;
use std::process::ExitCode;
use std::sync::mpsc;
use std::time::{Duration, Instant};
use tao::dpi::LogicalSize;
use tao::event::{Event, WindowEvent};
use tao::event_loop::{ControlFlow, EventLoopBuilder, EventLoopWindowTarget};
use tao::platform::run_return::EventLoopExtRunReturn;
use tao::window::{Window, WindowBuilder};
use wry::WebView;
use crate::webview::{self, BuildOptions, Permissions};
const PROBE_TIMEOUT: Duration = Duration::from_secs(15);
const FETCH_PROBE: &str = r#"
(function () {
var blocked = false;
document.addEventListener('securitypolicyviolation', function (e) {
var d = (e.effectiveDirective || e.violatedDirective || '');
if (d.indexOf('connect-src') === 0) blocked = true;
});
function done() { window.ipc.postMessage(blocked ? 'blocked' : 'allowed'); }
try {
fetch('https://example.com/').then(function () {}, function () {});
} catch (e) {
blocked = true;
}
setTimeout(done, 400);
})();
"#;
const CLIPBOARD_PROBE: &str = r#"
(function () {
var t = typeof navigator.clipboard;
var w = (navigator.clipboard && typeof navigator.clipboard.writeText) || 'none';
window.ipc.postMessage('clipboard:' + t + '|' + w);
})();
"#;
const STORAGE_SET_PROBE: &str = r#"
(function () {
try {
localStorage.setItem('tv_e2e', '42');
window.ipc.postMessage('set:ok');
} catch (e) {
window.ipc.postMessage('set:err:' + e);
}
})();
"#;
const STORAGE_GET_PROBE: &str = r#"
(function () {
try {
window.ipc.postMessage('get:' + localStorage.getItem('tv_e2e'));
} catch (e) {
window.ipc.postMessage('get:err:' + e);
}
})();
"#;
const STORAGE_CLEANUP_PROBE: &str = r#"
(function () {
try { localStorage.removeItem('tv_e2e'); } catch (e) {}
window.ipc.postMessage('cleanup');
})();
"#;
struct Step {
id: &'static str,
perms: Permissions,
probe: &'static str,
}
struct Active {
_window: Window,
_webview: WebView,
rx: mpsc::Receiver<String>,
started: Instant,
}
fn html_for(probe: &str) -> String {
format!(
"<!doctype html><html><head><title>e2e</title></head>\
<body><script>{probe}</script></body></html>"
)
}
fn start_step(target: &EventLoopWindowTarget<()>, step: &Step) -> Result<Active, String> {
let window = WindowBuilder::new()
.with_title("tinyview-e2e")
.with_visible(false)
.with_inner_size(LogicalSize::new(320.0, 240.0))
.build(target)
.map_err(|e| format!("window build failed: {e}"))?;
let (tx, rx) = mpsc::channel::<String>();
let html = html_for(step.probe);
let webview = webview::build(
&window,
BuildOptions {
html: &html,
perms: step.perms,
raw_mode: false,
transparent: false,
ipc_tx: Some(tx),
},
)
.map_err(|e| format!("webview build failed: {e}"))?;
Ok(Active {
_window: window,
_webview: webview,
rx,
started: Instant::now(),
})
}
pub fn run_selftest() -> ExitCode {
let allow_fetch = Permissions {
allow_fetch: true,
..Default::default()
};
let allow_clipboard = Permissions {
allow_clipboard: true,
..Default::default()
};
let allow_storage = Permissions {
allow_storage: true,
..Default::default()
};
let steps = [
Step {
id: "fetch_off",
perms: Permissions::default(),
probe: FETCH_PROBE,
},
Step {
id: "fetch_on",
perms: allow_fetch,
probe: FETCH_PROBE,
},
Step {
id: "clip_off",
perms: Permissions::default(),
probe: CLIPBOARD_PROBE,
},
Step {
id: "clip_on",
perms: allow_clipboard,
probe: CLIPBOARD_PROBE,
},
Step {
id: "store_off_set",
perms: Permissions::default(),
probe: STORAGE_SET_PROBE,
},
Step {
id: "store_off_get",
perms: Permissions::default(),
probe: STORAGE_GET_PROBE,
},
Step {
id: "store_on_set",
perms: allow_storage,
probe: STORAGE_SET_PROBE,
},
Step {
id: "store_on_get",
perms: allow_storage,
probe: STORAGE_GET_PROBE,
},
Step {
id: "cleanup",
perms: allow_storage,
probe: STORAGE_CLEANUP_PROBE,
},
];
let mut event_loop = EventLoopBuilder::<()>::with_user_event().build();
let mut results: Vec<Option<String>> = vec![None; steps.len()];
let mut idx: usize = 0;
let mut active: Option<Active> = None;
event_loop.run_return(|event, target, control_flow| {
*control_flow = ControlFlow::WaitUntil(Instant::now() + Duration::from_millis(20));
if active.is_none() && idx < steps.len() {
match start_step(target, &steps[idx]) {
Ok(a) => active = Some(a),
Err(e) => {
results[idx] = Some(format!("ERR:{e}"));
idx += 1;
}
}
}
if let Some(a) = active.as_ref() {
if let Ok(msg) = a.rx.try_recv() {
results[idx] = Some(msg);
active = None;
idx += 1;
} else if a.started.elapsed() > PROBE_TIMEOUT {
results[idx] = Some("TIMEOUT".to_string());
active = None;
idx += 1;
}
}
if idx >= steps.len() && active.is_none() {
*control_flow = ControlFlow::Exit;
}
if let Event::WindowEvent {
event: WindowEvent::CloseRequested,
..
} = event
{
*control_flow = ControlFlow::Exit;
}
});
let mut got: HashMap<&'static str, String> = HashMap::new();
for (step, res) in steps.iter().zip(results.iter()) {
got.insert(
step.id,
res.clone().unwrap_or_else(|| "MISSING".to_string()),
);
}
let g = |id: &str| {
got.get(id)
.cloned()
.unwrap_or_else(|| "MISSING".to_string())
};
let mut report = Report::default();
report.check(
"allow-fetch: default CSP blocks fetch",
g("fetch_off") == "blocked",
false,
g("fetch_off"),
);
report.check(
"allow-fetch: flag permits fetch (no CSP block)",
g("fetch_on") == "allowed",
false,
g("fetch_on"),
);
report.check(
"allow-clipboard: default does not expose navigator.clipboard",
g("clip_off") == "clipboard:undefined|none",
!cfg!(target_os = "macos"),
g("clip_off"),
);
report.check(
"allow-clipboard: opaque-origin in-memory HTML keeps clipboard unexposed",
g("clip_on") == "clipboard:undefined|none",
true,
g("clip_on"),
);
report.check(
"allow-storage: default does not persist data across runs",
g("store_off_get") != "get:42",
false,
format!("set={} get={}", g("store_off_set"), g("store_off_get")),
);
report.check(
"allow-storage: opaque-origin in-memory HTML keeps localStorage unavailable",
g("store_on_set").starts_with("set:err") || g("store_on_get") != "get:42",
true,
format!("set={} get={}", g("store_on_set"), g("store_on_get")),
);
print_report(&report)
}
#[derive(Default)]
struct Report {
passes: Vec<String>,
warnings: Vec<String>,
failures: Vec<String>,
}
impl Report {
fn check(&mut self, name: &str, ok: bool, soft: bool, detail: String) {
let line = format!("{name} — got {detail:?}");
if ok {
self.passes.push(line);
} else if soft {
self.warnings.push(line);
} else {
self.failures.push(line);
}
}
}
fn print_report(report: &Report) -> ExitCode {
println!(
"\n=== tinyview --allow-* E2E self-test ({}) ===",
std::env::consts::OS
);
for p in &report.passes {
println!(" PASS {p}");
}
for w in &report.warnings {
println!(" WARN {w}");
}
for f in &report.failures {
println!(" FAIL {f}");
}
println!(
"--- {} passed, {} warned, {} failed ---",
report.passes.len(),
report.warnings.len(),
report.failures.len()
);
if report.failures.is_empty() {
println!("E2E self-test: OK");
ExitCode::SUCCESS
} else {
eprintln!(
"E2E self-test: FAILED ({} hard failures)",
report.failures.len()
);
ExitCode::from(1)
}
}