diff --git a/.circleci/config.yml b/.circleci/config.yml
index 3eae40097..38f787e65 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -24,6 +24,7 @@ jobs:
paths:
- ~/.cargo
- ./target
+
ios:
macos:
xcode: "10.0.0"
@@ -32,6 +33,30 @@ jobs:
- checkout
- run: cd bindings/swift && xcodebuild test -workspace Example/StretchKit.xcworkspace -scheme StretchKit -destination 'platform=iOS Simulator,name=iPhone XR,OS=12.0'
+ js:
+ docker:
+ - image: rust:1
+
+ steps:
+ - checkout
+
+ - restore_cache:
+ key: target-cache
+
+ - run: rustc --version; cargo --version; rustup --version
+ - run: rustup component add rustfmt
+ - run: rustup component add clippy
+ - run: cargo install wasm-pack
+ - run: cargo fmt -- --check
+ - run: wasm-pack test --node
+ - run: cargo clippy
+ - run: cargo bench
+
+ - save_cache:
+ key: target-cache
+ paths:
+ - ~/.cargo
+ - ./target
workflows:
version: 2
@@ -39,3 +64,5 @@ workflows:
jobs:
- core
- ios
+ - js
+
diff --git a/.gitignore b/.gitignore
index d0ecad27b..2e87205f3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
/scripts/gentest/target
/bindings/kotlin/stretch/src/main/rust/target
/bindings/swift/StretchCore/target
+/bindings/js/target
**/*.rs.bk
Cargo.lock
diff --git a/bindings/js/Cargo.toml b/bindings/js/Cargo.toml
new file mode 100644
index 000000000..42d6a5a3a
--- /dev/null
+++ b/bindings/js/Cargo.toml
@@ -0,0 +1,38 @@
+[package]
+name = "stretch-js"
+version = "0.2.2"
+authors = ["Visly Inc. <emil@visly.app>"]
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[features]
+default = ["console_error_panic_hook"]
+
+[dependencies]
+wasm-bindgen = "0.2"
+js-sys = "0.3"
+stretch = "0.2.2"
+
+# The `console_error_panic_hook` crate provides better debugging of panics by
+# logging them with `console.error`. This is great for development, but requires
+# all the `std::fmt` and `std::panicking` infrastructure, so isn't great for
+# code size when deploying.
+console_error_panic_hook = { version = "0.1.1", optional = true }
+
+# `wee_alloc` is a tiny allocator for wasm that is only ~1K in code size
+# compared to the default allocator's ~10K. It is slower than the default
+# allocator, however.
+#
+# Unfortunately, `wee_alloc` requires nightly Rust when targeting wasm for now.
+wee_alloc = { version = "0.4.2", optional = true }
+
+[dev-dependencies]
+wasm-bindgen-test = "0.2"
+
+[profile.release]
+opt-level = "s"
+
+[workspace]
+members = []
diff --git a/bindings/js/src/lib.rs b/bindings/js/src/lib.rs
new file mode 100644
index 000000000..9fdb05c08
--- /dev/null
+++ b/bindings/js/src/lib.rs
@@ -0,0 +1,613 @@
+#![allow(non_snake_case)]
+
+mod utils;
+
+use wasm_bindgen::prelude::*;
+use js_sys::Reflect;
+use js_sys::Function;
+
+#[cfg(feature = "wee_alloc")]
+#[global_allocator]
+static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AlignItems {
+ FlexStart,
+ FlexEnd,
+ Center,
+ Baseline,
+ Stretch,
+}
+
+impl Into<stretch::style::AlignItems> for AlignItems {
+ fn into(self) -> stretch::style::AlignItems {
+ match self {
+ AlignItems::FlexStart => stretch::style::AlignItems::FlexStart,
+ AlignItems::FlexEnd => stretch::style::AlignItems::FlexEnd,
+ AlignItems::Center => stretch::style::AlignItems::Center,
+ AlignItems::Baseline => stretch::style::AlignItems::Baseline,
+ AlignItems::Stretch => stretch::style::AlignItems::Stretch,
+ }
+ }
+}
+
+impl From<i32> for AlignItems {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => AlignItems::FlexStart,
+ 1 => AlignItems::FlexEnd,
+ 2 => AlignItems::Center,
+ 3 => AlignItems::Baseline,
+ 4 => AlignItems::Stretch,
+ _ => AlignItems::Stretch,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AlignSelf {
+ Auto,
+ FlexStart,
+ FlexEnd,
+ Center,
+ Baseline,
+ Stretch,
+}
+
+impl Into<stretch::style::AlignSelf> for AlignSelf {
+ fn into(self) -> stretch::style::AlignSelf {
+ match self {
+ AlignSelf::Auto => stretch::style::AlignSelf::Auto,
+ AlignSelf::FlexStart => stretch::style::AlignSelf::FlexStart,
+ AlignSelf::FlexEnd => stretch::style::AlignSelf::FlexEnd,
+ AlignSelf::Center => stretch::style::AlignSelf::Center,
+ AlignSelf::Baseline => stretch::style::AlignSelf::Baseline,
+ AlignSelf::Stretch => stretch::style::AlignSelf::Stretch,
+ }
+ }
+}
+
+impl From<i32> for AlignSelf {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => AlignSelf::Auto,
+ 1 => AlignSelf::FlexStart,
+ 2 => AlignSelf::FlexEnd,
+ 3 => AlignSelf::Center,
+ 4 => AlignSelf::Baseline,
+ 5 => AlignSelf::Stretch,
+ _ => AlignSelf::Auto,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum AlignContent {
+ FlexStart,
+ FlexEnd,
+ Center,
+ Stretch,
+ SpaceBetween,
+ SpaceAround,
+}
+
+impl Into<stretch::style::AlignContent> for AlignContent {
+ fn into(self) -> stretch::style::AlignContent {
+ match self {
+ AlignContent::FlexStart => stretch::style::AlignContent::FlexStart,
+ AlignContent::FlexEnd => stretch::style::AlignContent::FlexEnd,
+ AlignContent::Center => stretch::style::AlignContent::Center,
+ AlignContent::Stretch => stretch::style::AlignContent::Stretch,
+ AlignContent::SpaceBetween => stretch::style::AlignContent::SpaceBetween,
+ AlignContent::SpaceAround => stretch::style::AlignContent::SpaceAround,
+ }
+ }
+}
+
+impl From<i32> for AlignContent {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => AlignContent::FlexStart,
+ 1 => AlignContent::FlexEnd,
+ 2 => AlignContent::Center,
+ 3 => AlignContent::Stretch,
+ 4 => AlignContent::SpaceBetween,
+ 5 => AlignContent::SpaceAround,
+ _ => AlignContent::Stretch,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Direction {
+ Inherit,
+ LTR,
+ RTL,
+}
+
+impl Into<stretch::style::Direction> for Direction {
+ fn into(self) -> stretch::style::Direction {
+ match self {
+ Direction::Inherit => stretch::style::Direction::Inherit,
+ Direction::LTR => stretch::style::Direction::LTR,
+ Direction::RTL => stretch::style::Direction::RTL,
+ }
+ }
+}
+
+impl From<i32> for Direction {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => Direction::Inherit,
+ 1 => Direction::LTR,
+ 2 => Direction::RTL,
+ _ => Direction::Inherit,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Display {
+ Flex,
+ None,
+}
+
+impl Into<stretch::style::Display> for Display {
+ fn into(self) -> stretch::style::Display {
+ match self {
+ Display::Flex => stretch::style::Display::Flex,
+ Display::None => stretch::style::Display::None,
+ }
+ }
+}
+
+impl From<i32> for Display {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => Display::Flex,
+ 1 => Display::None,
+ _ => Display::Flex,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum FlexDirection {
+ Row,
+ Column,
+ RowReverse,
+ ColumnReverse,
+}
+
+impl Into<stretch::style::FlexDirection> for FlexDirection {
+ fn into(self) -> stretch::style::FlexDirection {
+ match self {
+ FlexDirection::Row => stretch::style::FlexDirection::Row,
+ FlexDirection::Column => stretch::style::FlexDirection::Column,
+ FlexDirection::RowReverse => stretch::style::FlexDirection::RowReverse,
+ FlexDirection::ColumnReverse => stretch::style::FlexDirection::ColumnReverse,
+ }
+ }
+}
+
+impl From<i32> for FlexDirection {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => FlexDirection::Row,
+ 1 => FlexDirection::Column,
+ 2 => FlexDirection::RowReverse,
+ 3 => FlexDirection::ColumnReverse,
+ _ => FlexDirection::Row,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum JustifyContent {
+ FlexStart,
+ FlexEnd,
+ Center,
+ SpaceBetween,
+ SpaceAround,
+ SpaceEvenly,
+}
+
+impl Into<stretch::style::JustifyContent> for JustifyContent {
+ fn into(self) -> stretch::style::JustifyContent {
+ match self {
+ JustifyContent::FlexStart => stretch::style::JustifyContent::FlexStart,
+ JustifyContent::FlexEnd => stretch::style::JustifyContent::FlexEnd,
+ JustifyContent::Center => stretch::style::JustifyContent::Center,
+ JustifyContent::SpaceBetween => stretch::style::JustifyContent::SpaceBetween,
+ JustifyContent::SpaceAround => stretch::style::JustifyContent::SpaceAround,
+ JustifyContent::SpaceEvenly => stretch::style::JustifyContent::SpaceEvenly,
+ }
+ }
+}
+
+impl From<i32> for JustifyContent {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => JustifyContent::FlexStart,
+ 1 => JustifyContent::FlexEnd,
+ 2 => JustifyContent::Center,
+ 3 => JustifyContent::SpaceBetween,
+ 4 => JustifyContent::SpaceAround,
+ 5 => JustifyContent::SpaceEvenly,
+ _ => JustifyContent::FlexStart,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum Overflow {
+ Visible,
+ Hidden,
+ Scroll,
+}
+
+impl Into<stretch::style::Overflow> for Overflow {
+ fn into(self) -> stretch::style::Overflow {
+ match self {
+ Overflow::Visible => stretch::style::Overflow::Visible,
+ Overflow::Hidden => stretch::style::Overflow::Hidden,
+ Overflow::Scroll => stretch::style::Overflow::Scroll,
+ }
+ }
+}
+
+impl From<i32> for Overflow {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => Overflow::Visible,
+ 1 => Overflow::Hidden,
+ 2 => Overflow::Scroll,
+ _ => Overflow::Visible,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum PositionType {
+ Relative,
+ Absolute,
+}
+
+impl Into<stretch::style::PositionType> for PositionType {
+ fn into(self) -> stretch::style::PositionType {
+ match self {
+ PositionType::Relative => stretch::style::PositionType::Relative,
+ PositionType::Absolute => stretch::style::PositionType::Absolute,
+ }
+ }
+}
+
+impl From<i32> for PositionType {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => PositionType::Relative,
+ 1 => PositionType::Absolute,
+ _ => PositionType::Relative,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[repr(u8)]
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum FlexWrap {
+ NoWrap,
+ Wrap,
+ WrapReverse,
+}
+
+impl Into<stretch::style::FlexWrap> for FlexWrap {
+ fn into(self) -> stretch::style::FlexWrap {
+ match self {
+ FlexWrap::NoWrap => stretch::style::FlexWrap::NoWrap,
+ FlexWrap::Wrap => stretch::style::FlexWrap::Wrap,
+ FlexWrap::WrapReverse => stretch::style::FlexWrap::WrapReverse,
+ }
+ }
+}
+
+impl From<i32> for FlexWrap {
+ fn from(n: i32) -> Self {
+ match n {
+ 0 => FlexWrap::NoWrap,
+ 1 => FlexWrap::Wrap,
+ 2 => FlexWrap::WrapReverse,
+ _ => FlexWrap::NoWrap,
+ }
+ }
+}
+
+#[wasm_bindgen]
+#[derive(Clone, Debug)]
+pub struct Layout {
+ #[wasm_bindgen(readonly)]
+ pub width: f32,
+
+ #[wasm_bindgen(readonly)]
+ pub height: f32,
+
+ #[wasm_bindgen(readonly)]
+ pub x: f32,
+
+ #[wasm_bindgen(readonly)]
+ pub y: f32,
+
+ #[wasm_bindgen(readonly)]
+ pub childCount: usize,
+
+ children: Vec<Layout>,
+}
+
+#[wasm_bindgen]
+impl Layout {
+ fn new(layout: stretch::result::Layout) -> Layout {
+ Layout {
+ width: layout.size.width,
+ height: layout.size.height,
+ x: layout.location.x,
+ y: layout.location.y,
+ childCount: layout.children.len(),
+ children: layout.children.into_iter().map(|l| Layout::new(l)).collect(),
+ }
+ }
+
+ #[wasm_bindgen]
+ pub fn child(&self, at: usize) -> Layout {
+ self.children[at].clone()
+ }
+}
+
+#[wasm_bindgen]
+pub struct Node {
+ node: stretch::node::Node,
+ style: JsValue,
+
+ #[wasm_bindgen(readonly)]
+ pub childCount: usize,
+}
+
+#[wasm_bindgen]
+impl Node {
+
+ #[wasm_bindgen(constructor)]
+ pub fn new(style: &JsValue) -> Node {
+ Node {
+ node: stretch::node::Node::new(parse_style(&style), vec![]),
+ style: style.clone(),
+ childCount: 0,
+ }
+ }
+
+ #[wasm_bindgen(js_name = setMeasure)]
+ pub fn set_measure(&mut self, measure: &JsValue) {
+ let measure = Function::from(measure.clone());
+
+ self.node.set_measure(Some(Box::new(move |constraints| {
+ let widthConstraint = if let stretch::number::Number::Defined(val) = constraints.width {
+ val.into()
+ } else {
+ JsValue::UNDEFINED
+ };
+
+ let heightConstaint = if let stretch::number::Number::Defined(val) = constraints.height {
+ val.into()
+ } else {
+ JsValue::UNDEFINED
+ };
+
+ if let Ok(result) = measure.call2(&JsValue::UNDEFINED, &widthConstraint, &heightConstaint) {
+ let width = get_f32(&result, "width");
+ let height = get_f32(&result, "height");
+
+ if width.is_some() && height.is_some() {
+ return Ok(stretch::geometry::Size {width: width.unwrap(), height: height.unwrap()});
+ }
+ }
+
+ Err(Box::new("Failed in javascript"))
+ })));
+ }
+
+ #[wasm_bindgen(js_name = addChild)]
+ pub fn add_child(&mut self, child: &Node) {
+ self.node.add_child(&child.node);
+ self.childCount += 1;
+ }
+
+ #[wasm_bindgen(js_name = removeChild)]
+ pub fn remove_child(&mut self, child: &Node) {
+ self.node.remove_child(&child.node);
+ self.childCount -= 1;
+ }
+
+ #[wasm_bindgen(js_name = replaceChildAtIndex)]
+ pub fn replace_child_at_index(&mut self, index: usize, child: &Node) {
+ self.node.replace_child_at_index(index, &child.node);
+ }
+
+ #[wasm_bindgen(js_name = removeChildAtIndex)]
+ pub fn remove_child_at_index(&mut self, index: usize) {
+ self.node.remove_child_at_index(index);
+ self.childCount -= 1;
+ }
+
+ #[wasm_bindgen(js_name = getStyle)]
+ pub fn get_style(&self) -> JsValue {
+ self.style.clone()
+ }
+
+ #[wasm_bindgen(js_name = setStyle)]
+ pub fn set_style(&mut self, style: &JsValue) {
+ self.node.set_style(parse_style(style));
+ self.style = style.clone();
+ }
+
+ #[wasm_bindgen(js_name = markDirty)]
+ pub fn mark_dirty(&mut self) {
+ self.node.mark_dirty()
+ }
+
+ #[wasm_bindgen(js_name = isDirty)]
+ pub fn is_dirty(&self) -> bool {
+ self.node.dirty()
+ }
+
+ #[wasm_bindgen(js_name = computeLayout)]
+ pub fn compute_layout(&self, size: &JsValue) -> Layout {
+ Layout::new(
+ self.node.compute_layout(stretch::geometry::Size {
+ width: if let Some(val) = get_f32(size, "width") {
+ stretch::number::Number::Defined(val)
+ } else {
+ stretch::number::Number::Undefined
+ },
+ height: if let Some(val) = get_f32(size, "height") {
+ stretch::number::Number::Defined(val)
+ } else {
+ stretch::number::Number::Undefined
+ }
+ }).unwrap()
+ )
+ }
+}
+
+fn parse_style(style: &JsValue) -> stretch::style::Style {
+ stretch::style::Style {
+ display: get_i32(style, "display").map(|i| Display::from(i).into()).unwrap_or(Default::default()),
+ position_type: get_i32(style, "positionType").map(|i| PositionType::from(i).into()).unwrap_or(Default::default()),
+ direction: get_i32(style, "direction").map(|i| Direction::from(i).into()).unwrap_or(Default::default()),
+ flex_direction: get_i32(style, "flexDirection").map(|i| FlexDirection::from(i).into()).unwrap_or(Default::default()),
+ flex_wrap: get_i32(style, "flexWrap").map(|i| FlexWrap::from(i).into()).unwrap_or(Default::default()),
+ overflow: get_i32(style, "overflow").map(|i| Overflow::from(i).into()).unwrap_or(Default::default()),
+ align_items: get_i32(style, "alignItems").map(|i| AlignItems::from(i).into()).unwrap_or(Default::default()),
+ align_self: get_i32(style, "alignSelf").map(|i| AlignSelf::from(i).into()).unwrap_or(Default::default()),
+ align_content: get_i32(style, "alignContent").map(|i| AlignContent::from(i).into()).unwrap_or(Default::default()),
+ justify_content: get_i32(style, "justifyContent").map(|i| JustifyContent::from(i).into()).unwrap_or(Default::default()),
+
+ position: stretch::geometry::Rect {
+ start: get_dimension(style, "start"),
+ end: get_dimension(style, "end"),
+ top: get_dimension(style, "top"),
+ bottom: get_dimension(style, "bottom"),
+ },
+
+ margin: stretch::geometry::Rect {
+ start: get_dimension(style, "marginStart"),
+ end: get_dimension(style, "marginEnd"),
+ top: get_dimension(style, "marginTop"),
+ bottom: get_dimension(style, "marginBottom"),
+ },
+
+ padding: stretch::geometry::Rect {
+ start: get_dimension(style, "paddingStart"),
+ end: get_dimension(style, "paddingEnd"),
+ top: get_dimension(style, "paddingTop"),
+ bottom: get_dimension(style, "paddingBottom"),
+ },
+
+ border: stretch::geometry::Rect {
+ start: get_dimension(style, "borderStart"),
+ end: get_dimension(style, "borderEnd"),
+ top: get_dimension(style, "borderTop"),
+ bottom: get_dimension(style, "borderBottom"),
+ },
+
+ flex_grow: get_f32(style, "flexGrow").unwrap_or(0.0),
+ flex_shrink: get_f32(style, "flexShrink").unwrap_or(1.0),
+ flex_basis: get_dimension(style, "flexBasis"),
+
+ size: stretch::geometry::Size {
+ width: get_dimension(style, "width"),
+ height: get_dimension(style, "height"),
+ },
+
+ min_size: stretch::geometry::Size {
+ width: get_dimension(style, "minWidth"),
+ height: get_dimension(style, "minHeight"),
+ },
+
+ max_size: stretch::geometry::Size {
+ width: get_dimension(style, "maxWidth"),
+ height: get_dimension(style, "maxHeight"),
+ },
+
+ aspect_ratio: get_f32(style, "aspectRatio").map(|f| stretch::number::Number::Defined(f)).unwrap_or(stretch::number::Number::Undefined),
+
+ ..Default::default()
+ }
+}
+
+fn get_dimension(obj: &JsValue, key: &str) -> stretch::style::Dimension {
+ if has_key(obj, key) {
+ if let Ok(val) = Reflect::get(obj, &key.into()) {
+ if let Some(number) = val.as_f64() {
+ return stretch::style::Dimension::Points(number as f32);
+ }
+ if let Some(string) = val.as_string() {
+ if string == "auto" {
+ return stretch::style::Dimension::Auto;
+ }
+ if let Ok(number) = string.parse::<f32>() {
+ return stretch::style::Dimension::Points(number);
+ }
+ if string.chars().last().unwrap() == '%' {
+ let len = string.len();
+ if let Ok(number) = string[..len-1].parse::<f32>() {
+ return stretch::style::Dimension::Points(number);
+ }
+ }
+ }
+ }
+ }
+ stretch::style::Dimension::Undefined
+}
+
+fn get_i32(obj: &JsValue, key: &str) -> Option<i32> {
+ if has_key(obj, key) {
+ if let Ok(val) = Reflect::get(obj, &key.into()) {
+ return val.as_f64().map(|v| v as i32)
+ }
+ }
+ None
+}
+
+fn get_f32(obj: &JsValue, key: &str) -> Option<f32> {
+ if has_key(obj, key) {
+ if let Ok(val) = Reflect::get(obj, &key.into()) {
+ return val.as_f64().map(|v| v as f32)
+ }
+ }
+ None
+}
+
+fn has_key(obj: &JsValue, key: &str) -> bool {
+ if let Ok(exists) = Reflect::has(obj, &key.into()) {
+ exists
+ } else {
+ false
+ }
+}
+
diff --git a/bindings/js/src/utils.rs b/bindings/js/src/utils.rs
new file mode 100644
index 000000000..2265ed2f0
--- /dev/null
+++ b/bindings/js/src/utils.rs
@@ -0,0 +1,12 @@
+
+#[allow(dead_code)]
+pub fn set_panic_hook() {
+ // When the `console_error_panic_hook` feature is enabled, we can call the
+ // `set_panic_hook` function at least once during initialization, and then
+ // we will get better error messages if our code ever panics.
+ //
+ // For more details see
+ // https://github.com/rustwasm/console_error_panic_hook#readme
+ #[cfg(feature = "console_error_panic_hook")]
+ console_error_panic_hook::set_once();
+}
diff --git a/bindings/js/tests/web.rs b/bindings/js/tests/web.rs
new file mode 100644
index 000000000..c0344f74c
--- /dev/null
+++ b/bindings/js/tests/web.rs
@@ -0,0 +1,92 @@
+#![cfg(target_arch = "wasm32")]
+
+extern crate wasm_bindgen_test;
+use wasm_bindgen::prelude::*;
+use js_sys::Reflect;
+use js_sys::Function;
+use wasm_bindgen_test::*;
+use stretch_js::*;
+
+wasm_bindgen_test_configure!(run_in_browser);
+
+fn js_value(js: &str) -> JsValue {
+ Function::new_no_args(&format!("return {};", js)).call0(&JsValue::UNDEFINED).unwrap()
+}
+
+#[wasm_bindgen_test]
+fn set_measure() {
+ let mut node = Node::new(&js_value("{}"));
+ node.set_measure(&js_value("function (w, h) {
+ return {width: 100, height: 100};
+ }"));
+
+ let layout = node.compute_layout(&JsValue::UNDEFINED);
+ assert_eq!(layout.width, 100.0);
+ assert_eq!(layout.height, 100.0);
+}
+
+#[wasm_bindgen_test]
+fn add_child() {
+ let mut node = Node::new(&js_value("{}"));
+ let child = Node::new(&js_value("{}"));
+
+ node.add_child(&child);
+
+ assert_eq!(node.childCount, 1);
+}
+
+#[wasm_bindgen_test]
+fn remove_child() {
+ let mut node = Node::new(&js_value("{}"));
+ let child = Node::new(&js_value("{}"));
+
+ node.add_child(&child);
+ node.remove_child(&child);
+
+ assert_eq!(node.childCount, 0);
+}
+
+#[wasm_bindgen_test]
+fn remove_child_at_index() {
+ let mut node = Node::new(&js_value("{}"));
+ let child = Node::new(&js_value("{}"));
+
+ node.add_child(&child);
+ node.remove_child_at_index(0);
+
+ assert_eq!(node.childCount, 0);
+}
+
+#[wasm_bindgen_test]
+fn replace_child_at_index() {
+ let mut node = Node::new(&js_value("{}"));
+ let child1 = Node::new(&js_value("{width: 100, height: 100}"));
+ let child2 = Node::new(&js_value("{width: 200, height: 200}"));
+
+ node.add_child(&child1);
+ node.replace_child_at_index(0, &child2);
+ let layout = node.compute_layout(&JsValue::UNDEFINED);
+
+ assert_eq!(layout.width, 200.0);
+ assert_eq!(layout.height, 200.0);
+}
+
+#[wasm_bindgen_test]
+fn set_style() {
+ let mut node = Node::new(&js_value("{}"));
+ node.set_style(&js_value("{width: 200, height: 200}"));
+ let style = node.get_style();
+
+ assert_eq!(Reflect::get(&style, &"width".into()).unwrap().as_f64(), Some(200.0));
+ assert_eq!(Reflect::get(&style, &"height".into()).unwrap().as_f64(), Some(200.0));
+}
+
+#[wasm_bindgen_test]
+fn mark_dirty() {
+ let mut node = Node::new(&js_value("{}"));
+ node.compute_layout(&JsValue::UNDEFINED);
+
+ assert_eq!(node.is_dirty(), false);
+ node.mark_dirty();
+ assert_eq!(node.is_dirty(), true);
+}