//! Execute the emitted JavaScript for real (ADR-0009).
//!
//! ADR-0007 control 5 says the page **may not construct commands**: it
//! reports raw pointer facts and Rust decides what they mean. Until now
//! that contract was held up by a test that greps the emitted script for
//! game vocabulary — and grepping for the absence of words is a weak
//! proxy for *"this code cannot construct a command"*.
//!
//! This runs it. QuickJS, a DOM stub with exactly the surface `SCRIPT`
//! touches, and a Rust callback standing in for `fetch` so the test can
//! see precisely what would have gone on the wire.
//!
//! **The scripts are lifted from a real emitted document, not pasted
//! here.** Both `` body out of a document, in order.
pub fn scripts(html: &str) -> Vec {
let mut out = Vec::new();
let mut rest = html;
while let Some(i) = rest.find("") {
Some(j) => {
out.push(after[..j].to_string());
rest = &after[j..];
}
None => break,
}
}
out
}
/// The DOM surface `SCRIPT` touches, and nothing more.
///
/// Deliberately minimal: every additional stubbed API is a way for the
/// script to do something in the test that it could not do in a browser,
/// or vice versa. If `SCRIPT` ever needs more than this, that is a signal
/// about the script, not about the stub.
const DOM: &str = r#"
var __handlers = {};
var __status = { textContent: "" };
var document = {
addEventListener: function (type, fn) { __handlers[type] = fn; },
getElementById: function (id) { return __status; }
};
var window = { location: { reload: function () { __reloaded(); } } };
function fetch(url, opts) {
__post(url, (opts && opts.body) || "");
// The script chains .then(...).then(...); give it something to chain on
// without ever resolving, so response handling stays out of scope here.
var chainable = { then: function () { return chainable; } };
return chainable;
}
function __down(id) { __handlers['pointerdown']({ target: { id: id } }); }
function __up(id) { __handlers['pointerup']({ target: { id: id } }); }
"#;
/// Run the document's scripts, then a pointer gesture, and report what
/// the page tried to send.
///
/// `gesture` is `(down_id, up_id)`. Errors are JS errors, and they are
/// returned rather than swallowed: a script that throws must not look
/// like a script that sent nothing.
pub fn gesture(html: &str, down: &str, up: &str) -> Result, String> {
let ctx = quick_js::Context::new().map_err(|e| format!("quickjs init: {e}"))?;
// `Arc>` rather than `Rc>`: quick-js requires the
// callback to be unwind-safe, since a panic inside it would cross the
// C boundary.
let posted: Arc>> = Arc::new(Mutex::new(Vec::new()));
let sink = posted.clone();
ctx.add_callback("__post", move |url: String, body: String| {
sink.lock().expect("posted").push(Posted { url, body });
0i32
})
.map_err(|e| format!("register __post: {e}"))?;
ctx.add_callback("__reloaded", || 0i32)
.map_err(|e| format!("register __reloaded: {e}"))?;
ctx.eval(DOM).map_err(|e| format!("dom stub: {e}"))?;
let found = scripts(html);
if found.is_empty() {
return Err("the document carries no ", "a", "b").unwrap_err();
assert!(e.contains("no pointer handlers"), "{e}");
}
#[test]
fn both_script_blocks_are_extracted_in_order() {
let found = scripts(&page());
assert_eq!(
found.len(),
2,
"the page carries the endpoint and the script"
);
assert!(found[0].contains("CB_ENDPOINT"), "{}", found[0]);
assert!(found[1].contains("pointerdown"), "{}", found[1]);
}
}