diff --git a/.appveyor.yml b/.appveyor.yml
index 8f427ff..f50ea0e 100644
--- a/.appveyor.yml
+++ b/.appveyor.yml
@@ -3,9 +3,12 @@ environment:
     RUSTFLAGS: -C target-feature=+crt-static
 
 install:
+  - ps: Install-Product node 10
   - appveyor-retry appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
   - rustup-init.exe -y --default-host x86_64-pc-windows-msvc --default-toolchain nightly
   - set PATH=%PATH%;C:\Users\appveyor\.cargo\bin
+  - set RUST_BACKTRACE=1
+  - rustup target add wasm32-unknown-unknown --toolchain nightly
   - rustc -V
   - cargo -V
 
diff --git a/.gitignore b/.gitignore
index e156cff..4b2ee66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,5 @@
 target/
-**/target
 **/*.rs.bk
-**/pkg
-tests/fixtures/**/Cargo.lock
+tests/.crates.toml
+tests/bin
 wasm-pack.log
diff --git a/.travis.yml b/.travis.yml
index 941a276..5b143b2 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,13 @@
 language: rust
 sudo: false
 
+INSTALL_NODE_VIA_NVM: &INSTALL_NODE_VIA_NVM
+  |
+    rustup target add wasm32-unknown-unknown
+    curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.8/install.sh | bash
+    source ~/.nvm/nvm.sh
+    nvm install lts/carbon
+
 # Cache `cargo install`ed tools, but don't cache the project's `target`
 # directory (which ends up over-caching and filling all disk space!)
 cache:
@@ -32,9 +39,13 @@ matrix:
   include:
 
   # tests pass
-
   - env: JOB=test RUST_BACKTRACE=1
     rust: nightly
+    addons:
+      firefox: latest
+      chrome: stable
+    install:
+      - *INSTALL_NODE_VIA_NVM
     script:
     - cargo test --locked
     - rustup component add rustfmt-preview
diff --git a/Cargo.lock b/Cargo.lock
index 65e0e13..8aa26c7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,3 +1,8 @@
+[[package]]
+name = "adler32"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "aho-corasick"
 version = "0.6.8"
@@ -50,11 +55,34 @@ name = "bitflags"
 version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "build_const"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "byteorder"
 version = "1.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "bzip2"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bzip2-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "bzip2-sys"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cc 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "cc"
 version = "1.0.23"
@@ -131,6 +159,14 @@ dependencies = [
  "walkdir 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "crc"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "curl"
 version = "0.4.14"
@@ -174,7 +210,7 @@ name = "failure_derive"
 version = "0.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "synstructure 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -197,6 +233,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
  "miniz-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miniz_oxide_c_api 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -234,8 +271,8 @@ dependencies = [
  "backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "os_type 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)",
  "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
  "termcolor 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -332,6 +369,35 @@ dependencies = [
  "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "miniz_oxide"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "miniz_oxide_c_api"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "cc 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
+ "crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
+ "miniz_oxide 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
+[[package]]
+name = "msdos_time"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
+ "winapi 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.39"
@@ -424,9 +490,14 @@ name = "pkg-config"
 version = "0.3.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
+[[package]]
+name = "podio"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
 [[package]]
 name = "proc-macro2"
-version = "0.4.15"
+version = "0.4.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -437,7 +508,7 @@ name = "quote"
 version = "0.6.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -534,15 +605,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "serde"
-version = "1.0.75"
+version = "1.0.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 
 [[package]]
 name = "serde_derive"
-version = "1.0.75"
+version = "1.0.76"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -554,7 +625,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
  "itoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
  "ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -627,7 +698,7 @@ name = "structopt-derive"
 version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -637,7 +708,7 @@ name = "syn"
 version = "0.14.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
@@ -647,7 +718,7 @@ name = "synstructure"
 version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "proc-macro2 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "proc-macro2 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)",
  "quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)",
  "syn 0.14.9 (registry+https://github.com/rust-lang/crates.io-index)",
  "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -756,7 +827,7 @@ name = "toml"
 version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 dependencies = [
- "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -839,8 +910,8 @@ dependencies = [
  "lazy_static 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
  "openssl 0.10.11 (registry+https://github.com/rust-lang/crates.io-index)",
  "parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)",
- "serde_derive 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde_derive 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)",
  "serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)",
  "slog 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
@@ -850,6 +921,7 @@ dependencies = [
  "tempfile 3.0.3 (registry+https://github.com/rust-lang/crates.io-index)",
  "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)",
  "which 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "zip 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
 [[package]]
@@ -906,14 +978,30 @@ dependencies = [
  "libc 0.2.43 (registry+https://github.com/rust-lang/crates.io-index)",
 ]
 
+[[package]]
+name = "zip"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "flate2 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)",
+ "msdos_time 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "time 0.1.40 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
 [metadata]
+"checksum adler32 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "7e522997b529f05601e05166c07ed17789691f562762c7f3b987263d2dedee5c"
 "checksum aho-corasick 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "68f56c7353e5a9547cbd76ed90f7bb5ffc3ba09d4ea9bd1d8c06c8b1142eeb5a"
 "checksum ansi_term 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
 "checksum atty 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "9a7d5b8723950951411ee34d271d99dddcc2035a16ab25310ea2c8cfd4369652"
 "checksum backtrace 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "89a47830402e9981c5c41223151efcced65a0510c13097c769cede7efb34782a"
 "checksum backtrace-sys 0.1.24 (registry+https://github.com/rust-lang/crates.io-index)" = "c66d56ac8dabd07f6aacdaf633f4b8262f5b3601a810a0dcddffd5c22c69daa0"
 "checksum bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "228047a76f468627ca71776ecdebd732a3423081fcf5125585bcd7c49886ce12"
+"checksum build_const 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "39092a32794787acd8525ee150305ff051b0aa6cc2abaf193924f5ab05425f39"
 "checksum byteorder 1.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "90492c5858dd7d2e78691cfb89f90d273a2800fc11d98f60786e5d87e2f83781"
+"checksum bzip2 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "42b7c3cbf0fa9c1b82308d57191728ca0256cb821220f4e2fd410a72ade26e3b"
+"checksum bzip2-sys 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "2c5162604199bbb17690ede847eaa6120a3f33d5ab4dcc8e7c25b16d849ae79b"
 "checksum cc 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)" = "c37f0efaa4b9b001fa6f02d4b644dee4af97d3414df07c51e3e4f015f3a3e131"
 "checksum cfg-if 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "0c4e7bb64a8ebb0d856483e1e682ea3422f883c5f5615a90d51a2c82fe87fdd3"
 "checksum chrono 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "45912881121cb26fad7c38c17ba7daa18764771836b34fab7d3fbd93ed633878"
@@ -922,6 +1010,7 @@ dependencies = [
 "checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
 "checksum console 0.6.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7649ca90478264b9686aac8d269fcb014f14c2bed7c79a7e51b9f6afd4d783eb"
 "checksum copy_dir 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6e4281031634644843bd2f5aa9c48cf98fc48d6b083bd90bb11becf10deaf8b0"
+"checksum crc 1.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "d663548de7f5cca343f1e0a48d14dcfb0e9eb4e079ec58883b7251539fa10aeb"
 "checksum curl 0.4.14 (registry+https://github.com/rust-lang/crates.io-index)" = "444c2f9e71458b34e75471ed8d756947a0bb920b8b8b9bfc56dfcc4fc6819a13"
 "checksum curl-sys 0.4.8 (registry+https://github.com/rust-lang/crates.io-index)" = "981bd902fcd8b8b999cf71b81447e27d66c3493a7f62f1372866fd32986c0c82"
 "checksum failure 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7efb22686e4a466b1ec1a15c2898f91fa9cb340452496dca654032de20ff95b9"
@@ -944,6 +1033,9 @@ dependencies = [
 "checksum lock_api 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "949826a5ccf18c1b3a7c3d57692778d21768b79e46eb9dd07bfc4c2160036c54"
 "checksum memchr 2.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "a3b4142ab8738a78c51896f704f83c11df047ff1bda9a92a661aa6361552d93d"
 "checksum miniz-sys 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)" = "609ce024854aeb19a0ef7567d348aaa5a746b32fb72e336df7fcc16869d7e2b4"
+"checksum miniz_oxide 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "9ba430291c9d6cedae28bcd2d49d1c32fc57d60cd49086646c5dd5673a870eb5"
+"checksum miniz_oxide_c_api 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "5a5b8234d6103ebfba71e29786da4608540f862de5ce980a1c94f86a40ca0d51"
+"checksum msdos_time 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "aad9dfe950c057b1bfe9c1f2aa51583a8468ef2a5baba2ebbe06d775efeb7729"
 "checksum num-integer 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "e83d528d2677f0518c570baf2b7abdcf0cd2d248860b68507bdcb3e91d4c0cea"
 "checksum num-traits 0.2.5 (registry+https://github.com/rust-lang/crates.io-index)" = "630de1ef5cc79d0cdd78b7e33b81f083cbfe90de0f4b2b2f07f905867c70e9fe"
 "checksum openssl 0.10.11 (registry+https://github.com/rust-lang/crates.io-index)" = "6c24d3508b4fb6da175c10baac54c578b33f09c89ae90c6fe9788b3b4768efdc"
@@ -955,7 +1047,8 @@ dependencies = [
 "checksum parking_lot 0.6.4 (registry+https://github.com/rust-lang/crates.io-index)" = "f0802bff09003b291ba756dc7e79313e51cc31667e94afbe847def490424cde5"
 "checksum parking_lot_core 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "06a2b6aae052309c2fd2161ef58f5067bc17bb758377a0de9d4b279d603fdd8a"
 "checksum pkg-config 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "676e8eb2b1b4c9043511a9b7bea0915320d7e502b0a079fb03f9635a5252b18c"
-"checksum proc-macro2 0.4.15 (registry+https://github.com/rust-lang/crates.io-index)" = "295af93acfb1d5be29c16ca5b3f82d863836efd9cb0c14fd83811eb9a110e452"
+"checksum podio 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "780fb4b6698bbf9cf2444ea5d22411cef2953f0824b98f33cf454ec5615645bd"
+"checksum proc-macro2 0.4.17 (registry+https://github.com/rust-lang/crates.io-index)" = "7ee80fbbe0ae60bcad82d15bc59d5fe2aaebc1b83fbfb145abc8c74f8e769549"
 "checksum quote 0.6.8 (registry+https://github.com/rust-lang/crates.io-index)" = "dd636425967c33af890042c483632d33fa7a18f19ad1d7ea72e8998c6ef8dea5"
 "checksum rand 0.4.3 (registry+https://github.com/rust-lang/crates.io-index)" = "8356f47b32624fef5b3301c1be97e5944ecdd595409cc5da11d05f211db6cfbd"
 "checksum rand 0.5.5 (registry+https://github.com/rust-lang/crates.io-index)" = "e464cd887e869cddcae8792a4ee31d23c7edd516700695608f5b98c67ee0131c"
@@ -969,8 +1062,8 @@ dependencies = [
 "checksum ryu 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "7153dd96dade874ab973e098cb62fcdbb89a03682e46b144fd09550998d4a4a7"
 "checksum schannel 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)" = "dc1fabf2a7b6483a141426e1afd09ad543520a77ac49bd03c286e7696ccfd77f"
 "checksum scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "94258f53601af11e6a49f722422f6e3425c52b06245a5cf9bc09908b174f5e27"
-"checksum serde 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)" = "22d340507cea0b7e6632900a176101fea959c7065d93ba555072da90aaaafc87"
-"checksum serde_derive 1.0.75 (registry+https://github.com/rust-lang/crates.io-index)" = "234fc8b737737b148ccd625175fc6390f5e4dacfdaa543cb93a3430d984a9119"
+"checksum serde 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)" = "d00c69ae39089576cddfd235556e3b21bf41c2d80018063cb5ab8a1183c917fd"
+"checksum serde_derive 1.0.76 (registry+https://github.com/rust-lang/crates.io-index)" = "7d8384360683b7114fc6eeeb41dde6ee37eeba2cf3660deef3a7a0d7548e55e9"
 "checksum serde_json 1.0.26 (registry+https://github.com/rust-lang/crates.io-index)" = "44dd2cfde475037451fa99b7e5df77aa3cfd1536575fa8e7a538ab36dcde49ae"
 "checksum slog 2.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "09e4f1d0276ac7d448d98db16f0dab0220c24d4842d88ce4dad4b306fa234f1d"
 "checksum slog-async 2.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "e544d16c6b230d84c866662fe55e31aacfca6ae71e6fc49ae9a311cb379bfc2f"
@@ -1014,3 +1107,4 @@ dependencies = [
 "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 "checksum wincolor 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)" = "eeb06499a3a4d44302791052df005d5232b927ed1a9658146d842165c4de7767"
 "checksum xattr 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "244c3741f4240ef46274860397c7c74e50eb23624996930e484c16679633a54c"
+"checksum zip 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "36b9e08fb518a65cf7e08a1e482573eb87a2f4f8c6619316612a3c1f162fe822"
diff --git a/Cargo.toml b/Cargo.toml
index 77eac9a..ac1d24f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -28,6 +28,7 @@ structopt = "0.2"
 tar = "0.4.16"
 toml = "0.4"
 which = "2.0.0"
+zip = "0.4.2"
 
 [dev-dependencies]
 copy_dir = "0.1.2"
diff --git a/src/binaries.rs b/src/binaries.rs
new file mode 100644
index 0000000..a414405
--- /dev/null
+++ b/src/binaries.rs
@@ -0,0 +1,243 @@
+//! Utilities for finding and installing binaries that we depend on.
+
+use curl;
+use error::Error;
+use failure;
+use flate2;
+use slog::Logger;
+use std::collections::HashSet;
+use std::ffi;
+use std::fs;
+use std::io;
+use std::path::{Path, PathBuf};
+use tar;
+use target;
+use which::which;
+use zip;
+
+/// Get the path for a crate's directory of locally-installed binaries.
+///
+/// This does not check whether or ensure that the directory exists.
+pub fn local_bin_dir(crate_path: &Path) -> PathBuf {
+    crate_path.join("bin")
+}
+
+/// Ensure that the crate's directory for locally-installed binaries exists.
+pub fn ensure_local_bin_dir(crate_path: &Path) -> io::Result<()> {
+    fs::create_dir_all(local_bin_dir(crate_path))
+}
+
+/// Get the path for where `bin` would be if we have a crate-local install for
+/// it.
+///
+/// This does *not* check whether there is a file at that path or not.
+///
+/// This will automatically add the `.exe` extension for windows.
+pub fn local_bin_path(crate_path: &Path, bin: &str) -> PathBuf {
+    let mut p = local_bin_dir(crate_path).join(bin);
+    if target::WINDOWS {
+        p.set_extension("exe");
+    }
+    p
+}
+
+/// Get the local (at `$CRATE/bin/$BIN`; preferred) or global (on `$PATH`) path
+/// for the given binary.
+///
+/// If this function returns `Some(path)`, then a file at that path exists (or
+/// at least existed when we checked! In general, we aren't really worried about
+/// racing with an uninstall of a tool that we rely on.)
+pub fn bin_path(log: &Logger, crate_path: &Path, bin: &str) -> Option<PathBuf> {
+    assert!(!bin.ends_with(".exe"));
+    debug!(log, "Searching for {} binary...", bin);
+
+    // Return the path to the local binary, if it exists.
+    let local_path = |crate_path: &Path| -> Option<PathBuf> {
+        let p = local_bin_path(crate_path, bin);
+        debug!(log, "Checking for local {} binary at {}", bin, p.display());
+        if p.is_file() {
+            Some(p)
+        } else {
+            None
+        }
+    };
+
+    // Return the path to the global binary, if it exists.
+    let global_path = || -> Option<PathBuf> {
+        debug!(log, "Looking for global {} binary on $PATH", bin);
+        if let Ok(p) = which(bin) {
+            Some(p)
+        } else {
+            None
+        }
+    };
+
+    local_path(crate_path)
+        .or_else(global_path)
+        .map(|p| {
+            let p = p.canonicalize().unwrap_or(p);
+            debug!(log, "Using {} binary at {}", bin, p.display());
+            p
+        }).or_else(|| {
+            debug!(log, "Could not find {} binary.", bin);
+            None
+        })
+}
+
+fn with_url_context<T, E>(url: &str, r: Result<T, E>) -> Result<T, impl failure::Fail>
+where
+    Result<T, E>: failure::ResultExt<T, E>,
+{
+    use failure::ResultExt;
+    r.with_context(|_| format!("when requesting {}", url))
+}
+
+fn transfer(
+    url: &str,
+    easy: &mut curl::easy::Easy,
+    data: &mut Vec<u8>,
+) -> Result<(), failure::Error> {
+    let mut transfer = easy.transfer();
+    with_url_context(
+        url,
+        transfer.write_function(|part| {
+            data.extend_from_slice(part);
+            Ok(part.len())
+        }),
+    )?;
+    with_url_context(url, transfer.perform())?;
+    Ok(())
+}
+
+fn curl(url: &str) -> Result<Vec<u8>, failure::Error> {
+    let mut data = Vec::new();
+
+    let mut easy = curl::easy::Easy::new();
+    with_url_context(url, easy.follow_location(true))?;
+    with_url_context(url, easy.url(url))?;
+    transfer(url, &mut easy, &mut data)?;
+
+    let status_code = with_url_context(url, easy.response_code())?;
+    if 200 <= status_code && status_code < 300 {
+        Ok(data)
+    } else {
+        Err(Error::http(&format!(
+            "received a bad HTTP status code ({}) when requesting {}",
+            status_code, url
+        )).into())
+    }
+}
+
+/// Download the `.tar.gz` file at the given URL and unpack the given binaries
+/// from it into the given crate.
+///
+/// Upon success, every `$BIN` in `binaries` will be at `$CRATE/bin/$BIN`.
+pub fn install_binaries_from_targz_at_url<'a, I>(
+    crate_path: &Path,
+    url: &str,
+    binaries: I,
+) -> Result<(), Error>
+where
+    I: IntoIterator<Item = &'a str>,
+{
+    let mut binaries: HashSet<_> = binaries.into_iter().map(ffi::OsStr::new).collect();
+
+    let tarball = curl(&url).map_err(|e| Error::http(&e.to_string()))?;
+    let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(&tarball[..]));
+
+    ensure_local_bin_dir(crate_path)?;
+    let bin = local_bin_dir(crate_path);
+
+    for entry in archive.entries()? {
+        let mut entry = entry?;
+
+        let dest = match entry.path()?.file_stem() {
+            Some(f) if binaries.contains(f) => {
+                binaries.remove(f);
+                bin.join(entry.path()?.file_name().unwrap())
+            }
+            _ => continue,
+        };
+
+        entry.unpack(dest)?;
+    }
+
+    if binaries.is_empty() {
+        Ok(())
+    } else {
+        Err(Error::archive(&format!(
+            "the tarball at {} was missing expected executables: {}",
+            url,
+            binaries
+                .into_iter()
+                .map(|s| s.to_string_lossy())
+                .collect::<Vec<_>>()
+                .join(", "),
+        )))
+    }
+}
+
+/// Install binaries from within the given zip at the given URL.
+///
+/// Upon success, the binaries will be at the `$CRATE/bin/$BIN` path.
+pub fn install_binaries_from_zip_at_url<'a, I>(
+    crate_path: &Path,
+    url: &str,
+    binaries: I,
+) -> Result<(), Error>
+where
+    I: IntoIterator<Item = &'a str>,
+{
+    let mut binaries: HashSet<_> = binaries.into_iter().map(ffi::OsStr::new).collect();
+
+    let data = curl(&url).map_err(|e| Error::http(&e.to_string()))?;
+    let data = io::Cursor::new(data);
+    let mut zip = zip::ZipArchive::new(data)?;
+
+    ensure_local_bin_dir(crate_path)?;
+    let bin = local_bin_dir(crate_path);
+
+    for i in 0..zip.len() {
+        let mut entry = zip.by_index(i).unwrap();
+        let entry_path = entry.sanitized_name();
+        match entry_path.file_stem() {
+            Some(f) if binaries.contains(f) => {
+                binaries.remove(f);
+                let mut dest = bin_open_options()
+                    .write(true)
+                    .create_new(true)
+                    .open(bin.join(entry_path.file_name().unwrap()))?;
+                io::copy(&mut entry, &mut dest)?;
+            }
+            _ => continue,
+        };
+    }
+
+    if binaries.is_empty() {
+        Ok(())
+    } else {
+        Err(Error::archive(&format!(
+            "the zip at {} was missing expected executables: {}",
+            url,
+            binaries
+                .into_iter()
+                .map(|s| s.to_string_lossy())
+                .collect::<Vec<_>>()
+                .join(", "),
+        )))
+    }
+}
+
+#[cfg(unix)]
+fn bin_open_options() -> fs::OpenOptions {
+    use std::os::unix::fs::OpenOptionsExt;
+
+    let mut opts = fs::OpenOptions::new();
+    opts.mode(0o755);
+    opts
+}
+
+#[cfg(not(unix))]
+fn bin_open_options() -> fs::OpenOptions {
+    fs::OpenOptions::new()
+}
diff --git a/src/bindgen.rs b/src/bindgen.rs
index 93c70f0..2d4a2bb 100644
--- a/src/bindgen.rs
+++ b/src/bindgen.rs
@@ -1,18 +1,13 @@
 //! Functionality related to installing and running `wasm-bindgen`.
 
-use curl;
+use binaries::{self, bin_path, install_binaries_from_targz_at_url};
 use emoji;
 use error::Error;
-use failure;
-use flate2;
 use progressbar::Step;
 use slog::Logger;
-use std::ffi;
-use std::fs;
 use std::path::{Path, PathBuf};
 use std::process::Command;
-use tar;
-use which::which;
+use target;
 use PBAR;
 
 /// Install the `wasm-bindgen` CLI.
@@ -29,8 +24,8 @@ pub fn install_wasm_bindgen(
     log: &Logger,
 ) -> Result<(), Error> {
     // If the `wasm-bindgen` dependency is already met, print a message and return.
-    if wasm_bindgen_path(root_path)
-        .map(|bindgen_path| wasm_bindgen_version_check(&bindgen_path, version))
+    if wasm_bindgen_path(log, root_path)
+        .map(|bindgen_path| wasm_bindgen_version_check(&bindgen_path, version, log))
         .unwrap_or(false)
     {
         let msg = format!("{}wasm-bindgen already installed...", emoji::DOWN_ARROW);
@@ -57,56 +52,13 @@ pub fn install_wasm_bindgen(
     })
 }
 
-fn curl(url: &str) -> Result<Vec<u8>, failure::Error> {
-    let mut data = Vec::new();
-
-    fn with_url_context<T, E>(url: &str, r: Result<T, E>) -> Result<T, impl failure::Fail>
-    where
-        Result<T, E>: failure::ResultExt<T, E>,
-    {
-        use failure::ResultExt;
-        r.with_context(|_| format!("when requesting {}", url))
-    }
-
-    let mut easy = curl::easy::Easy::new();
-    with_url_context(url, easy.follow_location(true))?;
-    with_url_context(url, easy.url(url))?;
-
-    {
-        let mut transfer = easy.transfer();
-        with_url_context(
-            url,
-            transfer.write_function(|part| {
-                data.extend_from_slice(part);
-                Ok(part.len())
-            }),
-        )?;
-        with_url_context(url, transfer.perform())?;
-    }
-
-    let code = with_url_context(url, easy.response_code())?;
-    if 200 <= code && code < 300 {
-        Ok(data)
-    } else {
-        Err(Error::http(&format!(
-            "received a bad HTTP status code ({}) when requesting {}",
-            code, url
-        )).into())
-    }
-}
-
 /// Download a tarball containing a pre-built `wasm-bindgen` binary.
 pub fn download_prebuilt_wasm_bindgen(root_path: &Path, version: &str) -> Result<(), Error> {
-    let linux = cfg!(target_os = "linux");
-    let macos = cfg!(target_os = "macos");
-    let windows = cfg!(windows);
-    let x86_64 = cfg!(target_arch = "x86_64");
-
-    let target = if linux && x86_64 {
+    let target = if target::LINUX && target::x86_64 {
         "x86_64-unknown-linux-musl"
-    } else if macos && x86_64 {
+    } else if target::MACOS && target::x86_64 {
         "x86_64-apple-darwin"
-    } else if windows && x86_64 {
+    } else if target::WINDOWS && target::x86_64 {
         "x86_64-pc-windows-msvc"
     } else {
         return Err(Error::unsupported(
@@ -120,45 +72,16 @@ pub fn download_prebuilt_wasm_bindgen(root_path: &Path, version: &str) -> Result
         target
     );
 
-    let tarball = curl(&url).map_err(|e| Error::http(&e.to_string()))?;
-    let mut archive = tar::Archive::new(flate2::read::GzDecoder::new(&tarball[..]));
-
-    let bin = root_path.join("bin");
-    fs::create_dir_all(&bin)?;
-
-    let mut found_wasm_bindgen = false;
-    let mut found_test_runner = false;
-
-    for entry in archive.entries()? {
-        let mut entry = entry?;
-
-        let dest = match entry.path()?.file_stem() {
-            Some(f) if f == ffi::OsStr::new("wasm-bindgen") => {
-                found_wasm_bindgen = true;
-                bin.join(entry.path()?.file_name().unwrap())
-            }
-            Some(f) if f == ffi::OsStr::new("wasm-bindgen-test-runner") => {
-                found_test_runner = true;
-                bin.join(entry.path()?.file_name().unwrap())
-            }
-            _ => continue,
-        };
-
-        entry.unpack(dest)?;
-    }
-
-    if found_wasm_bindgen && found_test_runner {
-        Ok(())
-    } else {
-        Err(Error::archive(
-            "the wasm-bindgen tarball was missing expected executables",
-        ))
-    }
+    install_binaries_from_targz_at_url(
+        root_path,
+        &url,
+        vec!["wasm-bindgen", "wasm-bindgen-test-runner"],
+    )
 }
 
-/// Use `cargo install` to install the `wasm-bindgen` CLI to the given root
-/// path.
-pub fn cargo_install_wasm_bindgen(root_path: &Path, version: &str) -> Result<(), Error> {
+/// Use `cargo install` to install the `wasm-bindgen` CLI locally into the given
+/// crate.
+pub fn cargo_install_wasm_bindgen(crate_path: &Path, version: &str) -> Result<(), Error> {
     let output = Command::new("cargo")
         .arg("install")
         .arg("--force")
@@ -166,7 +89,7 @@ pub fn cargo_install_wasm_bindgen(root_path: &Path, version: &str) -> Result<(),
         .arg("--version")
         .arg(version)
         .arg("--root")
-        .arg(root_path)
+        .arg(crate_path)
         .output()?;
     if !output.status.success() {
         let message = "Installing wasm-bindgen failed".to_string();
@@ -176,11 +99,7 @@ pub fn cargo_install_wasm_bindgen(root_path: &Path, version: &str) -> Result<(),
             stderr: s.to_string(),
         })
     } else {
-        if cfg!(target_os = "windows") {
-            assert!(root_path.join("bin").join("wasm-bindgen.exe").is_file());
-        } else {
-            assert!(root_path.join("bin").join("wasm-bindgen").is_file());
-        }
+        assert!(binaries::local_bin_path(crate_path, "wasm-bindgen").is_file());
         Ok(())
     }
 }
@@ -195,6 +114,7 @@ pub fn wasm_bindgen_build(
     target: &str,
     debug: bool,
     step: &Step,
+    log: &Logger,
 ) -> Result<(), Error> {
     let msg = format!("{}Running WASM-bindgen...", emoji::RUNNER);
     PBAR.step(step, &msg);
@@ -204,7 +124,7 @@ pub fn wasm_bindgen_build(
 
     let out_dir = out_dir.to_str().unwrap();
 
-    if let Some(wasm_bindgen_path) = wasm_bindgen_path(path) {
+    if let Some(wasm_bindgen_path) = wasm_bindgen_path(log, path) {
         let wasm_path = format!(
             "target/wasm32-unknown-unknown/{}/{}.wasm",
             release_or_debug, binary_name
@@ -239,7 +159,7 @@ pub fn wasm_bindgen_build(
 }
 
 /// Check if the `wasm-bindgen` dependency is locally satisfied.
-fn wasm_bindgen_version_check(bindgen_path: &PathBuf, dep_version: &str) -> bool {
+fn wasm_bindgen_version_check(bindgen_path: &PathBuf, dep_version: &str, log: &Logger) -> bool {
     Command::new(bindgen_path)
         .arg("--version")
         .output()
@@ -250,38 +170,27 @@ fn wasm_bindgen_version_check(bindgen_path: &PathBuf, dep_version: &str) -> bool
                 .trim()
                 .split_whitespace()
                 .nth(1)
-                .map(|v| v == dep_version)
-                .unwrap_or(false)
+                .map(|v| {
+                    info!(
+                        log,
+                        "Checking installed `wasm-bindgen` version == expected version: {} == {}",
+                        v,
+                        dep_version
+                    );
+                    v == dep_version
+                }).unwrap_or(false)
         }).unwrap_or(false)
 }
 
 /// Return a `PathBuf` containing the path to either the local wasm-bindgen
 /// version, or the globally installed version if there is no local version.
-fn wasm_bindgen_path(crate_path: &Path) -> Option<PathBuf> {
-    // Return the path to the local `wasm-bindgen`, if it exists.
-    let local_bindgen_path = |crate_path: &Path| -> Option<PathBuf> {
-        let mut p = crate_path.to_path_buf();
-        p.push("bin");
-        if cfg!(target_os = "windows") {
-            p.push("wasm-bindgen.exe");
-        } else {
-            p.push("wasm-bindgen");
-        }
-        if p.is_file() {
-            Some(p)
-        } else {
-            None
-        }
-    };
-
-    // Return the path to the global `wasm-bindgen`, if it exists.
-    let global_bindgen_path = || -> Option<PathBuf> {
-        if let Ok(p) = which("wasm-bindgen") {
-            Some(p)
-        } else {
-            None
-        }
-    };
+fn wasm_bindgen_path(log: &Logger, crate_path: &Path) -> Option<PathBuf> {
+    bin_path(log, crate_path, "wasm-bindgen")
+}
 
-    local_bindgen_path(crate_path).or_else(global_bindgen_path)
+/// Return a `PathBuf` containing the path to either the local
+/// wasm-bindgen-test-runner version, or the globally installed version if there
+/// is no local version.
+pub fn wasm_bindgen_test_runner_path(log: &Logger, crate_path: &Path) -> Option<PathBuf> {
+    bin_path(log, crate_path, "wasm-bindgen-test-runner")
 }
diff --git a/src/build.rs b/src/build.rs
index c568d91..6200c59 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -67,3 +67,27 @@ pub fn cargo_build_wasm(path: &Path, debug: bool, step: &Step) -> Result<(), Err
         Ok(())
     }
 }
+
+/// Run `cargo build --tests` with the `nightly` toolchain and targetting
+/// `wasm32-unknown-unknown`.
+pub fn cargo_build_wasm_tests(path: &Path, debug: bool) -> Result<(), Error> {
+    let output = {
+        let mut cmd = Command::new("cargo");
+        cmd.current_dir(path)
+            .arg("+nightly")
+            .arg("build")
+            .arg("--tests");
+        if !debug {
+            cmd.arg("--release");
+        }
+        cmd.arg("--target").arg("wasm32-unknown-unknown");
+        cmd.output()?
+    };
+
+    if !output.status.success() {
+        let s = String::from_utf8_lossy(&output.stderr);
+        Error::cli("Compilation of your program failed", s)
+    } else {
+        Ok(())
+    }
+}
diff --git a/src/command/build.rs b/src/command/build.rs
index f382919..461abb8 100644
--- a/src/command/build.rs
+++ b/src/command/build.rs
@@ -1,3 +1,5 @@
+//! Implementation of the `wasm-pack build` command.
+
 use bindgen;
 use build;
 use command::utils::{create_pkg_dir, set_crate_path};
@@ -9,6 +11,7 @@ use progressbar::Step;
 use readme;
 use slog::Logger;
 use std::path::PathBuf;
+use std::str::FromStr;
 use std::time::Instant;
 use PBAR;
 
@@ -27,6 +30,7 @@ pub(crate) struct Build {
 
 /// The `BuildMode` determines which mode of initialization we are running, and
 /// what build and install steps we perform.
+#[derive(Clone, Copy, Debug)]
 pub enum BuildMode {
     /// Perform all the build and install steps.
     Normal,
@@ -35,6 +39,23 @@ pub enum BuildMode {
     Noinstall,
 }
 
+impl Default for BuildMode {
+    fn default() -> BuildMode {
+        BuildMode::Normal
+    }
+}
+
+impl FromStr for BuildMode {
+    type Err = Error;
+    fn from_str(s: &str) -> Result<Self, Error> {
+        match s {
+            "no-install" => Ok(BuildMode::Noinstall),
+            "normal" => Ok(BuildMode::Normal),
+            _ => Error::crate_config(&format!("Unknown build mode: {}", s)).map(|_| unreachable!()),
+        }
+    }
+}
+
 /// Everything required to configure and run the `wasm-pack build` command.
 #[derive(Debug, StructOpt)]
 pub struct BuildOptions {
@@ -48,7 +69,7 @@ pub struct BuildOptions {
 
     #[structopt(long = "mode", short = "m", default_value = "normal")]
     /// Sets steps to be run. [possible values: no-install, normal]
-    pub mode: String,
+    pub mode: BuildMode,
 
     #[structopt(long = "no-typescript")]
     /// By default a *.d.ts file is generated for the generated JS file, but
@@ -74,13 +95,9 @@ type BuildStep = fn(&mut Build, &Step, &Logger) -> Result<(), Error>;
 impl Build {
     /// Construct a build command from the given options.
     pub fn try_from_opts(build_opts: BuildOptions) -> Result<Self, Error> {
-        let crate_path = set_crate_path(build_opts.path);
+        let crate_path = set_crate_path(build_opts.path)?;
         let crate_name = manifest::get_crate_name(&crate_path)?;
         let out_dir = crate_path.join(PathBuf::from(build_opts.out_dir));
-        let mode = match build_opts.mode.as_str() {
-            "no-install" => BuildMode::Noinstall,
-            _ => BuildMode::Normal,
-        };
         // let build_config = manifest::xxx(&crate_path).xxx();
         Ok(Build {
             crate_path,
@@ -88,7 +105,7 @@ impl Build {
             disable_dts: build_opts.disable_dts,
             target: build_opts.target,
             debug: build_opts.debug,
-            mode,
+            mode: build_opts.mode,
             // build_config,
             crate_name,
             out_dir,
@@ -258,6 +275,7 @@ impl Build {
             &self.target,
             self.debug,
             step,
+            log,
         )?;
         info!(&log, "wasm bindings were built at {:#?}.", &self.out_dir);
         Ok(())
diff --git a/src/command/mod.rs b/src/command/mod.rs
index 135e009..a47df3f 100644
--- a/src/command/mod.rs
+++ b/src/command/mod.rs
@@ -1,15 +1,17 @@
 //! CLI command structures, parsing, and execution.
 
-mod build;
+pub mod build;
 mod login;
 mod pack;
 mod publish;
+pub mod test;
 pub mod utils;
 
 use self::build::{Build, BuildOptions};
 use self::login::login;
 use self::pack::pack;
 use self::publish::publish;
+use self::test::{Test, TestOptions};
 use error::Error;
 use slog::Logger;
 use std::path::PathBuf;
@@ -70,6 +72,10 @@ pub enum Command {
         /// strategies besides classic username/password entry in legacy npm.
         auth_type: Option<String>,
     },
+
+    #[structopt(name = "test")]
+    /// 👩‍🔬  test your wasm!
+    Test(TestOptions),
 }
 
 /// Run a command with the given logger!
@@ -108,6 +114,10 @@ pub fn run_wasm_pack(command: Command, log: &Logger) -> result::Result<(), Error
             );
             login(registry, scope, always_auth, auth_type, &log)
         }
+        Command::Test(test_opts) => {
+            info!(&log, "Running test command...");
+            Test::try_from_opts(test_opts).and_then(|t| t.run(&log))
+        }
     };
 
     match status {
diff --git a/src/command/pack.rs b/src/command/pack.rs
index fe1ca5f..5390b57 100644
--- a/src/command/pack.rs
+++ b/src/command/pack.rs
@@ -9,7 +9,7 @@ use PBAR;
 /// Executes the 'npm pack' command on the 'pkg' directory
 /// which creates a tarball that can be published to the NPM registry
 pub fn pack(path: Option<PathBuf>, log: &Logger) -> result::Result<(), Error> {
-    let crate_path = set_crate_path(path);
+    let crate_path = set_crate_path(path)?;
 
     info!(&log, "Packing up the npm package...");
     let pkg_directory = find_pkg_directory(&crate_path).ok_or(Error::PkgNotFound {
diff --git a/src/command/publish.rs b/src/command/publish.rs
index 2d67365..1c9f335 100644
--- a/src/command/publish.rs
+++ b/src/command/publish.rs
@@ -9,7 +9,7 @@ use PBAR;
 /// Creates a tarball from a 'pkg' directory
 /// and publishes it to the NPM registry
 pub fn publish(path: Option<PathBuf>, log: &Logger) -> result::Result<(), Error> {
-    let crate_path = set_crate_path(path);
+    let crate_path = set_crate_path(path)?;
 
     info!(&log, "Publishing the npm package...");
     info!(&log, "npm info located in the npm debug log");
diff --git a/src/command/test.rs b/src/command/test.rs
new file mode 100644
index 0000000..5bba1b9
--- /dev/null
+++ b/src/command/test.rs
@@ -0,0 +1,402 @@
+//! Implementation of the `wasm-pack test` command.
+
+use super::build::BuildMode;
+use bindgen;
+use build;
+use command::utils::set_crate_path;
+use emoji;
+use error::Error;
+use indicatif::HumanDuration;
+use manifest;
+use progressbar::Step;
+use slog::Logger;
+use std::path::PathBuf;
+use std::time::Instant;
+use test::{self, webdriver};
+use PBAR;
+
+#[derive(Debug, Default, StructOpt)]
+/// Everything required to configure the `wasm-pack test` command.
+pub struct TestOptions {
+    #[structopt(parse(from_os_str))]
+    /// The path to the Rust crate.
+    pub path: Option<PathBuf>,
+
+    #[structopt(long = "node")]
+    /// Run the tests in Node.js.
+    pub node: bool,
+
+    #[structopt(long = "firefox")]
+    /// Run the tests in Firefox. This machine must have a Firefox installation.
+    /// If the `geckodriver` WebDriver client is not on the `$PATH`, and not
+    /// specified with `--geckodriver`, then `wasm-pack` will download a local
+    /// copy.
+    pub firefox: bool,
+
+    #[structopt(long = "geckodriver", parse(from_os_str))]
+    /// The path to the `geckodriver` WebDriver client for testing in
+    /// Firefox. Implies `--firefox`.
+    pub geckodriver: Option<PathBuf>,
+
+    #[structopt(long = "chrome")]
+    /// Run the tests in Chrome. This machine must have a Chrome installation.
+    /// If the `chromedriver` WebDriver client is not on the `$PATH`, and not
+    /// specified with `--chromedriver`, then `wasm-pack` will download a local
+    /// copy.
+    pub chrome: bool,
+
+    #[structopt(long = "chromedriver", parse(from_os_str))]
+    /// The path to the `chromedriver` WebDriver client for testing in
+    /// Chrome. Implies `--chrome`.
+    pub chromedriver: Option<PathBuf>,
+
+    #[structopt(long = "safari")]
+    /// Run the tests in Safari. This machine must have a Safari installation,
+    /// and the `safaridriver` WebDriver client must either be on the `$PATH` or
+    /// specified explicitly with the `--safaridriver` flag. `wasm-pack` cannot
+    /// download the `safaridriver` WebDriver client for you.
+    pub safari: bool,
+
+    #[structopt(long = "safaridriver", parse(from_os_str))]
+    /// The path to the `safaridriver` WebDriver client for testing in
+    /// Safari. Implies `--safari`.
+    pub safaridriver: Option<PathBuf>,
+
+    #[structopt(long = "headless")]
+    /// When running browser tests, run the browser in headless mode without any
+    /// UI or windows.
+    pub headless: bool,
+
+    #[structopt(long = "mode", short = "m", default_value = "normal")]
+    /// Sets steps to be run. [possible values: no-install, normal]
+    pub mode: BuildMode,
+
+    #[structopt(long = "release", short = "r")]
+    /// Build with the release profile.
+    pub release: bool,
+}
+
+/// A configured `wasm-pack test` command.
+pub struct Test {
+    crate_path: PathBuf,
+    node: bool,
+    mode: BuildMode,
+    firefox: bool,
+    geckodriver: Option<PathBuf>,
+    chrome: bool,
+    chromedriver: Option<PathBuf>,
+    safari: bool,
+    safaridriver: Option<PathBuf>,
+    headless: bool,
+    release: bool,
+    test_runner_path: Option<PathBuf>,
+}
+
+type TestStep = fn(&mut Test, &Step, &Logger) -> Result<(), Error>;
+
+impl Test {
+    /// Construct a test command from the given options.
+    pub fn try_from_opts(test_opts: TestOptions) -> Result<Self, Error> {
+        let TestOptions {
+            path,
+            node,
+            mode,
+            headless,
+            release,
+            chrome,
+            chromedriver,
+            firefox,
+            geckodriver,
+            safari,
+            safaridriver,
+        } = test_opts;
+
+        let crate_path = set_crate_path(path)?;
+
+        // let geckodriver = get_web_driver("geckodriver", test_opts.geckodriver, test_opts.firefox)?;
+        // let chromedriver =
+        //     get_web_driver("chromedriver", test_opts.chromedriver, test_opts.chrome)?;
+        // let safaridriver =
+        //     get_web_driver("safaridriver", test_opts.safaridriver, test_opts.safari)?;
+
+        let any_browser = chrome || firefox || safari;
+
+        if !node && !any_browser {
+            return Error::crate_config(
+                "Must specify at least one of `--node`, `--chrome`, `--firefox`, or `--safari`",
+            ).map(|_| unreachable!());
+        }
+
+        if headless && !any_browser {
+            return Error::crate_config(
+                "The `--headless` flag only applies to browser tests. Node does not provide a UI, \
+                 so it doesn't make sense to talk about a headless version of Node tests.",
+            ).map(|_| unreachable!());
+        }
+
+        Ok(Test {
+            crate_path,
+            node,
+            mode,
+            chrome,
+            chromedriver,
+            firefox,
+            geckodriver,
+            safari,
+            safaridriver,
+            headless,
+            release,
+            test_runner_path: None,
+        })
+    }
+
+    /// Execute this test command.
+    pub fn run(mut self, log: &Logger) -> Result<(), Error> {
+        let process_steps = self.get_process_steps();
+        let mut step_counter = Step::new(process_steps.len());
+
+        let started = Instant::now();
+        for (_, process_step) in process_steps {
+            process_step(&mut self, &step_counter, log)?;
+            step_counter.inc();
+        }
+        let duration = HumanDuration(started.elapsed());
+        info!(&log, "Done in {}.", &duration);
+
+        Ok(())
+    }
+
+    fn get_process_steps(&self) -> Vec<(&'static str, TestStep)> {
+        macro_rules! steps {
+            ($($name:ident $(if $e:expr)* ),+) => {
+                {
+                    let mut steps: Vec<(&'static str, TestStep)> = Vec::new();
+                    $(
+                        $(if $e)* {
+                            steps.push((stringify!($name), Test::$name));
+                        }
+                    )*
+                    steps
+                }
+            };
+            ($($name:ident $(if $e:expr)* ,)*) => (steps![$($name $(if $e)* ),*])
+        }
+        match self.mode {
+            BuildMode::Normal => steps![
+                step_check_crate_config,
+                step_add_wasm_target,
+                step_build_tests,
+                step_install_wasm_bindgen,
+                step_test_node if self.node,
+                step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
+                step_test_chrome if self.chrome,
+                step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
+                step_test_firefox if self.firefox,
+                step_get_safaridriver if self.safari && self.safaridriver.is_none(),
+                step_test_safari if self.safari,
+            ],
+            BuildMode::Noinstall => steps![
+                step_check_crate_config,
+                step_build_tests,
+                step_install_wasm_bindgen,
+                step_test_node if self.node,
+                step_get_chromedriver if self.chrome && self.chromedriver.is_none(),
+                step_test_chrome if self.chrome,
+                step_get_geckodriver if self.firefox && self.geckodriver.is_none(),
+                step_test_firefox if self.firefox,
+                step_get_safaridriver if self.safari && self.safaridriver.is_none(),
+                step_test_safari if self.safari,
+            ],
+        }
+    }
+
+    fn step_check_crate_config(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        info!(log, "Checking crate configuration...");
+        manifest::check_crate_config(&self.crate_path, step)?;
+        info!(log, "Crate is correctly configured.");
+        Ok(())
+    }
+
+    fn step_add_wasm_target(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        info!(&log, "Adding wasm-target...");
+        build::rustup_add_wasm_target(step)?;
+        info!(&log, "Adding wasm-target was successful.");
+        Ok(())
+    }
+
+    fn step_build_tests(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        info!(log, "Compiling tests to wasm...");
+
+        let msg = format!("{}Compiling tests to WASM...", emoji::CYCLONE);
+        PBAR.step(step, &msg);
+
+        build::cargo_build_wasm_tests(&self.crate_path, !self.release)?;
+
+        info!(log, "Finished compiling tests to wasm.");
+        Ok(())
+    }
+
+    fn step_install_wasm_bindgen(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        info!(&log, "Identifying wasm-bindgen dependency...");
+        let bindgen_version = manifest::get_wasm_bindgen_version(&self.crate_path)?;
+        info!(&log, "Installing wasm-bindgen-cli...");
+
+        let install_permitted = match self.mode {
+            BuildMode::Normal => true,
+            BuildMode::Noinstall => false,
+        };
+
+        bindgen::install_wasm_bindgen(
+            &self.crate_path,
+            &bindgen_version,
+            install_permitted,
+            step,
+            log,
+        )?;
+
+        self.test_runner_path = Some(bindgen::wasm_bindgen_test_runner_path(log, &self.crate_path)
+            .expect("if installing wasm-bindgen succeeded, then we should have wasm-bindgen-test-runner too"));
+
+        info!(&log, "Installing wasm-bindgen-cli was successful.");
+        Ok(())
+    }
+
+    fn step_test_node(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        assert!(self.node);
+        info!(log, "Running tests in node...");
+        PBAR.step(step, "Running tests in node...");
+        test::cargo_test_wasm(
+            &self.crate_path,
+            self.release,
+            log,
+            Some((
+                "CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER",
+                &self.test_runner_path.as_ref().unwrap(),
+            )),
+        )?;
+        info!(log, "Finished running tests in node.");
+        Ok(())
+    }
+
+    fn step_get_chromedriver(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        PBAR.step(step, "Getting chromedriver...");
+        assert!(self.chrome && self.chromedriver.is_none());
+
+        self.chromedriver = Some(webdriver::get_or_install_chromedriver(
+            log,
+            &self.crate_path,
+            self.mode,
+        )?);
+        Ok(())
+    }
+
+    fn step_test_chrome(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        PBAR.step(step, "Running tests in Chrome...");
+
+        let chromedriver = self.chromedriver.as_ref().unwrap().display().to_string();
+        let chromedriver = chromedriver.as_str();
+        info!(
+            log,
+            "Running tests in Chrome with chromedriver at {}", chromedriver
+        );
+
+        let test_runner = self
+            .test_runner_path
+            .as_ref()
+            .unwrap()
+            .display()
+            .to_string();
+        let test_runner = test_runner.as_str();
+        info!(log, "Using wasm-bindgen test runner at {}", test_runner);
+
+        let mut envs = vec![
+            ("CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER", test_runner),
+            ("CHROMEDRIVER", chromedriver),
+        ];
+        if !self.headless {
+            envs.push(("NO_HEADLESS", "1"));
+        }
+
+        test::cargo_test_wasm(&self.crate_path, self.release, log, envs)
+    }
+
+    fn step_get_geckodriver(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        PBAR.step(step, "Getting geckodriver...");
+        assert!(self.firefox && self.geckodriver.is_none());
+
+        self.geckodriver = Some(webdriver::get_or_install_geckodriver(
+            log,
+            &self.crate_path,
+            self.mode,
+        )?);
+        Ok(())
+    }
+
+    fn step_test_firefox(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        PBAR.step(step, "Running tests in Firefox...");
+
+        let geckodriver = self.geckodriver.as_ref().unwrap().display().to_string();
+        let geckodriver = geckodriver.as_str();
+        info!(
+            log,
+            "Running tests in Firefox with geckodriver at {}", geckodriver
+        );
+
+        let test_runner = self
+            .test_runner_path
+            .as_ref()
+            .unwrap()
+            .display()
+            .to_string();
+        let test_runner = test_runner.as_str();
+        info!(log, "Using wasm-bindgen test runner at {}", test_runner);
+
+        let mut envs = vec![
+            ("CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER", test_runner),
+            ("GECKODRIVER", geckodriver),
+        ];
+        if !self.headless {
+            envs.push(("NO_HEADLESS", "1"));
+        }
+
+        test::cargo_test_wasm(&self.crate_path, self.release, log, envs)
+    }
+
+    fn step_get_safaridriver(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        PBAR.step(step, "Getting safaridriver...");
+        assert!(self.safari && self.safaridriver.is_none());
+
+        self.safaridriver = Some(webdriver::get_safaridriver(log, &self.crate_path)?);
+        Ok(())
+    }
+
+    fn step_test_safari(&mut self, step: &Step, log: &Logger) -> Result<(), Error> {
+        PBAR.step(step, "Running tests in Safari...");
+
+        let safaridriver = self.safaridriver.as_ref().unwrap().display().to_string();
+        let safaridriver = safaridriver.as_str();
+        info!(
+            log,
+            "Running tests in Safari with safaridriver at {}", safaridriver
+        );
+
+        let test_runner = self
+            .test_runner_path
+            .as_ref()
+            .unwrap()
+            .display()
+            .to_string();
+        let test_runner = test_runner.as_str();
+        info!(log, "Using wasm-bindgen test runner at {}", test_runner);
+
+        let mut envs = vec![
+            ("CARGO_TARGET_WASM32_UNKNOWN_UNKNOWN_RUNNER", test_runner),
+            ("SAFARIDRIVER", safaridriver),
+        ];
+        if !self.headless {
+            envs.push(("NO_HEADLESS", "1"));
+        }
+
+        test::cargo_test_wasm(&self.crate_path, self.release, log, envs)
+    }
+}
diff --git a/src/command/utils.rs b/src/command/utils.rs
index 4ab70f3..07db00c 100644
--- a/src/command/utils.rs
+++ b/src/command/utils.rs
@@ -4,18 +4,19 @@ use emoji;
 use error::Error;
 use progressbar::Step;
 use std::fs;
+use std::io;
 use std::path::{Path, PathBuf};
 use PBAR;
 
 /// If an explicit path is given, then use it, otherwise assume the current
 /// directory is the crate path.
-pub fn set_crate_path(path: Option<PathBuf>) -> PathBuf {
+pub fn set_crate_path(path: Option<PathBuf>) -> io::Result<PathBuf> {
     let crate_path = match path {
         Some(p) => p,
         None => PathBuf::from("."),
     };
 
-    crate_path
+    crate_path.canonicalize()
 }
 
 /// Construct our `pkg` directory in the crate.
diff --git a/src/error.rs b/src/error.rs
index 597211f..3646398 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -4,6 +4,7 @@ use serde_json;
 use std::borrow::Cow;
 use std::io;
 use toml;
+use zip;
 
 /// Errors that can potentially occur in `wasm-pack`.
 #[derive(Debug, Fail)]
@@ -24,6 +25,10 @@ pub enum Error {
     /// A curl error.
     Curl(#[cause] curl::Error),
 
+    #[fail(display = "{}", _0)]
+    /// An error handling zip archives.
+    Zip(#[cause] zip::result::ZipError),
+
     /// An error invoking another CLI tool.
     #[fail(display = "{}. stderr:\n\n{}", message, stderr)]
     Cli {
@@ -113,6 +118,7 @@ impl Error {
             Error::Io(_) => "There was an I/O error. Details:\n\n",
             Error::SerdeJson(_) => "There was an JSON error. Details:\n\n",
             Error::SerdeToml(_) => "There was an TOML error. Details:\n\n",
+            Error::Zip(_) => "There was an error handling zip files. Details:\n\n",
             Error::Cli {
                 message: _,
                 stderr: _,
@@ -149,6 +155,12 @@ impl From<serde_json::Error> for Error {
     }
 }
 
+impl From<zip::result::ZipError> for Error {
+    fn from(e: zip::result::ZipError) -> Self {
+        Error::Zip(e)
+    }
+}
+
 impl From<toml::de::Error> for Error {
     fn from(e: toml::de::Error) -> Self {
         Error::SerdeToml(e)
diff --git a/src/lib.rs b/src/lib.rs
index 5873494..7db8931 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -23,7 +23,9 @@ extern crate slog_term;
 extern crate tar;
 extern crate toml;
 extern crate which;
+extern crate zip;
 
+pub mod binaries;
 pub mod bindgen;
 pub mod build;
 pub mod command;
@@ -34,6 +36,8 @@ pub mod manifest;
 pub mod npm;
 pub mod progressbar;
 pub mod readme;
+pub mod target;
+pub mod test;
 
 use progressbar::ProgressOutput;
 
diff --git a/src/logger.rs b/src/logger.rs
index 0f272a9..3e6ef83 100644
--- a/src/logger.rs
+++ b/src/logger.rs
@@ -39,6 +39,7 @@ fn log_file_path(cmd: &Command) -> PathBuf {
         Command::Build(build_opts) => &build_opts.path,
         Command::Pack { path } => path,
         Command::Publish { path } => path,
+        Command::Test(test_opts) => &test_opts.path,
         Command::Login { .. } => &None,
     };
 
diff --git a/src/manifest.rs b/src/manifest.rs
index f0bbca2..ea5f3f6 100644
--- a/src/manifest.rs
+++ b/src/manifest.rs
@@ -13,14 +13,41 @@ use serde_json;
 use toml;
 use PBAR;
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 struct CargoManifest {
     package: CargoPackage,
     dependencies: Option<HashMap<String, CargoDependency>>,
+    #[serde(rename = "dev-dependencies")]
+    dev_dependencies: Option<HashMap<String, CargoDependency>>,
     lib: Option<CargoLib>,
 }
 
-#[derive(Deserialize)]
+fn normalize_dependency_name(dep: &str) -> String {
+    dep.replace("-", "_")
+}
+
+fn normalize_dependencies(
+    deps: HashMap<String, CargoDependency>,
+) -> HashMap<String, CargoDependency> {
+    let mut new_deps = HashMap::with_capacity(deps.len());
+    for (key, val) in deps {
+        new_deps.insert(normalize_dependency_name(&key), val);
+    }
+    new_deps
+}
+
+impl CargoManifest {
+    fn normalize_dependencies(&mut self) {
+        if let Some(deps) = self.dependencies.take() {
+            self.dependencies = Some(normalize_dependencies(deps));
+        }
+        if let Some(dev_deps) = self.dev_dependencies.take() {
+            self.dev_dependencies = Some(normalize_dependencies(dev_deps));
+        }
+    }
+}
+
+#[derive(Debug, Deserialize)]
 struct CargoPackage {
     name: String,
     authors: Vec<String>,
@@ -30,19 +57,19 @@ struct CargoPackage {
     repository: Option<String>,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 #[serde(untagged)]
 enum CargoDependency {
     Simple(String),
     Detailed(DetailedCargoDependency),
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 struct DetailedCargoDependency {
     version: Option<String>,
 }
 
-#[derive(Deserialize)]
+#[derive(Debug, Deserialize)]
 struct CargoLib {
     #[serde(rename = "crate-type")]
     crate_type: Option<Vec<String>>,
@@ -86,7 +113,10 @@ fn read_cargo_toml(path: &Path) -> Result<CargoManifest, Error> {
     let mut cargo_contents = String::new();
     cargo_file.read_to_string(&mut cargo_contents)?;
 
-    Ok(toml::from_str(&cargo_contents)?)
+    let mut manifest: CargoManifest = toml::from_str(&cargo_contents)?;
+    manifest.normalize_dependencies();
+
+    Ok(manifest)
 }
 
 impl CargoManifest {
@@ -192,6 +222,7 @@ pub fn check_crate_config(path: &Path, step: &Step) -> Result<(), Error> {
     let msg = format!("{}Checking crate configuration...", emoji::WRENCH);
     PBAR.step(&step, &msg);
     check_wasm_bindgen(path)?;
+    check_wasm_bindgen_test(path)?;
     check_crate_type(path)?;
     Ok(())
 }
@@ -201,6 +232,24 @@ fn check_wasm_bindgen(path: &Path) -> Result<(), Error> {
     Ok(())
 }
 
+fn check_wasm_bindgen_test(path: &Path) -> Result<(), Error> {
+    let expected_version = get_wasm_bindgen_version(path)?;
+
+    // Only do the version check if `wasm-bindgen-test` is actually a
+    // dependency. Not every crate needs to have tests!
+    if let Ok(actual_version) = get_wasm_bindgen_test_version(path) {
+        if expected_version != actual_version {
+            return Error::crate_config(&format!(
+                "The `wasm-bindgen-test` dependency version ({}) must match \
+                 the `wasm-bindgen` dependency version ({}), but it does not.",
+                actual_version, expected_version
+            ));
+        }
+    }
+
+    Ok(())
+}
+
 fn check_crate_type(path: &Path) -> Result<(), Error> {
     if read_cargo_toml(path)?.lib.map_or(false, |lib| {
         lib.crate_type
@@ -209,17 +258,22 @@ fn check_crate_type(path: &Path) -> Result<(), Error> {
         return Ok(());
     }
     Error::crate_config(
-      "crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your Cargo.toml file:\n\n[lib]\ncrate-type = [\"cdylib\"]"
+      "crate-type must be cdylib to compile to wasm32-unknown-unknown. Add the following to your \
+       Cargo.toml file:\n\n\
+       [lib]\n\
+       crate-type = [\"cdylib\"]"
     )
 }
 
-/// Get the version of `wasm-bindgen` specified as a dependency.
-pub fn get_wasm_bindgen_version(path: &Path) -> Result<String, Error> {
-    if let Some(deps) = read_cargo_toml(path)?.dependencies {
-        match deps
-            .get("wasm-bindgen")
-            .or_else(|| deps.get("wasm_bindgen"))
-        {
+fn get_dependency_version(
+    dependencies: Option<&HashMap<String, CargoDependency>>,
+    dependency: &str,
+    dependencies_section_name: &str,
+    version_suggestion: &str,
+) -> Result<String, Error> {
+    if let Some(deps) = dependencies {
+        let dependency = normalize_dependency_name(dependency);
+        match deps.get(&dependency) {
             Some(CargoDependency::Simple(version))
             | Some(CargoDependency::Detailed(DetailedCargoDependency {
                 version: Some(version),
@@ -227,14 +281,20 @@ pub fn get_wasm_bindgen_version(path: &Path) -> Result<String, Error> {
             Some(CargoDependency::Detailed(DetailedCargoDependency { version: None })) => {
                 let msg = format!(
                     "\"{}\" dependency is missing its version number",
-                    style("wasm-bindgen").bold().dim()
+                    style(&dependency).bold().dim()
                 );
                 Err(Error::CrateConfig { message: msg })
             }
             None => {
                 let message = format!(
-                    "Ensure that you have \"{}\" as a dependency in your Cargo.toml file:\n[dependencies]\nwasm-bindgen = \"0.2\"",
-                    style("wasm-bindgen").bold().dim());
+                    "Ensure that you have \"{}\" as a dependency in your Cargo.toml file:\n\
+                     [{}]\n\
+                     {} = \"{}\"",
+                    style(&dependency).bold().dim(),
+                    dependencies_section_name,
+                    dependency,
+                    version_suggestion
+                );
                 Err(Error::CrateConfig { message })
             }
         }
@@ -243,3 +303,25 @@ pub fn get_wasm_bindgen_version(path: &Path) -> Result<String, Error> {
         Err(Error::CrateConfig { message })
     }
 }
+
+/// Get the version of `wasm-bindgen` specified as a dependency.
+pub fn get_wasm_bindgen_version(path: &Path) -> Result<String, Error> {
+    let toml = read_cargo_toml(path)?;
+    get_dependency_version(
+        toml.dependencies.as_ref(),
+        "wasm-bindgen",
+        "dependencies",
+        "0.2",
+    )
+}
+
+/// Get the version of `wasm-bindgen-test` specified as a dependency.
+pub fn get_wasm_bindgen_test_version(path: &Path) -> Result<String, Error> {
+    let toml = read_cargo_toml(path)?;
+    get_dependency_version(
+        toml.dev_dependencies.as_ref(),
+        "wasm-bindgen-test",
+        "dev-dependencies",
+        "0.2",
+    )
+}
diff --git a/src/target.rs b/src/target.rs
new file mode 100644
index 0000000..cfaff6a
--- /dev/null
+++ b/src/target.rs
@@ -0,0 +1,15 @@
+//! Information about the target wasm-pack is currently being compiled for.
+//!
+//! That is, whether we are building wasm-pack for windows vs linux, and x86 vs
+//! x86-64, etc.
+
+#![allow(missing_docs)]
+
+pub const LINUX: bool = cfg!(target_os = "linux");
+pub const MACOS: bool = cfg!(target_os = "macos");
+pub const WINDOWS: bool = cfg!(target_os = "windows");
+
+#[allow(non_upper_case_globals)]
+pub const x86_64: bool = cfg!(target_arch = "x86_64");
+#[allow(non_upper_case_globals)]
+pub const x86: bool = cfg!(target_arch = "x86");
diff --git a/src/test/mod.rs b/src/test/mod.rs
new file mode 100644
index 0000000..f0ea233
--- /dev/null
+++ b/src/test/mod.rs
@@ -0,0 +1,51 @@
+//! Testing a Rust crate compiled to wasm.
+
+pub mod webdriver;
+
+use error::Error;
+use slog::Logger;
+use std::ffi::OsStr;
+use std::path::Path;
+use std::process::Command;
+
+/// Run `cargo test` with the `nightly` toolchain and targeting
+/// `wasm32-unknown-unknown`.
+pub fn cargo_test_wasm<I, K, V>(
+    path: &Path,
+    release: bool,
+    log: &Logger,
+    envs: I,
+) -> Result<(), Error>
+where
+    I: IntoIterator<Item = (K, V)>,
+    K: AsRef<OsStr>,
+    V: AsRef<OsStr>,
+{
+    use std::sync::Mutex;
+    lazy_static! {
+        static ref ONE_TEST_AT_A_TIME: Mutex<()> = Mutex::new(());
+    }
+    let _locked = ONE_TEST_AT_A_TIME.lock().unwrap();
+
+    let output = {
+        let mut cmd = Command::new("cargo");
+        cmd.envs(envs);
+        cmd.current_dir(path).arg("+nightly").arg("test");
+        if release {
+            cmd.arg("--release");
+        }
+        cmd.arg("--target").arg("wasm32-unknown-unknown");
+        cmd.output()?
+    };
+
+    if !output.status.success() {
+        let s = String::from_utf8_lossy(&output.stderr);
+        Error::cli("Running wasm tests failed", s)
+    } else {
+        for line in String::from_utf8_lossy(&output.stdout).lines() {
+            info!(log, "test output: {}", line);
+            println!("{}", line);
+        }
+        Ok(())
+    }
+}
diff --git a/src/test/webdriver.rs b/src/test/webdriver.rs
new file mode 100644
index 0000000..f507775
--- /dev/null
+++ b/src/test/webdriver.rs
@@ -0,0 +1,134 @@
+//! Getting WebDriver client binaries.
+
+use binaries::{
+    self, bin_path, install_binaries_from_targz_at_url, install_binaries_from_zip_at_url,
+};
+use command::build::BuildMode;
+use error::Error;
+use slog::Logger;
+use std::path::{Path, PathBuf};
+use target;
+
+/// Get the path to an existing `chromedriver`, or install it if no existing
+/// binary is found.
+pub fn get_or_install_chromedriver(
+    log: &Logger,
+    crate_path: &Path,
+    mode: BuildMode,
+) -> Result<PathBuf, Error> {
+    match (mode, bin_path(log, crate_path, "chromedriver")) {
+        (_, Some(path)) => Ok(path),
+        (BuildMode::Normal, None) => install_chromedriver(crate_path),
+        (BuildMode::Noinstall, None) => Error::crate_config(
+            "No crate-local `chromedriver` binary found, and could not find a global \
+             `chromedriver` on the `$PATH`. Not installing `chromedriver` because of noinstall \
+             mode.",
+        ).map(|_| unreachable!()),
+    }
+}
+
+fn get_local_chromedriver_path(crate_path: &Path) -> PathBuf {
+    binaries::local_bin_path(crate_path, "chromedriver")
+}
+
+fn get_chromedriver_url() -> Result<String, Error> {
+    let target = if target::LINUX && target::x86_64 {
+        "linux64"
+    } else if target::MACOS && target::x86_64 {
+        "mac64"
+    } else if target::WINDOWS && target::x86 {
+        "win32"
+    } else {
+        return Err(Error::unsupported(
+            "geckodriver binaries are unavailable for this target",
+        ));
+    };
+
+    Ok(format!(
+        "https://chromedriver.storage.googleapis.com/2.41/chromedriver_{}.zip",
+        target
+    ))
+}
+
+/// Download and install a pre-built `chromedriver` binary.
+pub fn install_chromedriver(crate_path: &Path) -> Result<PathBuf, Error> {
+    let url = get_chromedriver_url()?;
+    install_binaries_from_zip_at_url(crate_path, &url, Some("chromedriver"))?;
+    let chromedriver = get_local_chromedriver_path(crate_path);
+    assert!(chromedriver.is_file());
+    Ok(chromedriver)
+}
+
+/// Get the path to an existing `geckodriver`, or install it if no existing
+/// binary is found.
+pub fn get_or_install_geckodriver(
+    log: &Logger,
+    crate_path: &Path,
+    mode: BuildMode,
+) -> Result<PathBuf, Error> {
+    match (mode, bin_path(log, crate_path, "geckodriver")) {
+        (_, Some(path)) => Ok(path),
+        (BuildMode::Normal, None) => install_geckodriver(crate_path),
+        (BuildMode::Noinstall, None) => Error::crate_config(
+            "No crate-local `geckodriver` binary found, and could not find a global `geckodriver` \
+             on the `$PATH`. Not installing `geckodriver` because of noinstall mode.",
+        ).map(|_| unreachable!()),
+    }
+}
+
+fn get_geckodriver_url() -> Result<String, Error> {
+    let (target, ext) = if target::LINUX && target::x86 {
+        ("linux32", "tar.gz")
+    } else if target::LINUX && target::x86_64 {
+        ("linux64", "tar.gz")
+    } else if target::MACOS {
+        ("macos", "tar.gz")
+    } else if target::WINDOWS && target::x86 {
+        ("win32", "zip")
+    } else if target::WINDOWS && target::x86_64 {
+        ("win64", "zip")
+    } else {
+        return Err(Error::unsupported(
+            "geckodriver binaries are unavailable for this target",
+        ));
+    };
+
+    Ok(format!(
+        "https://github.com/mozilla/geckodriver/releases/download/v0.21.0/geckodriver-v0.21.0-{}.{}",
+        target,
+        ext,
+    ))
+}
+
+fn get_local_geckodriver_path(crate_path: &Path) -> PathBuf {
+    binaries::local_bin_path(crate_path, "geckodriver")
+}
+
+/// Download and install a pre-built `geckodriver` binary.
+pub fn install_geckodriver(crate_path: &Path) -> Result<PathBuf, Error> {
+    let url = get_geckodriver_url()?;
+
+    if url.ends_with("tar.gz") {
+        install_binaries_from_targz_at_url(crate_path, &url, Some("geckodriver"))?;
+    } else {
+        assert!(url.ends_with("zip"));
+        install_binaries_from_zip_at_url(crate_path, &url, Some("geckodriver"))?;
+    }
+
+    let geckodriver = get_local_geckodriver_path(crate_path);
+    assert!(geckodriver.is_file());
+    Ok(geckodriver)
+}
+
+/// Get the path to an existing `safaridriver`.
+///
+/// We can't install `safaridriver` if an existing one is not found because
+/// Apple does not provide pre-built binaries. However, `safaridriver` *should*
+/// be present by default.
+pub fn get_safaridriver(log: &Logger, crate_path: &Path) -> Result<PathBuf, Error> {
+    if let Some(p) = bin_path(log, crate_path, "safaridriver") {
+        Ok(p)
+    } else {
+        Error::crate_config("could not find `safaridriver` on the `$PATH`").map(|_| unreachable!())
+    }
+}
diff --git a/tests/all/main.rs b/tests/all/main.rs
index 5699894..2fc73ec 100644
--- a/tests/all/main.rs
+++ b/tests/all/main.rs
@@ -1,16 +1,18 @@
 extern crate copy_dir;
 extern crate failure;
 #[macro_use]
+extern crate lazy_static;
+#[macro_use]
 extern crate serde_derive;
 extern crate serde_json;
 extern crate structopt;
 extern crate tempfile;
 extern crate wasm_pack;
-#[macro_use]
-extern crate lazy_static;
 
 mod bindgen;
 mod build;
 mod manifest;
 mod readme;
+mod test;
 mod utils;
+mod webdriver;
diff --git a/tests/all/manifest.rs b/tests/all/manifest.rs
index 668e112..d300ea1 100644
--- a/tests/all/manifest.rs
+++ b/tests/all/manifest.rs
@@ -250,3 +250,16 @@ fn it_gets_wasm_bindgen_version_with_underscores() {
         "0.2"
     );
 }
+
+#[test]
+fn the_wasm_bindgen_test_version_should_match_the_wasm_bindgen_version() {
+    let fixture = fixture::fixture("tests/fixtures/wbg-test-bad-versions");
+    let step = wasm_pack::progressbar::Step::new(1);
+    let result = manifest::check_crate_config(&fixture.path, &step);
+    assert!(result.is_err());
+    let msg = result.unwrap_err().to_string();
+    assert!(msg.contains(&format!(
+        "The `wasm-bindgen-test` dependency version (0.2.19) must match \
+         the `wasm-bindgen` dependency version (0.2.21), but it does not."
+    )));
+}
diff --git a/tests/all/test.rs b/tests/all/test.rs
new file mode 100644
index 0000000..00d21d0
--- /dev/null
+++ b/tests/all/test.rs
@@ -0,0 +1,164 @@
+use std::env;
+use std::fs;
+use tempfile;
+use utils::fixture::fixture;
+use wasm_pack::binaries;
+use wasm_pack::command::{self, build, test, Command};
+use wasm_pack::logger;
+
+#[test]
+fn it_can_run_node_tests() {
+    let fixture = fixture("tests/fixtures/wbg-test-node");
+    fixture.install_local_wasm_bindgen();
+    let cmd = Command::Test(test::TestOptions {
+        path: Some(fixture.path.clone()),
+        node: true,
+        mode: build::BuildMode::Noinstall,
+        ..Default::default()
+    });
+    let logger = logger::new(&cmd, 3).unwrap();
+    command::run_wasm_pack(cmd, &logger).expect("should run test command OK");
+}
+
+#[test]
+#[cfg(any(
+    all(target_os = "linux", target_arch = "x86_64"),
+    all(target_os = "macos", target_arch = "x86_64"),
+    all(target_os = "windows", target_arch = "x86"),
+    all(target_os = "windows", target_arch = "x86_64")
+))]
+fn it_can_run_browser_tests() {
+    let fixture = fixture("tests/fixtures/wbg-test-browser");
+    fixture.install_local_wasm_bindgen();
+
+    let firefox = cfg!(any(
+        all(target_os = "linux", target_arch = "x86"),
+        all(target_os = "linux", target_arch = "x86_64"),
+        all(target_os = "macos", target_arch = "x86_64"),
+        all(target_os = "windows", target_arch = "x86"),
+        all(target_os = "windows", target_arch = "x86_64")
+    ));
+    if firefox {
+        fixture.install_local_geckodriver();
+    }
+
+    let chrome = cfg!(any(
+        all(target_os = "linux", target_arch = "x86_64"),
+        all(target_os = "macos", target_arch = "x86_64"),
+        all(target_os = "windows", target_arch = "x86")
+    ));
+    if chrome {
+        fixture.install_local_chromedriver();
+    }
+
+    let safari = cfg!(target_os = "macos");
+
+    if !firefox && !chrome && !safari {
+        return;
+    }
+
+    let cmd = Command::Test(test::TestOptions {
+        path: Some(fixture.path.clone()),
+        firefox,
+        chrome,
+        safari,
+        headless: true,
+        mode: build::BuildMode::Noinstall,
+        ..Default::default()
+    });
+
+    let logger = logger::new(&cmd, 3).unwrap();
+    command::run_wasm_pack(cmd, &logger).expect("should run test command OK");
+}
+
+#[test]
+fn it_can_run_failing_tests() {
+    let fixture = fixture("tests/fixtures/wbg-test-fail");
+    fixture.install_local_wasm_bindgen();
+    let cmd = Command::Test(test::TestOptions {
+        path: Some(fixture.path.clone()),
+        node: true,
+        mode: build::BuildMode::Noinstall,
+        ..Default::default()
+    });
+    let logger = logger::new(&cmd, 3).unwrap();
+    assert!(
+        command::run_wasm_pack(cmd, &logger).is_err(),
+        "failing tests should return Err"
+    );
+}
+
+#[test]
+#[cfg(any(
+    all(target_os = "linux", target_arch = "x86"),
+    all(target_os = "linux", target_arch = "x86_64"),
+    all(target_os = "macos", target_arch = "x86_64"),
+    all(target_os = "windows", target_arch = "x86"),
+    all(target_os = "windows", target_arch = "x86_64")
+))]
+fn it_can_find_a_webdriver_on_path() {
+    let fixture = fixture("tests/fixtures/wbg-test-browser");
+    fixture.install_local_wasm_bindgen();
+    fixture.install_local_geckodriver();
+
+    let geckodriver_dir = tempfile::TempDir::new().unwrap();
+    let local_geckodriver = binaries::local_bin_path(&fixture.path, "geckodriver");
+    fs::copy(
+        &local_geckodriver,
+        geckodriver_dir
+            .path()
+            .join(local_geckodriver.file_name().unwrap()),
+    ).unwrap();
+    fs::remove_file(&local_geckodriver).unwrap();
+
+    let mut paths: Vec<_> = env::split_paths(&env::var("PATH").unwrap()).collect();
+    paths.insert(0, geckodriver_dir.path().into());
+    env::set_var("PATH", env::join_paths(paths).unwrap());
+
+    let cmd = Command::Test(test::TestOptions {
+        path: Some(fixture.path.clone()),
+        firefox: true,
+        headless: true,
+        mode: build::BuildMode::Noinstall,
+        ..Default::default()
+    });
+    let logger = logger::new(&cmd, 3).unwrap();
+    command::run_wasm_pack(cmd, &logger).expect("should run test command OK");
+}
+
+#[test]
+fn it_requires_node_or_a_browser() {
+    let fixture = fixture("tests/fixtures/wbg-test-node");
+    fixture.install_local_wasm_bindgen();
+
+    let cmd = Command::Test(test::TestOptions {
+        path: Some(fixture.path.clone()),
+        mode: build::BuildMode::Noinstall,
+        // Note: not setting node or any browser to true here.
+        ..Default::default()
+    });
+    let logger = logger::new(&cmd, 3).unwrap();
+    assert!(
+        command::run_wasm_pack(cmd, &logger).is_err(),
+        "need to enable node or browser testing"
+    );
+}
+
+#[test]
+fn the_headless_flag_requires_a_browser() {
+    let fixture = fixture("tests/fixtures/wbg-test-node");
+    fixture.install_local_wasm_bindgen();
+
+    let cmd = Command::Test(test::TestOptions {
+        path: Some(fixture.path.clone()),
+        node: true,
+        mode: build::BuildMode::Noinstall,
+        headless: true,
+        ..Default::default()
+    });
+    let logger = logger::new(&cmd, 3).unwrap();
+    assert!(
+        command::run_wasm_pack(cmd, &logger).is_err(),
+        "running headless tests in node doesn't make sense"
+    );
+}
diff --git a/tests/all/utils/fixture.rs b/tests/all/utils/fixture.rs
index 3122936..2d4c76c 100644
--- a/tests/all/utils/fixture.rs
+++ b/tests/all/utils/fixture.rs
@@ -1,10 +1,19 @@
+use std::env;
+use std::fs;
+use std::mem::ManuallyDrop;
 use std::path::{Path, PathBuf};
+use std::sync::{Once, ONCE_INIT};
+use std::thread;
+use wasm_pack;
 
 use copy_dir::copy_dir;
 use tempfile;
 
 pub struct Fixture {
-    pub dir: tempfile::TempDir,
+    // NB: we wrap the fixture's tempdir in a `ManuallyDrop` so that if a test
+    // fails, its directory isn't deleted, and we have a chance to manually
+    // inspect its state and figure out what is going on.
+    pub dir: ManuallyDrop<tempfile::TempDir>,
     pub path: PathBuf,
 }
 
@@ -17,11 +26,21 @@ pub fn fixture<P>(fixture: P) -> Fixture
 where
     P: AsRef<Path>,
 {
+    // Make sure that all fixtures end up sharing a target dir, and we don't
+    // recompile wasm-bindgen and friends many times over.
+    static SET_TARGET_DIR: Once = ONCE_INIT;
+    SET_TARGET_DIR.call_once(|| {
+        env::set_var(
+            "CARGO_TARGET_DIR",
+            Path::new(env!("CARGO_MANIFEST_DIR")).join("target"),
+        );
+    });
+
     let fixture = fixture
         .as_ref()
         .canonicalize()
         .expect("should canonicalize fixture path OK");
-    let dir = tempfile::tempdir().expect("should create temporary directory OK");
+    let dir = ManuallyDrop::new(tempfile::tempdir().expect("should create temporary directory OK"));
     let path = dir.path().join("wasm-pack");
     println!(
         "wasm-pack: copying test fixture '{}' to temporary directory '{}'",
@@ -44,3 +63,102 @@ where
 
     Fixture { dir, path }
 }
+
+impl Fixture {
+    /// Install a local wasm-bindgen for this fixture.
+    ///
+    /// Takes care not to re-install for every fixture, but only the one time
+    /// for the whole test suite.
+    pub fn install_local_wasm_bindgen(&self) {
+        static INSTALL_WASM_BINDGEN: Once = ONCE_INIT;
+
+        let tests = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
+        let bin = tests.join("bin");
+
+        INSTALL_WASM_BINDGEN.call_once(|| {
+            if bin.join("wasm-bindgen").is_file() {
+                return;
+            }
+
+            const WASM_BINDGEN_VERSION: &str = "0.2.21";
+            wasm_pack::bindgen::download_prebuilt_wasm_bindgen(&tests, WASM_BINDGEN_VERSION)
+                .or_else(|_| {
+                    wasm_pack::bindgen::cargo_install_wasm_bindgen(&tests, WASM_BINDGEN_VERSION)
+                }).unwrap();
+        });
+
+        copy_dir(bin, self.path.join("bin")).expect("could not copy `bin` directory into temp dir");
+    }
+
+    /// Download `geckodriver` and return its path.
+    ///
+    /// Takes care to ensure that only one `geckodriver` is downloaded for the whole
+    /// test suite.
+    pub fn install_local_geckodriver(&self) {
+        static FETCH_GECKODRIVER: Once = ONCE_INIT;
+
+        let tests = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
+
+        wasm_pack::binaries::ensure_local_bin_dir(&tests)
+            .expect("could not create fixture's `bin` directory");
+
+        let geckodriver = wasm_pack::binaries::local_bin_path(&tests, "geckodriver");
+
+        FETCH_GECKODRIVER.call_once(|| {
+            if geckodriver.is_file() {
+                return;
+            }
+
+            wasm_pack::test::webdriver::install_geckodriver(&tests).unwrap();
+            assert!(geckodriver.is_file());
+        });
+
+        wasm_pack::binaries::ensure_local_bin_dir(&self.path)
+            .expect("could not create fixture's `bin` directory");
+
+        fs::copy(
+            &geckodriver,
+            wasm_pack::binaries::local_bin_path(&self.path, "geckodriver"),
+        ).expect("could not copy `geckodriver` to fixture directory");
+    }
+
+    /// Download `chromedriver` and return its path.
+    ///
+    /// Takes care to ensure that only one `chromedriver` is downloaded for the whole
+    /// test suite.
+    pub fn install_local_chromedriver(&self) {
+        static FETCH_CHROMEDRIVER: Once = ONCE_INIT;
+
+        let tests = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests");
+
+        wasm_pack::binaries::ensure_local_bin_dir(&tests)
+            .expect("could not create fixture's `bin` directory");
+
+        let chromedriver = wasm_pack::binaries::local_bin_path(&tests, "chromedriver");
+
+        FETCH_CHROMEDRIVER.call_once(|| {
+            if chromedriver.is_file() {
+                return;
+            }
+
+            wasm_pack::test::webdriver::install_chromedriver(&tests).unwrap();
+            assert!(chromedriver.is_file());
+        });
+
+        wasm_pack::binaries::ensure_local_bin_dir(&self.path)
+            .expect("could not create fixture's `bin` directory");
+
+        fs::copy(
+            &chromedriver,
+            wasm_pack::binaries::local_bin_path(&self.path, "chromedriver"),
+        ).expect("could not copy `chromedriver` to fixture directory");
+    }
+}
+
+impl Drop for Fixture {
+    fn drop(&mut self) {
+        if !thread::panicking() {
+            unsafe { ManuallyDrop::drop(&mut self.dir) }
+        }
+    }
+}
diff --git a/tests/all/webdriver.rs b/tests/all/webdriver.rs
new file mode 100644
index 0000000..21e3341
--- /dev/null
+++ b/tests/all/webdriver.rs
@@ -0,0 +1,26 @@
+use utils::fixture::fixture;
+use wasm_pack::test::webdriver;
+
+#[test]
+#[cfg(any(
+    all(target_os = "linux", target_arch = "x86_64"),
+    all(target_os = "macos", target_arch = "x86_64"),
+    all(target_os = "windows", target_arch = "x86")
+))]
+fn can_install_chromedriver() {
+    let fixture = fixture("tests/fixtures/js-hello-world");
+    assert!(webdriver::install_chromedriver(&fixture.path).is_ok());
+}
+
+#[test]
+#[cfg(any(
+    all(target_os = "linux", target_arch = "x86"),
+    all(target_os = "linux", target_arch = "x86_64"),
+    all(target_os = "macos", target_arch = "x86_64"),
+    all(target_os = "windows", target_arch = "x86"),
+    all(target_os = "windows", target_arch = "x86_64")
+))]
+fn can_install_geckodriver() {
+    let fixture = fixture("tests/fixtures/js-hello-world");
+    assert!(webdriver::install_geckodriver(&fixture.path).is_ok());
+}
diff --git a/tests/fixtures/wbg-test-bad-versions/Cargo.toml b/tests/fixtures/wbg-test-bad-versions/Cargo.toml
new file mode 100644
index 0000000..e88d9be
--- /dev/null
+++ b/tests/fixtures/wbg-test-bad-versions/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "wbg-test-node"
+version = "0.1.0"
+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+# We depend on wasm-bindgen 0.2.21
+wasm-bindgen = "0.2.21"
+
+[dev-dependencies]
+# And we depend on wasm-bindgen-test 0.2.19. But this should match the
+# wasm-bindgen dependency!
+wasm-bindgen-test = "0.2.19"
diff --git a/tests/fixtures/wbg-test-bad-versions/src/lib.rs b/tests/fixtures/wbg-test-bad-versions/src/lib.rs
new file mode 100644
index 0000000..31e1bb2
--- /dev/null
+++ b/tests/fixtures/wbg-test-bad-versions/src/lib.rs
@@ -0,0 +1,7 @@
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert_eq!(2 + 2, 4);
+    }
+}
diff --git a/tests/fixtures/wbg-test-bad-versions/tests/node.rs b/tests/fixtures/wbg-test-bad-versions/tests/node.rs
new file mode 100644
index 0000000..84e09f5
--- /dev/null
+++ b/tests/fixtures/wbg-test-bad-versions/tests/node.rs
@@ -0,0 +1,8 @@
+extern crate wasm_bindgen_test;
+
+use wasm_bindgen_test::*;
+
+#[wasm_bindgen_test]
+fn pass() {
+    assert_eq!(1, 1);
+}
diff --git a/tests/fixtures/wbg-test-browser/Cargo.toml b/tests/fixtures/wbg-test-browser/Cargo.toml
new file mode 100644
index 0000000..ea109aa
--- /dev/null
+++ b/tests/fixtures/wbg-test-browser/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "wbg-test-browser"
+version = "0.1.0"
+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+wasm-bindgen = "0.2.21"
+
+[dev-dependencies]
+wasm-bindgen-test = "0.2.21"
diff --git a/tests/fixtures/wbg-test-browser/src/lib.rs b/tests/fixtures/wbg-test-browser/src/lib.rs
new file mode 100644
index 0000000..28599bb
--- /dev/null
+++ b/tests/fixtures/wbg-test-browser/src/lib.rs
@@ -0,0 +1,5 @@
+extern crate wasm_bindgen;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub fn hello() {}
diff --git a/tests/fixtures/wbg-test-browser/tests/browser.rs b/tests/fixtures/wbg-test-browser/tests/browser.rs
new file mode 100644
index 0000000..dbdf099
--- /dev/null
+++ b/tests/fixtures/wbg-test-browser/tests/browser.rs
@@ -0,0 +1,9 @@
+extern crate wasm_bindgen_test;
+use wasm_bindgen_test::*;
+
+wasm_bindgen_test_configure!(run_in_browser);
+
+#[wasm_bindgen_test]
+fn pass() {
+    assert_eq!(1, 1);
+}
diff --git a/tests/fixtures/wbg-test-fail/Cargo.toml b/tests/fixtures/wbg-test-fail/Cargo.toml
new file mode 100644
index 0000000..831cf6b
--- /dev/null
+++ b/tests/fixtures/wbg-test-fail/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "wbg-test-ok"
+version = "0.1.0"
+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+wasm-bindgen = "0.2.21"
+
+[dev-dependencies]
+wasm-bindgen-test = "0.2.21"
diff --git a/tests/fixtures/wbg-test-fail/src/lib.rs b/tests/fixtures/wbg-test-fail/src/lib.rs
new file mode 100644
index 0000000..1659959
--- /dev/null
+++ b/tests/fixtures/wbg-test-fail/src/lib.rs
@@ -0,0 +1,5 @@
+extern crate wasm_bindgen;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub fn hi() {}
diff --git a/tests/fixtures/wbg-test-fail/tests/node.rs b/tests/fixtures/wbg-test-fail/tests/node.rs
new file mode 100644
index 0000000..bb2c049
--- /dev/null
+++ b/tests/fixtures/wbg-test-fail/tests/node.rs
@@ -0,0 +1,7 @@
+extern crate wasm_bindgen_test;
+use wasm_bindgen_test::*;
+
+#[wasm_bindgen_test]
+fn fail() {
+    assert_eq!(1, 2);
+}
diff --git a/tests/fixtures/wbg-test-node/Cargo.toml b/tests/fixtures/wbg-test-node/Cargo.toml
new file mode 100644
index 0000000..75f2391
--- /dev/null
+++ b/tests/fixtures/wbg-test-node/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "wbg-test-node"
+version = "0.1.0"
+authors = ["Nick Fitzgerald <fitzgen@gmail.com>"]
+
+[lib]
+crate-type = ["cdylib"]
+
+[dependencies]
+wasm-bindgen = "0.2.21"
+
+[dev-dependencies]
+wasm-bindgen-test = "0.2.21"
diff --git a/tests/fixtures/wbg-test-node/src/lib.rs b/tests/fixtures/wbg-test-node/src/lib.rs
new file mode 100644
index 0000000..31e1bb2
--- /dev/null
+++ b/tests/fixtures/wbg-test-node/src/lib.rs
@@ -0,0 +1,7 @@
+#[cfg(test)]
+mod tests {
+    #[test]
+    fn it_works() {
+        assert_eq!(2 + 2, 4);
+    }
+}
diff --git a/tests/fixtures/wbg-test-node/tests/node.rs b/tests/fixtures/wbg-test-node/tests/node.rs
new file mode 100644
index 0000000..84e09f5
--- /dev/null
+++ b/tests/fixtures/wbg-test-node/tests/node.rs
@@ -0,0 +1,8 @@
+extern crate wasm_bindgen_test;
+
+use wasm_bindgen_test::*;
+
+#[wasm_bindgen_test]
+fn pass() {
+    assert_eq!(1, 1);
+}