//! 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]); } }