845 lines
30 KiB
Rust
845 lines
30 KiB
Rust
use super::*;
|
|
use std::{
|
|
io::Write,
|
|
net::{SocketAddr, TcpStream},
|
|
sync::atomic::{AtomicUsize, Ordering},
|
|
time::Duration,
|
|
};
|
|
|
|
#[test]
|
|
fn preserved_picture_close_does_not_retire_same_number_in_fresh_console_vm() {
|
|
let (_, mut picture) = endor_host::ReplSession::load(
|
|
include_str!("../../../examples/windows.endor"),
|
|
"app.main",
|
|
1,
|
|
150,
|
|
)
|
|
.unwrap();
|
|
picture.frame.session = 0;
|
|
for view in picture.windows.values_mut() {
|
|
view.frame.session = 0;
|
|
}
|
|
let mut fresh = endor_host::ReplSession::new(151).unwrap();
|
|
assert!(
|
|
fresh
|
|
.execute("pending=Window()\nui.window().resize(0,600)", "app.main")
|
|
.is_err()
|
|
);
|
|
assert!(fresh.accepts_window(2));
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
document: picture,
|
|
submission: 1,
|
|
runtime_session: 151,
|
|
..NamespaceState::default()
|
|
}));
|
|
let mut worker = Worker {
|
|
repl: Some((1, fresh)),
|
|
next_session: 151,
|
|
};
|
|
let notify: Notify = Arc::new(|| {});
|
|
namespace.write().unwrap().window_viewports.insert(
|
|
2,
|
|
crate::layout::Size {
|
|
width: 800.0,
|
|
height: 600.0,
|
|
},
|
|
);
|
|
worker.refresh_viewport(&namespace, ¬ify);
|
|
assert_eq!(
|
|
namespace.read().unwrap().document.windows[&2].frame.session,
|
|
0
|
|
);
|
|
let response = worker.close_inactive_window(
|
|
&crate::events::Input {
|
|
window: 2,
|
|
session: 0,
|
|
layout_version: 0,
|
|
mount: 0,
|
|
kind: "window-close".into(),
|
|
point: None,
|
|
delta: None,
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
},
|
|
1,
|
|
&namespace,
|
|
¬ify,
|
|
);
|
|
assert_eq!(response.status_code(), StatusCode(200));
|
|
assert!(namespace.read().unwrap().document.windows.is_empty());
|
|
let runtime = &mut worker.repl.as_mut().unwrap().1;
|
|
assert_eq!(runtime.id, 151);
|
|
assert!(runtime.accepts_window(2));
|
|
assert_eq!(
|
|
runtime.execute("pending.id()", "app.main").unwrap().value,
|
|
"2"
|
|
);
|
|
}
|
|
|
|
struct TestServer {
|
|
server: Arc<Server>,
|
|
address: SocketAddr,
|
|
api: Option<thread::JoinHandle<()>>,
|
|
}
|
|
|
|
impl TestServer {
|
|
fn request(&self, method: &str, path: &str, body: &str) -> (u16, String, String) {
|
|
let mut stream = TcpStream::connect(self.address).unwrap();
|
|
stream
|
|
.set_read_timeout(Some(Duration::from_secs(10)))
|
|
.unwrap();
|
|
stream
|
|
.set_write_timeout(Some(Duration::from_secs(10)))
|
|
.unwrap();
|
|
write!(stream, "{method} {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\nContent-Type: text/plain; charset=utf-8\r\nContent-Length: {}\r\n\r\n{body}", body.len()).unwrap();
|
|
let mut response = String::new();
|
|
stream.read_to_string(&mut response).unwrap();
|
|
let (headers, body) = response.split_once("\r\n\r\n").unwrap();
|
|
let status = headers.split_whitespace().nth(1).unwrap().parse().unwrap();
|
|
(status, headers.to_owned(), body.to_owned())
|
|
}
|
|
}
|
|
|
|
impl Drop for TestServer {
|
|
fn drop(&mut self) {
|
|
self.server.unblock();
|
|
if let Some(api) = self.api.take() {
|
|
let _ = api.join();
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn native_tab_drop_route_reorders_live_pages_and_rejects_reused_frames() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
|
|
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let source = r#"class Tracked: Page {
|
|
calls: Object
|
|
function moveTab(self: Tracked, position: Number): Page { self.calls.increment() return Page.moveTab(self,position) }
|
|
}
|
|
calls=ui.state(0)
|
|
first=Tracked(calls)
|
|
first.title="first"
|
|
first.render(TextBlock("first"))
|
|
second=Page()
|
|
second.title="second"
|
|
second.render(TextBlock("second"))
|
|
first.activate()"#;
|
|
let result = client.request("PUT", "/v1/namespaces/app.main/code", source);
|
|
assert_eq!(result.0, 200, "{}", result.2);
|
|
let input = {
|
|
let current = namespace.read().unwrap();
|
|
let frame = ¤t.document.frame;
|
|
crate::events::Input {
|
|
window: 1,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: frame.tabs[0].id,
|
|
kind: "tab-drop".into(),
|
|
point: None,
|
|
press_point: None,
|
|
delta: None,
|
|
screen_position: None,
|
|
tab_drop: Some(crate::events::TabDrop {
|
|
window: 1,
|
|
layout_version: frame.layout_version,
|
|
index: 2,
|
|
}),
|
|
}
|
|
};
|
|
let body = serde_json::to_string(&input).unwrap();
|
|
let response = client.request("POST", "/v1/namespaces/app.main/events", &body);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let response = client.request("POST", "/v1/namespaces/app.main/repl", "calls.value");
|
|
let value: serde_json::Value = serde_json::from_str(&response.2).unwrap();
|
|
assert_eq!(value["value"], "1");
|
|
assert_eq!(
|
|
namespace.read().unwrap().document.frame.tabs[1].title,
|
|
"first"
|
|
);
|
|
assert_eq!(
|
|
client
|
|
.request("POST", "/v1/namespaces/app.main/events", &body)
|
|
.0,
|
|
409
|
|
);
|
|
assert!(namespace.read().unwrap().document.frame.fault.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn background_failure_preserves_its_cause_after_rejected_console_input() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
|
|
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let source = r#"class Failure {
|
|
function stop(self: Failure): Unit { ui.window().resize(0,600) }
|
|
}
|
|
failure=Failure()
|
|
page=Page()
|
|
page.render(TextBlock("preserved"))
|
|
task=ui.after(ui,"failure",0,failure.stop)"#;
|
|
let result = client.request("PUT", "/v1/namespaces/app.main/code", source);
|
|
assert_eq!(result.0, 200, "{}", result.2);
|
|
let repl = "/v1/namespaces/app.main/repl";
|
|
assert_eq!(client.request("POST", repl, "42").0, 422);
|
|
let fault = namespace
|
|
.read()
|
|
.unwrap()
|
|
.document
|
|
.frame
|
|
.fault
|
|
.clone()
|
|
.unwrap();
|
|
assert!(fault.contains("window size"), "{fault}");
|
|
assert!(fault.contains("stop"), "{fault}");
|
|
assert_eq!(client.request("POST", repl, "42").0, 422);
|
|
assert_eq!(
|
|
namespace.read().unwrap().document.frame.fault.as_ref(),
|
|
Some(&fault)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn console_keeps_memory_until_reset_or_complete_put_without_erasing_the_page() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
|
|
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let repl = "/v1/namespaces/app.main/repl";
|
|
let code = "/v1/namespaces/app.main/code";
|
|
let created = client.request("POST", repl, "title = ui.text(\"中文\")");
|
|
assert_eq!(created.0, 200, "{}", created.2);
|
|
let created: serde_json::Value = serde_json::from_str(&created.2).unwrap();
|
|
assert_eq!(created["rendered"], false);
|
|
assert_eq!(created["revision"], 0);
|
|
let read: serde_json::Value =
|
|
serde_json::from_str(&client.request("POST", repl, "title").2).unwrap();
|
|
assert_eq!(read["value"], created["value"]);
|
|
assert_eq!(
|
|
client.request("POST", repl, "ui.page().render(title)").0,
|
|
200
|
|
);
|
|
let snapshot = client.request("GET", code, "");
|
|
assert!(
|
|
snapshot
|
|
.1
|
|
.to_lowercase()
|
|
.contains("x-tessera-source-kind: repl-transcript")
|
|
);
|
|
assert_eq!(namespace.read().unwrap().revision, 1);
|
|
assert_eq!(client.request("DELETE", repl, "").0, 200);
|
|
assert_eq!(client.request("GET", code, "").2, snapshot.2);
|
|
assert_eq!(client.request("POST", repl, "title").0, 422);
|
|
assert_eq!(namespace.read().unwrap().revision, 1);
|
|
assert_eq!(client.request("POST", repl, "retained = 42").0, 200);
|
|
assert_eq!(
|
|
client
|
|
.request("PUT", code, include_str!("../../../examples/page.endor"))
|
|
.0,
|
|
200
|
|
);
|
|
assert_eq!(client.request("POST", repl, "retained").0, 422);
|
|
assert_eq!(namespace.read().unwrap().revision, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn http_executes_real_endor_preserves_failed_updates_and_supports_delete() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
|
|
let notifications = Arc::new(AtomicUsize::new(0));
|
|
let counter = notifications.clone();
|
|
let notify: Notify = Arc::new(move || {
|
|
counter.fetch_add(1, Ordering::SeqCst);
|
|
});
|
|
let api = start_server(server.clone(), namespace.clone(), notify).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let route = "/v1/namespaces/app.main/code";
|
|
let example = include_str!("../../../examples/page.endor");
|
|
let (status, _, guide) = client.request("GET", "/agent", "");
|
|
assert_eq!(status, 200);
|
|
assert!(guide.contains("let myui = ui.page()"));
|
|
let (status, _, body) = client.request("PUT", route, example);
|
|
assert_eq!(status, 200, "{body}");
|
|
assert_eq!(
|
|
serde_json::from_str::<serde_json::Value>(&body).unwrap()["revision"],
|
|
1
|
|
);
|
|
let (status, headers, source) = client.request("GET", route, "");
|
|
assert_eq!(status, 200);
|
|
assert!(headers.to_lowercase().contains("x-tessera-revision: 1"));
|
|
assert_eq!(source, example);
|
|
let (status, _, body) =
|
|
client.request("PUT", route, "function main(): View { return missing }");
|
|
assert_eq!(status, 422, "{body}");
|
|
assert_eq!(
|
|
serde_json::from_str::<serde_json::Value>(&body).unwrap()["diagnostics"][0]["line"],
|
|
1
|
|
);
|
|
assert_eq!(client.request("GET", route, "").2, example);
|
|
let second = example.replace("Hello Endor", "Second");
|
|
assert_eq!(client.request("PUT", route, &second).0, 200);
|
|
assert_eq!(namespace.read().unwrap().revision, 2);
|
|
assert_eq!(client.request("DELETE", route, "").0, 200);
|
|
assert_eq!(client.request("GET", route, "").2, "");
|
|
assert!(namespace.read().unwrap().document.frame.commands.is_empty());
|
|
assert_eq!(client.request("PUT", route, example).0, 200);
|
|
assert_eq!(namespace.read().unwrap().revision, 4);
|
|
assert_eq!(
|
|
client
|
|
.request("PUT", "/v1/namespaces/other/code", example)
|
|
.0,
|
|
404
|
|
);
|
|
assert_eq!(notifications.load(Ordering::SeqCst), 4);
|
|
}
|
|
|
|
#[test]
|
|
fn queued_candidate_keeps_live_owner_until_delete_revokes_it() {
|
|
let (runtime, document) = endor_host::ReplSession::load(
|
|
"retained = ui.state(9)\npage = Page()\npage.render(ui.textOf(retained,\"value\"))",
|
|
"app.main",
|
|
1,
|
|
277,
|
|
)
|
|
.unwrap();
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
document,
|
|
submission: 2,
|
|
runtime_session: runtime.id,
|
|
..NamespaceState::default()
|
|
}));
|
|
let mut worker = Worker {
|
|
repl: Some((1, runtime)),
|
|
next_session: 277,
|
|
};
|
|
let notify: Notify = Arc::new(|| {});
|
|
worker.refresh_viewport(&namespace, ¬ify);
|
|
assert_eq!(worker.repl.as_ref().unwrap().0, 2);
|
|
assert_eq!(
|
|
worker
|
|
.repl
|
|
.as_mut()
|
|
.unwrap()
|
|
.1
|
|
.execute("retained.value", "app.main")
|
|
.unwrap()
|
|
.value,
|
|
"9"
|
|
);
|
|
{
|
|
let mut current = namespace.write().unwrap();
|
|
current.submission = 3;
|
|
worker.preserve_live_owner(¤t);
|
|
}
|
|
assert_eq!(worker.repl.as_ref().unwrap().0, 3);
|
|
namespace.write().unwrap().clear().unwrap();
|
|
worker.refresh_viewport(&namespace, ¬ify);
|
|
assert!(worker.repl.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn http_delete_remains_available_and_invalidates_queued_put() {
|
|
use std::{
|
|
sync::{Mutex, mpsc},
|
|
time::Instant,
|
|
};
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState::default()));
|
|
let (entered_tx, entered_rx) = mpsc::channel();
|
|
let (release_tx, release_rx) = mpsc::channel();
|
|
let release_rx = Mutex::new(release_rx);
|
|
let calls = AtomicUsize::new(0);
|
|
let notify: Notify = Arc::new(move || {
|
|
if calls.fetch_add(1, Ordering::SeqCst) == 0 {
|
|
entered_tx.send(()).unwrap();
|
|
release_rx.lock().unwrap().recv().unwrap();
|
|
}
|
|
});
|
|
let api = start_server(server.clone(), namespace.clone(), notify).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
// Unblock even if a test assertion fails, before scoped threads join.
|
|
struct Release(mpsc::Sender<()>);
|
|
impl Drop for Release {
|
|
fn drop(&mut self) {
|
|
let _ = self.0.send(());
|
|
}
|
|
}
|
|
thread::scope(|scope| {
|
|
let release = Release(release_tx);
|
|
let client = &client;
|
|
let route = "/v1/namespaces/app.main/code";
|
|
let example = include_str!("../../../examples/page.endor");
|
|
let first = scope.spawn(move || client.request("PUT", route, example));
|
|
entered_rx.recv_timeout(Duration::from_secs(10)).unwrap();
|
|
let pending = scope.spawn(move || client.request("PUT", route, example));
|
|
let deadline = Instant::now() + Duration::from_secs(10);
|
|
while namespace.read().unwrap().submission < 2 {
|
|
assert!(Instant::now() < deadline, "second PUT was not admitted");
|
|
thread::sleep(Duration::from_millis(5));
|
|
}
|
|
assert_eq!(client.request("GET", route, "").2, example);
|
|
assert_eq!(client.request("DELETE", route, "").0, 200);
|
|
drop(release);
|
|
assert_eq!(first.join().unwrap().0, 200);
|
|
assert_eq!(pending.join().unwrap().0, 409);
|
|
assert_eq!(client.request("GET", route, "").2, "");
|
|
assert_eq!(namespace.read().unwrap().revision, 2);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
fn extracts_namespace_only_from_code_routes() {
|
|
assert_eq!(
|
|
namespace_from_path("/v1/namespaces/app.main/code"),
|
|
Some("app.main")
|
|
);
|
|
assert_eq!(namespace_from_path("/v1/namespaces/app.main"), None);
|
|
assert_eq!(namespace_from_path("/v1/namespaces/a/b/code"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn guide_contains_only_code_crud_routes() {
|
|
let guide = agent_guide(&NamespaceState::default());
|
|
assert!(guide.contains("Code CRUD"));
|
|
assert!(guide.contains("let myui = ui.page()"));
|
|
assert!(!guide.contains("export scene"));
|
|
assert!(guide.contains("PUT http://127.0.0.1:4737"));
|
|
assert!(!guide.contains("display_list"));
|
|
}
|
|
|
|
#[test]
|
|
fn foreground_refresh_does_not_deliver_new_callbacks_ahead_of_input() {
|
|
let source = r#"
|
|
class Action {
|
|
count: Object
|
|
function fire(self: Action): Unit { self.count.increment() }
|
|
}
|
|
count=ui.state(0)
|
|
action=Action(count)
|
|
ui.after(ui,"ready",0,action.fire)
|
|
page=Page()
|
|
page.render(TextBlock("input priority"))
|
|
"#;
|
|
let (runtime, document) = endor_host::ReplSession::load(source, "app.main", 1, 801).unwrap();
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
document,
|
|
submission: 1,
|
|
runtime_session: 801,
|
|
..NamespaceState::default()
|
|
}));
|
|
let mut worker = Worker {
|
|
repl: Some((1, runtime)),
|
|
next_session: 801,
|
|
};
|
|
let notify: Notify = Arc::new(|| {});
|
|
worker.refresh_viewport_mode(&namespace, ¬ify, true);
|
|
let value = worker
|
|
.repl
|
|
.as_mut()
|
|
.unwrap()
|
|
.1
|
|
.execute("count.value", "app.main")
|
|
.unwrap()
|
|
.value;
|
|
assert_eq!(value, "0");
|
|
worker.refresh_viewport(&namespace, ¬ify);
|
|
let runtime = &mut worker.repl.as_mut().unwrap().1;
|
|
for _ in 0..64 {
|
|
if !runtime.has_pending_refresh() {
|
|
break;
|
|
}
|
|
runtime
|
|
.resize_windows_incremental(&std::collections::BTreeMap::new(), false)
|
|
.unwrap();
|
|
}
|
|
let value = runtime.execute("count.value", "app.main").unwrap().value;
|
|
assert_eq!(value, "1");
|
|
}
|
|
|
|
#[test]
|
|
fn canvas_camera_crosses_real_http_route_and_preserves_negative_world_clicks() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
viewport: Some(crate::layout::Size {
|
|
width: 600.0,
|
|
height: 400.0,
|
|
}),
|
|
..NamespaceState::default()
|
|
}));
|
|
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let source = r#"
|
|
count=ui.state(0)
|
|
button=Button("world")
|
|
button.onClick(count.increment)
|
|
button.x=-160
|
|
button.y=-80
|
|
page=Page()
|
|
page.mode="canvas"
|
|
page.render(button)
|
|
"#;
|
|
let response = client.request("PUT", "/v1/namespaces/app.main/code", source);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let camera = |kind: &str, delta: [f32; 2], point: Option<[f32; 2]>| {
|
|
let frame = &namespace.read().unwrap().document.frame;
|
|
serde_json::to_string(&crate::events::Input {
|
|
window: 1,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: 0,
|
|
kind: kind.into(),
|
|
delta: Some(delta),
|
|
point,
|
|
press_point: None,
|
|
screen_position: None,
|
|
tab_drop: None,
|
|
})
|
|
.unwrap()
|
|
};
|
|
let event_path = "/v1/namespaces/app.main/events";
|
|
let pan = camera("page-pan", [-240.0, -160.0], None);
|
|
let response = client.request("POST", event_path, &pan);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert_eq!(
|
|
namespace.read().unwrap().document.frame.surface.pan,
|
|
crate::geometry::Point::new(-240.0, -160.0)
|
|
);
|
|
assert_eq!(client.request("POST", event_path, &pan).0, 409);
|
|
let response = client.request(
|
|
"POST",
|
|
event_path,
|
|
&camera("page-zoom", [2.0, 0.0], Some([120.0, 90.0])),
|
|
);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
{
|
|
let current = namespace.read().unwrap();
|
|
assert_eq!(current.document.frame.surface.zoom, 2.0);
|
|
assert_eq!(
|
|
current.document.frame.surface.pan,
|
|
crate::geometry::Point::new(-180.0, -115.0)
|
|
);
|
|
}
|
|
let input = {
|
|
let current = namespace.read().unwrap();
|
|
let frame = ¤t.document.frame;
|
|
let mount = frame.mounts.iter().find(|m| m.interactive).unwrap();
|
|
assert!(mount.x < 0.0 && mount.y < 0.0);
|
|
crate::events::Input {
|
|
window: 1,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: mount.id,
|
|
kind: "click".into(),
|
|
point: Some([-150.0, -70.0]),
|
|
press_point: Some([-150.0, -70.0]),
|
|
delta: None,
|
|
screen_position: None,
|
|
tab_drop: None,
|
|
}
|
|
};
|
|
let response = client.request("POST", event_path, &serde_json::to_string(&input).unwrap());
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let response = client.request("POST", "/v1/namespaces/app.main/repl", "count.value");
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert_eq!(
|
|
serde_json::from_str::<serde_json::Value>(&response.2).unwrap()["value"],
|
|
"1"
|
|
);
|
|
let response = client.request(
|
|
"POST",
|
|
event_path,
|
|
&camera("page-zoom", [0.0, 0.0], Some([120.0, 90.0])),
|
|
);
|
|
assert_eq!(response.0, 400);
|
|
assert!(namespace.read().unwrap().document.frame.fault.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_viewport_camera_crosses_http_and_rejects_bad_anchors_without_faulting() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
viewport: Some(crate::layout::Size {
|
|
width: 600.0,
|
|
height: 400.0,
|
|
}),
|
|
..NamespaceState::default()
|
|
}));
|
|
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let source = r#"
|
|
content = Button("inside")
|
|
content.x = 40
|
|
content.y = 20
|
|
scene = Scene2D(content)
|
|
view = CanvasViewport(200, 100, scene)
|
|
view.borderLeft = 10
|
|
page = Page()
|
|
page.render(Column(20, 0, view))
|
|
"#;
|
|
let response = client.request("PUT", "/v1/namespaces/app.main/code", source);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let camera = |kind: &str, delta: [f32; 2], point: Option<[f32; 2]>| {
|
|
let current = namespace.read().unwrap();
|
|
let frame = ¤t.document.frame;
|
|
serde_json::to_string(&crate::events::Input {
|
|
window: 1,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: frame
|
|
.mounts
|
|
.iter()
|
|
.find(|mount| mount.canvas_viewport)
|
|
.unwrap()
|
|
.id,
|
|
kind: kind.into(),
|
|
delta: Some(delta),
|
|
point,
|
|
press_point: None,
|
|
screen_position: None,
|
|
tab_drop: None,
|
|
})
|
|
.unwrap()
|
|
};
|
|
let path = "/v1/namespaces/app.main/events";
|
|
let pan = camera("viewport-pan", [20.0, -10.0], None);
|
|
let response = client.request("POST", path, &pan);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert_eq!(client.request("POST", path, &pan).0, 409);
|
|
let response = client.request(
|
|
"POST",
|
|
path,
|
|
&camera("viewport-zoom", [2.0, 0.0], Some([80.0, 60.0])),
|
|
);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
for (expression, expected) in [
|
|
("view.camera.x", "45"),
|
|
("view.camera.y", "10"),
|
|
("view.camera.zoom", "2"),
|
|
] {
|
|
let response = client.request("POST", "/v1/namespaces/app.main/repl", expression);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert_eq!(
|
|
serde_json::from_str::<serde_json::Value>(&response.2).unwrap()["value"],
|
|
expected
|
|
);
|
|
}
|
|
for point in [[25.0, 30.0], [220.0, 30.0], [700.0, 700.0]] {
|
|
let response = client.request(
|
|
"POST",
|
|
path,
|
|
&camera("viewport-zoom", [2.0, 0.0], Some(point)),
|
|
);
|
|
assert_eq!(response.0, 400, "{}", response.2);
|
|
assert!(namespace.read().unwrap().document.frame.fault.is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn scene3d_controllers_cross_http_and_can_be_replaced_by_endor_code() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
viewport: Some(crate::layout::Size {
|
|
width: 400.,
|
|
height: 300.,
|
|
}),
|
|
..NamespaceState::default()
|
|
}));
|
|
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let source = r##"
|
|
class PanOnly {
|
|
function update(self: PanOnly, camera: Object, gesture: CameraGesture): Unit { camera.translate(gesture.dx, gesture.dy, 0) }
|
|
}
|
|
view = Viewport3D(200,100,Scene3D(Box3D(2,2,2,"#CC4455")))
|
|
page = Page()
|
|
page.render(view)
|
|
"##;
|
|
let response = client.request("PUT", "/v1/namespaces/app.main/code", source);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let gesture = |kind: &str, delta: [f32; 2], point: Option<[f32; 2]>| {
|
|
let current = namespace.read().unwrap();
|
|
let frame = ¤t.document.frame;
|
|
serde_json::to_string(&crate::events::Input {
|
|
window: 1,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: frame
|
|
.mounts
|
|
.iter()
|
|
.find(|mount| mount.scene3d_viewport)
|
|
.unwrap()
|
|
.id,
|
|
kind: kind.into(),
|
|
delta: Some(delta),
|
|
point,
|
|
press_point: None,
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
})
|
|
.unwrap()
|
|
};
|
|
let event_path = "/v1/namespaces/app.main/events";
|
|
let zoom = gesture("scene-zoom", [2., 0.], Some([100., 50.]));
|
|
let response = client.request("POST", event_path, &zoom);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert_eq!(client.request("POST", event_path, &zoom).0, 409);
|
|
let value = |expression: &str| {
|
|
let response = client.request("POST", "/v1/namespaces/app.main/repl", expression);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
serde_json::from_str::<serde_json::Value>(&response.2).unwrap()["value"]
|
|
.as_str()
|
|
.unwrap()
|
|
.to_owned()
|
|
};
|
|
assert_eq!(value("view.camera.z"), "2.5");
|
|
let response = client.request("POST", event_path, &gesture("scene-orbit", [20., 0.], None));
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let x = value("view.camera.x").parse::<f32>().unwrap();
|
|
assert!((x - 2.5 * 0.1f32.sin()).abs() < 0.0001);
|
|
value("view.camera = OrthographicCamera()");
|
|
let response = client.request(
|
|
"POST",
|
|
event_path,
|
|
&gesture("scene-zoom", [2., 0.], Some([100., 50.])),
|
|
);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert_eq!(value("view.camera.height"), "2.5");
|
|
value("view.controller = PanOnly()");
|
|
let response = client.request("POST", event_path, &gesture("scene-pan", [7., 0.], None));
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert_eq!(value("view.camera.x"), "7");
|
|
let response = client.request(
|
|
"POST",
|
|
event_path,
|
|
&gesture("scene-zoom", [2., 0.], Some([250., 50.])),
|
|
);
|
|
assert_eq!(response.0, 400);
|
|
assert!(namespace.read().unwrap().document.frame.fault.is_none());
|
|
value("view.controller = \"none\"");
|
|
let response = client.request("POST", event_path, &gesture("scene-pan", [7., 0.], None));
|
|
assert_eq!(response.0, 409);
|
|
assert!(namespace.read().unwrap().document.frame.fault.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn canvas_in_world_ui_uses_ray_uv_coordinates_for_real_http_camera_events() {
|
|
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
|
let namespace = Arc::new(RwLock::new(NamespaceState {
|
|
viewport: Some(crate::layout::Size {
|
|
width: 400.,
|
|
height: 300.,
|
|
}),
|
|
..NamespaceState::default()
|
|
}));
|
|
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
|
let client = TestServer {
|
|
address: server.server_addr().to_ip().unwrap(),
|
|
server,
|
|
api: Some(api),
|
|
};
|
|
let source = r##"
|
|
canvas=CanvasViewport(100,100,Scene2D(Button("UI in a world")))
|
|
panel=Panel3D(2,2,canvas)
|
|
panel.uiWidth=100
|
|
panel.uiHeight=100
|
|
panel.rotationY=25
|
|
view=Viewport3D(200,200,Scene3D(panel))
|
|
page=Page()
|
|
page.render(view)
|
|
"##;
|
|
let response = client.request("PUT", "/v1/namespaces/app.main/code", source);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let gesture = |kind: &str, delta: [f32; 2], point: Option<[f32; 2]>| {
|
|
let current = namespace.read().unwrap();
|
|
let frame = ¤t.document.frame;
|
|
serde_json::to_string(&crate::events::Input {
|
|
window: 1,
|
|
session: frame.session,
|
|
layout_version: frame.layout_version,
|
|
mount: frame
|
|
.mounts
|
|
.iter()
|
|
.find(|mount| mount.canvas_viewport && mount.panel.is_some())
|
|
.unwrap()
|
|
.id,
|
|
kind: kind.into(),
|
|
point,
|
|
press_point: None,
|
|
delta: Some(delta),
|
|
tab_drop: None,
|
|
screen_position: None,
|
|
})
|
|
.unwrap()
|
|
};
|
|
let path = "/v1/namespaces/app.main/events";
|
|
assert_eq!(
|
|
client
|
|
.request("POST", path, &gesture("viewport-pan", [10., 0.], None))
|
|
.0,
|
|
400
|
|
);
|
|
let response = client.request(
|
|
"POST",
|
|
path,
|
|
&gesture("viewport-pan", [10., 0.], Some([100., 100.])),
|
|
);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
let value = |expression: &str| {
|
|
let response = client.request("POST", "/v1/namespaces/app.main/repl", expression);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
serde_json::from_str::<serde_json::Value>(&response.2).unwrap()["value"]
|
|
.as_str()
|
|
.unwrap()
|
|
.parse::<f32>()
|
|
.unwrap()
|
|
};
|
|
let pan = value("canvas.camera.x");
|
|
assert!(pan > 10.);
|
|
assert_eq!(value("view.camera.x"), 0.);
|
|
let response = client.request(
|
|
"POST",
|
|
path,
|
|
&gesture("viewport-zoom", [2., 0.], Some([100., 100.])),
|
|
);
|
|
assert_eq!(response.0, 200, "{}", response.2);
|
|
assert!((value("canvas.camera.x") - pan - 25.).abs() < 0.001);
|
|
assert_eq!(value("canvas.camera.zoom"), 2.);
|
|
assert!(namespace.read().unwrap().document.frame.fault.is_none());
|
|
}
|