//! ScriptUI resource strings, onDraw graphics, live onChanging updates and sockets. use std::sync::Arc; use effectcraft_engine::Session; use effectcraft_engine::scriptui::{DrawOp, PathSeg, WidgetKind, WindowKind}; use serde_json::json; use crate::run_code; fn session() -> Session { let mut s = Session { expr: Some(Arc::new(effectcraft_expr::Expressions)), ..Default::default() }; crate::install(&mut s); s.config = Some(Arc::new(effectcraft_engine::config::MemoryConfig::default())); s } const RESOURCE: &str = r#" var w = new Window("palette { text: 'Res', orientation: 'column', alignChildren: ['fill', 'top'], \ g: Group { orientation: 'row', b: Button { text: 'OK', properties: { name: 'okb' } }, cancel: Button { text: \"Can't\" } }, \ nm: EditText { text: 'abc', characters: 10 }, \ s: Slider { minvalue: 0, maxvalue: 10, value: 4 }, \ dd: DropDownList { properties: { items: ['A', 'B'] }, selection: 1 }, \ /* a panel */ p: Panel { text: 'Opts', preferredSize: [200, -1], c: Checkbox { text: 'On', value: true } } }"); var extra = w.p.add("group { orientation: 'column', t: StaticText { text: 'hi' } }"); w.g.b.onClick = function () { writeLn("ok " + w.nm.text + " " + w.s.value); }; w.show(); [w.text, w.g.orientation, w.g.b.text, w.g.cancel.text, w.s.maxvalue, w.dd.selection.text, w.p.c.value, extra.t.text, w.g.children.length, w.p.preferredSize.width].join("|") "#; #[test] fn resource_strings_build_windows_and_controls() { let mut s = session(); let o = run_code(&mut s, RESOURCE, "res.jsx"); assert!(o.error.is_none(), "{:?}", o.error); assert_eq!(o.result, json!("Res|row|OK|Can't|10|B|true|hi|2|200")); let w = s.script_ui.windows[0].clone(); assert_eq!((w.kind, w.title.as_str()), (WindowKind::Palette, "Res")); let kinds: Vec = w.root.children.iter().map(|c| c.kind).collect(); assert_eq!(kinds, [WidgetKind::Group, WidgetKind::EditText, WidgetKind::Slider, WidgetKind::DropDownList, WidgetKind::Panel]); assert_eq!(w.root.children[2].value, 4.0); assert_eq!(w.root.children[3].items, ["A", "B"]); assert_eq!(w.root.children[3].selection, [1]); assert!(w.root.children[4].children[0].checked); assert_eq!(w.root.children[4].children[1].children[0].text, "hi"); // Elements are named after their resource keys (agents address them by it). assert_eq!(w.root.children[1].name, "nm"); s.execute("scriptui.set", json!({"widget": "nm", "value": "xyz"})).unwrap(); let r = s.execute("scriptui.click", json!({"widget": "okb"})).unwrap(); assert_eq!(r["output"], "ok xyz 4", "{r}"); s.execute("scriptui.close", json!({})).unwrap(); // Syntax errors say where. let o = run_code(&mut s, "new Window(\"dialog { text: 'x' oops }\")", "bad.jsx"); assert!(o.error.unwrap().message.contains("resource string")); let o = run_code(&mut s, "new Window(\"panel { }\")", "bad.jsx"); assert!(o.error.unwrap().message.contains("Bad window type")); } const DRAW: &str = r#" var w = new Window("palette", "Draw"); var c = w.add("group"); c.preferredSize = [100, 50]; c.onDraw = function () { var g = this.graphics; var b = g.newBrush(g.BrushType.SOLID_COLOR, [1, 0, 0, 1]); g.newPath(); g.rectPath(0, 0, this.size.width, this.size.height); g.fillPath(b); var p = g.newPen(g.PenType.SOLID_COLOR, [0, 0, 1], 2); g.newPath(); g.moveTo(0, 0); g.lineTo(10, 10); g.strokePath(p); g.ellipsePath(20, 20, 10, 10); g.drawString("Hi", p, 5, 6, ScriptUI.newFont("dialog", "BOLD", 20)); }; var m = c.graphics.measureString("Hello", ScriptUI.newFont("dialog", "REGULAR", 12)); w.show(); [m.width, m[1]].join(",") "#; #[test] fn on_draw_handlers_record_a_draw_list() { let mut s = session(); let o = run_code(&mut s, DRAW, "draw.jsx"); assert!(o.error.is_none(), "{:?}", o.error); assert_eq!(o.result, json!("35,15")); let w = s.script_ui.windows[0].clone(); let g = &w.root.children[0]; assert!(g.handlers.contains(&"onDraw".to_string())); assert_eq!(g.draw.len(), 3, "{:?}", g.draw); // The handler saw the laid-out size. assert_eq!(g.draw[0], DrawOp::Fill { color: Some([1.0, 0.0, 0.0, 1.0]), path: vec![PathSeg::R { x: 0.0, y: 0.0, w: 100.0, h: 50.0 }] }); match &g.draw[1] { DrawOp::Stroke { color, width, path } => { assert_eq!((*color, *width), (Some([0.0, 0.0, 1.0, 1.0]), 2.0)); assert_eq!(path.len(), 2); } o => panic!("{o:?}"), } assert!(matches!(&g.draw[2], DrawOp::Text { text, size, style, .. } if text == "Hi" && *size == 20.0 && style == "BOLD")); // scriptui.get carries it for agents. let tree = s.execute("scriptui.get", json!({})).unwrap(); assert_eq!(tree["root"]["children"][0]["draw"][0]["op"], "fill"); s.execute("scriptui.close", json!({})).unwrap(); } const LIVE: &str = r#" var w = new Window("palette", "Live"); var e = w.add("edittext", undefined, ""); var sl = w.add("slider", undefined, 0, 0, 100); var log = w.add("statictext", undefined, "-"); e.onChanging = function () { writeLn("changing " + e.text); log.text = "typing " + e.text; }; e.onChange = function () { writeLn("change " + e.text); }; sl.onChanging = function () { writeLn("slide " + Math.round(sl.value)); }; sl.onChange = function () { writeLn("slid " + Math.round(sl.value)); }; w.show(); "#; #[test] fn on_changing_fires_per_keystroke_and_slider_step() { let mut s = session(); let o = run_code(&mut s, LIVE, "live.jsx"); assert!(o.error.is_none(), "{:?}", o.error); let r = s.execute("scriptui.set", json!({"widget": "#1", "value": "a", "changing": true})).unwrap(); assert_eq!(r["output"], "changing a"); let r = s.execute("scriptui.set", json!({"widget": "#1", "value": "ab", "changing": true})).unwrap(); assert_eq!(r["output"], "changing ab"); assert_eq!(s.script_ui.windows[0].root.children[2].text, "typing ab"); // Committing the same text runs onChange only. let r = s.execute("scriptui.set", json!({"widget": "#1", "value": "ab"})).unwrap(); assert_eq!(r["output"], "change ab"); // A committed edit without live updates runs both. let r = s.execute("scriptui.set", json!({"widget": "#1", "value": "abc"})).unwrap(); assert_eq!(r["output"], "changing abc\nchange abc"); // Slider drags update the value live. let r = s.execute("scriptui.set", json!({"widget": "#2", "value": 40, "changing": true})).unwrap(); assert_eq!(r["output"], "slide 40"); assert_eq!(s.script_ui.windows[0].root.children[1].value, 40.0); let r = s.execute("scriptui.set", json!({"widget": "#2", "value": 41})).unwrap(); assert_eq!(r["output"], "slide 41\nslid 41"); s.execute("scriptui.close", json!({})).unwrap(); } const MOGRT: &str = r#" var c = app.project.activeItem; var t = c.layer(1); var st = t.property("ADBE Text Properties").property("ADBE Text Document"); var op = t.property("ADBE Transform Group").property("ADBE Opacity"); var r = [st.canAddToMotionGraphicsTemplate(c), st.addToMotionGraphicsTemplate(c), op.addToMotionGraphicsTemplateAs(c, "Fade")]; c.motionGraphicsTemplateName = "Lower Third"; c.setMotionGraphicsControllerName(1, "Title"); r.concat([c.motionGraphicsTemplateName, c.motionGraphicsTemplateControllerCount, c.getMotionGraphicsTemplateControllerName(1), c.getMotionGraphicsTemplateControllerName(2)]).join("|") "#; #[test] fn essential_graphics_scripting_hooks() { let mut s = session(); s.execute("comp.new", json!({"name": "Card", "width": 64, "height": 64, "duration": 1})).unwrap(); s.execute("layer.newText", json!({"text": "Hi"})).unwrap(); let o = run_code(&mut s, MOGRT, "mogrt.jsx"); assert!(o.error.is_none(), "{:?} {:?}", o.error, o.output); assert_eq!(o.result, json!("true|true|true|Lower Third|2|Title|Fade")); let cid = s.active_comp_id().unwrap(); assert_eq!(s.project.comp(cid).unwrap().essential.as_ref().unwrap().name, "Lower Third"); // Another comp can't take this comp's properties. s.execute("comp.new", json!({"name": "Other", "width": 64, "height": 64, "duration": 1})).unwrap(); let code = "var card = null; for (var i = 1; i <= app.project.numItems; i++) if (app.project.item(i).name === 'Card') card = app.project.item(i); var o = app.project.activeItem; var p = card.layer(1).property('ADBE Transform Group').property('ADBE Position'); [p.canAddToMotionGraphicsTemplate(o), p.canAddToMotionGraphicsTemplate(card)].join('|')"; let o = run_code(&mut s, code, "x.jsx"); assert_eq!(o.result, json!("false|true"), "{:?}", o.error); // Export (behind the file-write preference). let dir = std::env::temp_dir().join(format!("ec-mogrt-{}", std::process::id())); let _ = std::fs::create_dir_all(&dir); let path = dir.join("lower.ectemplate"); let export = format!( "var card = null; for (var i = 1; i <= app.project.numItems; i++) if (app.project.item(i).name === 'Card') card = app.project.item(i); card.exportAsMotionGraphicsTemplate(false, {:?})", path.to_string_lossy() ); assert!(run_code(&mut s, &export, "x.jsx").error.is_some()); s.prefs.scripting.allow_scripts_write_files = true; assert_eq!(run_code(&mut s, &export, "x.jsx").result, json!(true)); assert!(std::fs::metadata(&path).unwrap().len() > 100); // It exists now: no overwrite → false. assert_eq!(run_code(&mut s, &export, "x.jsx").result, json!(false)); let _ = std::fs::remove_dir_all(&dir); } #[test] fn create_nulls_from_paths_expressions_evaluate() { let mut s = session(); s.execute("comp.new", json!({"name": "Paths", "width": 100, "height": 80, "frameRate": 10, "duration": 1})).unwrap(); let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#808080", "width": 40, "height": 40})).unwrap()["layer"].as_u64().unwrap(); s.execute("mask.new", json!({"layer": l, "vertices": [[0, 0], [40, 0], [40, 40], [0, 40]], "closed": true})).unwrap(); s.execute("paths.nullsFollowPoints", json!({"layer": l})).unwrap(); s.execute("paths.tracePath", json!({"layer": l})).unwrap(); // Layer 1: the trace null; layers 2..5: the vertex nulls (top = last vertex). let pos = |i: u32, t: f64| format!("app.project.activeItem.layer({i}).property('ADBE Transform Group').property('ADBE Position').valueAtTime({t}, false)"); let o = run_code(&mut s, &format!("[{}, {}, {}].join('|')", pos(2, 0.0), pos(1, 0.0), pos(1, 0.5)), "x.jsx"); assert!(o.error.is_none(), "{:?}", o.error); let parts: Vec> = o.result.as_str().unwrap().split('|').map(|p| p.split(',').map(|x| x.parse().unwrap()).collect()).collect(); assert_eq!(&parts[0][..2], &[30.0, 60.0], "vertex 4 (0, 40) in comp space"); assert_eq!(&parts[1][..2], &[30.0, 20.0], "progress 0: the first vertex"); // Halfway in time along the closed square: the opposite corner. assert!((parts[2][0] - 70.0).abs() < 0.5 && (parts[2][1] - 60.0).abs() < 0.5, "{:?}", parts[2]); // Points Follow Nulls: moving the last vertex's null moves the vertex. let r = s.execute("paths.pointsFollowNulls", json!({"layer": l})).unwrap(); let last = r["nulls"][3].as_u64().unwrap(); s.execute("prop.set", json!({"layer": last, "path": "transform/position", "value": [10, 10]})).unwrap(); let code = "var c = app.project.activeItem; var m = c.layer(c.numLayers).property('ADBE Mask Parade').property(1).property('ADBE Mask Shape').valueAtTime(0, false); [m.vertices[3][0], m.vertices[3][1], m.vertices[0][0], m.vertices.length].join(',')"; let o = run_code(&mut s, code, "x.jsx"); assert_eq!(o.result, json!("-20,-10,0,4"), "{:?}", o.error); } fn pin_at(s: &mut Session, id: u64, i: usize) -> [f64; 2] { let info = s.execute("puppet.info", json!({"layer": id})).unwrap(); let v = &info["meshes"][0]["pins"][i]["position"]; [v[0].as_f64().unwrap(), v[1].as_f64().unwrap()] } /// A layer's Position now, with its expression applied (what renders). fn position_now(s: &Session, layer: u64) -> [f64; 2] { let cid = s.active_comp_id().unwrap(); let comp = s.project.comp(cid).unwrap(); let l = comp.layer(effectcraft_engine::project::LayerId(layer)).unwrap(); let ctx = effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: None }; let v = ctx.value(l, l.props.prop("transform/position").unwrap()).components(); [v[0], v[1]] } fn near(a: [f64; 2], b: [f64; 2]) -> bool { (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6 } #[test] fn puppet_pin_rigs_follow_and_lead_with_live_expressions() { let mut s = session(); s.execute("comp.new", json!({"name": "C", "width": 200, "height": 100, "duration": 2, "frameRate": 30})).unwrap(); let id = s.execute("layer.newSolid", json!({"width": 100, "height": 60, "color": [0, 0, 1]})).unwrap()["layer"].as_u64().unwrap(); s.execute("puppet.addPin", json!({"layer": id, "position": [10, 30]})).unwrap(); s.execute("puppet.addPin", json!({"layer": id, "position": [90, 30]})).unwrap(); s.execute("puppet.selectPins", json!({"layer": id, "pins": []})).unwrap(); let r = s.execute("paths.pointsFollowNulls", json!({"layer": id})).unwrap(); let nulls: Vec = r["nulls"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect(); // Rigged at rest: the pins haven't moved. Layer (10, 30) is comp (60, 50). assert!(near(pin_at(&mut s, id, 0), [10.0, 30.0]), "{:?}", pin_at(&mut s, id, 0)); // Moving the null moves its pin. s.execute("prop.set", json!({"layer": nulls[0], "path": "transform/position", "value": [60, 80]})).unwrap(); assert!(near(pin_at(&mut s, id, 0), [10.0, 60.0]), "{:?}", pin_at(&mut s, id, 0)); // Parented nulls rig a limb: pin 2's null rides on pin 1's null. s.execute("layer.setParent", json!({"layers": [nulls[1]], "parent": nulls[0]})).unwrap(); s.execute("prop.set", json!({"layer": nulls[0], "path": "transform/position", "value": [60, 90]})).unwrap(); assert!(near(pin_at(&mut s, id, 1), [90.0, 40.0]), "{:?}", pin_at(&mut s, id, 1)); // Nulls Follow Points: a null riding on pin 1, now at comp (60, 90). let r = s.execute("paths.nullsFollowPoints", json!({"layer": id, "pins": ["Puppet Pin 1"]})).unwrap(); let rider = r["nulls"][0].as_u64().unwrap(); assert!(near(position_now(&s, rider), [60.0, 90.0]), "{:?}", position_now(&s, rider)); } #[test] fn puppet_follow_through_delays_the_leaders_motion() { let mut s = session(); s.execute("comp.new", json!({"name": "C", "width": 200, "height": 100, "duration": 2, "frameRate": 30})).unwrap(); let id = s.execute("layer.newSolid", json!({"width": 100, "height": 60, "color": [0, 0, 1]})).unwrap()["layer"].as_u64().unwrap(); let lead = s.execute("puppet.addPin", json!({"layer": id, "position": [10, 30]})).unwrap()["pin"].clone(); s.execute("puppet.addPin", json!({"layer": id, "position": [50, 30]})).unwrap(); s.execute("puppet.addPin", json!({"layer": id, "position": [90, 30]})).unwrap(); // The leader drops 20 px between 0 and 0.5 s. s.execute("puppet.movePin", json!({"layer": id, "pin": lead, "position": [10, 50], "time": 0.5})).unwrap(); s.execute("puppet.follow", json!({"layer": id, "leader": "Puppet Pin 1", "pins": ["Puppet Pin 2", "Puppet Pin 3"], "delay": 0.2})).unwrap(); let y = |s: &mut Session, pin: usize, t: f64| { let info = s.execute("puppet.info", json!({"layer": id, "time": t})).unwrap(); info["meshes"][0]["pins"][pin]["position"][1].as_f64().unwrap() }; // At rest before anything moves; pin 2 trails 0.2 s and pin 3 (farther) 0.4 s. assert_eq!((y(&mut s, 1, 0.0), y(&mut s, 2, 0.0)), (30.0, 30.0)); let (lead_y, p2, p3) = (y(&mut s, 0, 0.5), y(&mut s, 1, 0.5), y(&mut s, 2, 0.5)); assert!(lead_y > p2 && p2 > p3 && p3 > 30.0, "{lead_y} {p2} {p3}"); assert!((y(&mut s, 1, 0.7) - 50.0).abs() < 1e-6, "pin 2 arrives 0.2 s after the leader"); assert!(y(&mut s, 2, 0.7) < 50.0 - 1e-3); assert!((y(&mut s, 2, 0.9) - 50.0).abs() < 1e-6, "pin 3 arrives 0.4 s after the leader"); } #[test] fn sockets_talk_tcp_behind_the_network_preference() { use std::io::{BufRead, BufReader, Write}; let mut s = session(); let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); let server = std::thread::spawn(move || { let (mut c, _) = listener.accept().unwrap(); let mut line = String::new(); BufReader::new(c.try_clone().unwrap()).read_line(&mut line).unwrap(); c.write_all(format!("pong:{}\nrest", line.trim()).as_bytes()).unwrap(); }); let code = format!( "var s = new Socket(); var ok = s.open('127.0.0.1:{port}'); s.writeln('ping'); var r = s.readln(); var rest = s.read(); var e = s.eof; s.close(); [ok, r, rest, e, s.connected].join('|')" ); // Off: no network. let o = run_code(&mut s, &code, "net.jsx"); assert!(o.error.unwrap().message.contains("Access Network")); s.prefs.scripting.allow_scripts_write_files = true; let o = run_code(&mut s, &code, "net.jsx"); assert!(o.error.is_none(), "{:?}", o.error); assert_eq!(o.result, json!("true|pong:ping|rest|true|false")); server.join().unwrap(); // Refused connections return false with an error; listen + poll serve clients. let o = run_code(&mut s, "var s = new Socket(); s.timeout = 1; var ok = s.open('127.0.0.1:1'); [ok, s.error.length > 0].join('|')", "net.jsx"); assert_eq!(o.result, json!("false|true")); let o = run_code(&mut s, "var l = new Socket(); l.listen(0); var r = [l.port > 0, l.poll() === null].join('|'); l.close(); r", "net.jsx"); assert_eq!(o.result, json!("true|true")); } /// `__uiParseRes(src)` as JSON, or the error message. fn parse_res(s: &mut Session, src: &str) -> Result { let code = format!("JSON.stringify(__uiParseRes({}))", serde_json::to_string(src).unwrap()); let o = run_code(s, &code, "res.jsx"); match o.error { Some(e) => Err(e.message), None => Ok(serde_json::from_str(o.result.as_str().unwrap_or("null")).unwrap_or_default()), } } #[test] fn resource_string_parser_edge_cases() { let mut s = session(); // Accepted: trailing commas, quoted keys, number forms, escapes, comments, empty bodies, // null / undefined, bare words, nested arrays and object literals. let ok = parse_res( &mut s, "group { 'quoted key': 1, \"dq\": -.5, n: +3, e: 1e2, arr: [[1, [2]], 'x', ], \ esc: 'it\\'s\\n\\\\', /* block */ nul: null, u: undefined, w: fill, // line\n \ obj: { a: { b: true } }, kid: Button {}, }", ) .unwrap(); assert_eq!(ok["type"], "group"); let list = ok["list"].as_array().unwrap(); let get = |k: &str| list.iter().find(|e| e[0] == k).map(|e| e[1].clone()).unwrap(); assert_eq!(get("quoted key"), json!(1)); assert_eq!(get("dq"), json!(-0.5)); assert_eq!(get("n"), json!(3)); assert_eq!(get("e"), json!(100)); assert_eq!(get("arr"), json!([[1, [2]], "x"])); assert_eq!(get("esc"), json!("it's\n\\")); assert_eq!(get("nul"), json!(null)); assert_eq!(get("w"), json!("fill")); assert_eq!(get("kid"), json!({"__element": "Button", "list": []})); // Rejected with a located message (never a crash). for bad in [ "", " ", "dialog", "dialog {", "dialog { text: 'unterminated }", "dialog { text 'x' }", "dialog { a: [1, 2 }", "dialog { a: 1 } trailing", "dialog { /* unterminated comment", "dialog { a: 'x\\", "{ }", "123 { }", "dialog { a: 1 b: 2 }", "dialog { : 1 }", "dialog { a: }", "dialog { a: Button { }", ] { let e = parse_res(&mut s, bad).expect_err(bad); assert!(e.contains("resource string"), "{bad:?}: {e}"); assert!(e.contains("offset"), "{bad:?}: {e}"); } // Deep nesting builds; absurd nesting fails cleanly (the script engine's recursion limit). let deep = |n: usize| format!("group {{ {}{} }}", "g: Group { ".repeat(n), "}".repeat(n)); let d = parse_res(&mut s, &deep(40)).unwrap(); assert_eq!(d["list"][0][1]["__element"], "Group"); let _ = parse_res(&mut s, &deep(5000)); // A window from a deep resource. let o = run_code( &mut s, &format!( "var w = new Window({}); w.children.length", serde_json::to_string(&format!("palette {{ {}{} }}", "g: Group { ".repeat(20), "}".repeat(20))).unwrap() ), "deep.jsx", ); assert_eq!(o.result, json!(1), "{:?}", o.error); } #[test] fn socket_edge_cases() { let mut s = session(); s.prefs.scripting.allow_scripts_write_files = true; // Bad addresses and ports: false with an error, never a throw or crash. for host in ["", "nohostport", "127.0.0.1", "127.0.0.1:99999", "[::1", "a:b:c", "127.0.0.1:-5"] { let code = format!( "var s = new Socket(); s.timeout = 1; var ok = s.open({}); [ok, s.error.length > 0, s.connected, s.eof].join('|')", serde_json::to_string(host).unwrap() ); let o = run_code(&mut s, &code, "net.jsx"); assert_eq!(o.result, json!("false|true|false|true"), "{host:?}: {:?}", o.error); } for port in ["70000", "-1", "1.5"] { let o = run_code(&mut s, &format!("var l = new Socket(); [l.listen({port}), l.error.length > 0].join('|')"), "net.jsx"); assert_eq!(o.result, json!("false|true"), "{port}: {:?}", o.error); } // Never opened or closed: reads are empty, writes fail, close is idempotent. let o = run_code( &mut s, "var s = new Socket(); var r = [s.read(), s.read(5), s.readln(), s.write('x'), s.connected, s.eof, s.poll(), s.close(), s.close()]; r.join('|')", "net.jsx", ); assert_eq!(o.result, json!("|||false|false|true||true|true"), "{:?}", o.error); // Binary bytes round trip in BINARY encoding; invalid UTF-8 is replaced in UTF-8; partial // and zero-length reads; read stops at the timeout without data. let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); let port = listener.local_addr().unwrap().port(); let server = std::thread::spawn(move || { use std::io::Write; for _ in 0..2 { let (mut c, _) = listener.accept().unwrap(); let bytes: Vec = (0..=255u8).collect(); c.write_all(&bytes).unwrap(); } }); let code = format!( "var s = new Socket(); s.open('127.0.0.1:{port}', 'BINARY'); var z = s.read(0); var a = s.read(10); var b = s.read(); s.close(); \ var ok = true; for (var i = 0; i < 10; i++) ok = ok && a.charCodeAt(i) === i; for (var j = 0; j < 246; j++) ok = ok && b.charCodeAt(j) === j + 10; \ [z.length, a.length, b.length, ok].join('|')" ); let o = run_code(&mut s, &code, "net.jsx"); assert_eq!(o.result, json!("0|10|246|true"), "{:?}", o.error); let code = format!( "var s = new Socket(); s.open('127.0.0.1:{port}', 'UTF-8'); var d = s.read(); s.close(); [d.charCodeAt(0), d.charCodeAt(127), d.indexOf('\\ufffd') > 0].join('|')" ); let o = run_code(&mut s, &code, "net.jsx"); assert_eq!(o.result, json!("0|127|true"), "{:?}", o.error); server.join().unwrap(); // A listener with nobody connecting: a short read timeout returns quickly. let t = std::time::Instant::now(); let o = run_code( &mut s, "var l = new Socket(); l.listen(0); l.timeout = 0; var c = new Socket(); c.timeout = 0.2; c.open(l.host); var x = c.read(); c.close(); l.close(); x.length", "net.jsx", ); assert_eq!(o.result, json!(0), "{:?}", o.error); assert!(t.elapsed().as_secs_f64() < 5.0); }