Files
endorphin-tessera-gui/src/runtime/api/tests.rs
T

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, &notify);
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,
&notify,
);
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 = &current.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, &notify);
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(&current);
}
assert_eq!(worker.repl.as_ref().unwrap().0, 3);
namespace.write().unwrap().clear().unwrap();
worker.refresh_viewport(&namespace, &notify);
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, &notify, true);
let value = worker
.repl
.as_mut()
.unwrap()
.1
.execute("count.value", "app.main")
.unwrap()
.value;
assert_eq!(value, "0");
worker.refresh_viewport(&namespace, &notify);
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 = &current.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 = &current.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 = &current.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 = &current.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());
}