tawasuyu

tawasuyu/taffy

A high performance rust-powered UI layout library

HTML 231Rust 8TOML 3CSS 1317 Sutichaykuna

Implement js bindings

a33184b2d8cdcb69578f3f5789e1cbfd6f85ec7b · ruwaq Emil Sjölander · (7 wata ñawpaq) · tayta f3206bf94 · kay commitpi sach'ata qhaway

Semántico sayk'uy · 61

bindings/js/src/lib.rs

bindings/js/src/utils.rs

bindings/js/tests/web.rs

6 qillqakuna · +783 −0

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);
+}