diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml index 1a47b46d..e4d01564 100644 --- a/crates/cli/Cargo.toml +++ b/crates/cli/Cargo.toml @@ -13,11 +13,15 @@ information see https://github.com/alexcrichton/wasm-bindgen. """ [dependencies] +curl = "0.4.13" docopt = "1.0" +env_logger = "0.5" failure = "0.1" +log = "0.4" parity-wasm = "0.31" rouille = { version = "2.1.0", default-features = false } serde = "1.0" serde_derive = "1.0" +serde_json = "1.0" wasm-bindgen-cli-support = { path = "../cli-support", version = "=0.2.15" } wasm-bindgen-shared = { path = "../shared", version = "=0.2.15" } diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/headless.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/headless.rs new file mode 100644 index 00000000..062f50f2 --- /dev/null +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/headless.rs @@ -0,0 +1,444 @@ +use std::cell::{Cell, RefCell}; +use std::env; +use std::io::{self, Read}; +use std::net::{SocketAddr, TcpListener, TcpStream}; +use std::path::{PathBuf, Path}; +use std::process::{Command, Stdio}; +use std::thread; +use std::time::{Instant, Duration}; + +use curl::easy::Easy; +use failure::{ResultExt, Error}; +use serde::{Serialize, Deserialize}; +use serde_json; + +use shell::Shell; + +/// Execute a headless browser tests against a server running on `server` +/// address. +pub fn run(server: &SocketAddr, shell: &Shell) -> Result<(), Error> { + let (driver, args) = Driver::find()?; + println!("Running headless tests in {} with `{}`", + driver.browser(), + driver.path().display()); + + // Allow tests to run in parallel (in theory) by finding any open port + // available for our driver. We can't bind the port for the driver, but + // hopefully the OS gives this invocation unique ports across processes + let driver_addr = TcpListener::bind("127.0.0.1:0")?.local_addr()?; + + // Spawn the driver binary, collecting its stdout/stderr in separate + // threads. We'll print this output later. + shell.status("Spawning Geckodriver..."); + let mut cmd = Command::new(&driver.path()); + cmd.args(&args) + .arg(format!("--port={}", driver_addr.port().to_string())) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .stdin(Stdio::null()); + let mut child = cmd.spawn() + .context(format!("failed to spawn {:?} binary", driver.path()))?; + let mut stdout = child.stdout.take().unwrap(); + let mut stderr = child.stderr.take().unwrap(); + let mut stdout = Some(thread::spawn(move || read(&mut stdout))); + let mut stderr = Some(thread::spawn(move || read(&mut stderr))); + let print_driver_stdio = Cell::new(true); + let _f = OnDrop(|| { + child.kill().unwrap(); + let status = child.wait().unwrap(); + if !print_driver_stdio.get() { + return + } + + shell.clear(); + println!("driver status: {}", status); + + let stdout = stdout.take().unwrap().join().unwrap().unwrap(); + if stdout.len() > 0 { + println!("driver stdout:\n{}", tab(&String::from_utf8_lossy(&stdout))); + } + let stderr = stderr.take().unwrap().join().unwrap().unwrap(); + if stderr.len() > 0 { + println!("driver stderr:\n{}", tab(&String::from_utf8_lossy(&stderr))); + } + }); + + // Wait for the driver to come online and bind its port before we try to + // connect to it. + let start = Instant::now(); + let max = Duration::new(5, 0); + let mut bound = false; + while start.elapsed() < max { + if TcpStream::connect(&driver_addr).is_ok() { + bound = true; + break + } + thread::sleep(Duration::from_millis(100)); + } + if !bound { + bail!("driver failed to bind port during startup") + } + + let client = Client { + handle: RefCell::new(Easy::new()), + driver_addr, + }; + shell.status("Starting new webdriver session..."); + // Allocate a new session with the webdriver protocol, and once we've done + // so schedule the browser to get closed with a call to `close_window`. + let id = client.new_session(&driver)?; + let _f = OnDrop(|| { + if let Err(e) = client.close_window(&id) { + warn!("failed to close window {:?}", e); + } + }); + + // Visit our local server to open up the page that runs tests, and then get + // some handles to objects on the page which we'll be scraping output from. + let url = format!("http://{}", server); + shell.status(&format!("Visiting {}...", url)); + client.goto(&id, &url)?; + shell.status("Loading page elements..."); + let output = client.element(&id, "#output")?; + let logs = client.element(&id, "#console_log")?; + let errors = client.element(&id, "#console_error")?; + + // At this point we need to wait for the test to finish before we can take a + // look at what happened. There appears to be no great way to do this with + // the webdriver protocol today (in terms of synchronization), so for now we + // just go with a loop. + // + // We periodically check the page to see if the output contains a known + // string to only be printed when tests have finished running. + // + // TODO: harness failures aren't well handled here, they always force a + // timeout. These sorts of failures could be "you typo'd the path to a + // local script" which is pretty bad to time out for, we should detect + // this on the page and look for such output here, printing diagnostic + // information. + shell.status("Waiting for test to finish..."); + let start = Instant::now(); + let max = Duration::new(20, 0); + while start.elapsed() < max { + if client.text(&id, &output)?.contains("test result: ") { + break + } + thread::sleep(Duration::from_millis(100)); + } + shell.clear(); + + // Tests have now finished or have timed out. At this point we need to print + // what happened on the console. Currently we just do this by scraping the + // output of various fields and printing them out, hopefully providing + // enough diagnostic info to see what went wrong (if anything). + let output = client.text(&id, &output)?; + let logs = client.text(&id, &logs)?; + let errors = client.text(&id, &errors)?; + + if output.contains("test result: ") { + println!("{}", output); + + // If the tests harness finished (either successfully or unsuccessfully) + // then in theory all the info needed to debug the failure is in its own + // output, so we shouldn't need the driver logs to get printed. + print_driver_stdio.set(false); + } else { + println!("failed to detect test as having been run"); + if output.len() > 0 { + println!("output div contained:\n{}", tab(&output)); + } + } + if logs.len() > 0 { + println!("console.log div contained:\n{}", tab(&logs)); + } + if errors.len() > 0 { + println!("console.log div contained:\n{}", tab(&errors)); + } + + Ok(()) +} + +enum Driver { + Gecko(PathBuf), + Safari(PathBuf), + Chrome(PathBuf), +} + +impl Driver { + fn find() -> Result<(Driver, Vec), Error> { + let env_args = |name: &str| { + env::var(format!("{}_ARGS", name.to_uppercase())) + .unwrap_or_default() + .split_whitespace() + .map(|s| s.to_string()) + .collect::>() + }; + + let drivers = [ + ("geckodriver", Driver::Gecko as fn(PathBuf) -> Driver), + ("safaridriver", Driver::Safari as fn(PathBuf) -> Driver), + ("chromedriver", Driver::Chrome as fn(PathBuf) -> Driver), + ]; + + // First up, if env vars like GECKODRIVER are present, use those to + // allow forcing usage of a particular driver. + for (driver, ctor) in drivers.iter() { + let env = driver.to_uppercase(); + let path = match env::var_os(&env) { + Some(path) => path, + None => continue, + }; + return Ok((ctor(path.into()), env_args(driver))) + } + + // Next, check PATH. If we can find any supported driver, use that by + // default. + for path in env::split_paths(&env::var_os("PATH").unwrap_or_default()) { + let found = drivers + .iter() + .find(|(name, _)| { + path.join(name) + .with_extension(env::consts::EXE_EXTENSION) + .exists() + }); + let (name, ctor) = match found { + Some(p) => p, + None => continue, + }; + return Ok((ctor(name.into()), env_args(name))) + } + + // TODO: download an appropriate driver? How to know which one to + // download? + + bail!("failed to find a suitable WebDriver binary to drive headless \ + testing") + } + + fn path(&self) -> &Path { + match self { + Driver::Gecko(path) => path, + Driver::Safari(path) => path, + Driver::Chrome(path) => path, + } + } + + fn browser(&self) -> &str { + match self { + Driver::Gecko(_) => "Firefox", + Driver::Safari(_) => "Safari", + Driver::Chrome(_) => "Chrome", + } + } +} + +struct Client { + handle: RefCell, + driver_addr: SocketAddr, +} + +enum Method<'a> { + Get, + Post(&'a str), + Delete, +} + +impl Client { + fn new_session(&self, driver: &Driver) -> Result { + match driver { + Driver::Gecko(_) => { + #[derive(Deserialize)] + struct Response { + value: ResponseValue, + } + + #[derive(Deserialize)] + struct ResponseValue { + #[serde(rename = "sessionId")] + session_id: String, + } + let request = json!({ + "capabilities": { + "alwaysMatch": { + "moz:firefoxOptions": { + "args": ["-headless"], + } + } + } + }); + let x: Response = self.post("/session", &request)?; + Ok(x.value.session_id) + } + Driver::Safari(_) => { + #[derive(Deserialize)] + struct Response { + #[serde(rename = "sessionId")] + session_id: String, + } + let request = json!({ + "desiredCapabilities": { + } + }); + let x: Response = self.post("/session", &request)?; + Ok(x.session_id) + } + Driver::Chrome(_) => { + #[derive(Deserialize)] + struct Response { + #[serde(rename = "sessionId")] + session_id: String, + } + let request = json!({ + "desiredCapabilities": { + } + }); + let x: Response = self.post("/session", &request)?; + Ok(x.session_id) + } + } + } + + fn close_window(&self, id: &str) -> Result<(), Error> { + #[derive(Deserialize)] + struct Response { + } + let x: Response = self.delete(&format!("/session/{}/window", id))?; + drop(x); + Ok(()) + } + + fn goto(&self, id: &str, url: &str) -> Result<(), Error> { + #[derive(Serialize)] + struct Request { + url: String, + } + #[derive(Deserialize)] + struct Response { + } + + let request = Request { + url: url.to_string(), + }; + let x: Response = self.post(&format!("/session/{}/url", id), &request)?; + drop(x); + Ok(()) + } + + fn element(&self, id: &str, selector: &str) -> Result { + #[derive(Serialize)] + struct Request { + using: String, + value: String, + } + #[derive(Deserialize)] + struct Response { + value: Reference, + } + #[derive(Deserialize)] + struct Reference { + #[serde(rename = "element-6066-11e4-a52e-4f735466cecf")] + gecko_reference: Option, + #[serde(rename = "ELEMENT")] + safari_reference: Option, + } + + let request = Request { + using: "css selector".to_string(), + value: selector.to_string(), + }; + let x: Response = self.post(&format!("/session/{}/element", id), &request)?; + Ok(x.value.gecko_reference + .or(x.value.safari_reference) + .ok_or(format_err!("failed to find element reference in response"))?) + + } + + fn text(&self, id: &str, element: &str) -> Result { + #[derive(Deserialize)] + struct Response { + value: String, + } + let x: Response = self.get(&format!("/session/{}/element/{}/text", id, element))?; + Ok(x.value) + } + + fn get(&self, path: &str) -> Result + where U: for<'a> Deserialize<'a>, + { + debug!("GET {}", path); + let result = self.doit(path, Method::Get)?; + Ok(serde_json::from_str(&result)?) + } + + fn post(&self, path: &str, data: &T) -> Result + where T: Serialize, + U: for<'a> Deserialize<'a>, + { + let input = serde_json::to_string(data)?; + debug!("POST {} {}", path, input); + let result = self.doit(path, Method::Post(&input))?; + Ok(serde_json::from_str(&result)?) + } + + fn delete(&self, path: &str) -> Result + where U: for<'a> Deserialize<'a>, + { + debug!("DELETE {}", path); + let result = self.doit(path, Method::Delete)?; + Ok(serde_json::from_str(&result)?) + } + + fn doit(&self, path: &str, method: Method) -> Result { + let url = format!("http://{}{}", self.driver_addr, path); + let mut handle = self.handle.borrow_mut(); + handle.reset(); + handle.url(&url)?; + match method { + Method::Post(data) => { + handle.post(true)?; + handle.post_fields_copy(data.as_bytes())?; + } + Method::Delete => handle.custom_request("DELETE")?, + Method::Get => handle.get(true)?, + } + let mut result = Vec::new(); + { + let mut t = handle.transfer(); + t.write_function(|buf| { + result.extend_from_slice(buf); + Ok(buf.len()) + })?; + t.perform()? + } + let result = String::from_utf8_lossy(&result); + if handle.response_code()? != 200 { + bail!("non-200 response code: {}\n{}", handle.response_code()?, result); + } + debug!("got: {}", result); + Ok(result.into_owned()) + } +} + +fn read(r: &mut R) -> io::Result> { + let mut dst = Vec::new(); + r.read_to_end(&mut dst)?; + Ok(dst) +} + +fn tab(s: &str) -> String { + let mut result = String::new(); + for line in s.lines() { + result.push_str(" "); + result.push_str(line); + result.push_str("\n"); + } + return result; +} + +struct OnDrop(F); + +impl Drop for OnDrop { + fn drop(&mut self) { + (self.0)(); + } +} diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html new file mode 100644 index 00000000..566bcf70 --- /dev/null +++ b/crates/cli/src/bin/wasm-bindgen-test-runner/index-headless.html @@ -0,0 +1,37 @@ + + + + + +
Loading scripts...
+

+    

+    
+    
+  
+
diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs
index c316f475..33f8d597 100644
--- a/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs
+++ b/crates/cli/src/bin/wasm-bindgen-test-runner/main.rs
@@ -1,23 +1,35 @@
+extern crate curl;
+extern crate env_logger;
 #[macro_use]
 extern crate failure;
+#[macro_use]
+extern crate log;
 extern crate parity_wasm;
 extern crate rouille;
+extern crate serde;
+#[macro_use]
+extern crate serde_derive;
+#[macro_use]
+extern crate serde_json;
 extern crate wasm_bindgen_cli_support;
 
 use std::env;
 use std::fs;
-use std::io::{self, Write};
 use std::path::PathBuf;
 use std::process;
+use std::thread;
 
 use failure::{ResultExt, Error};
-use parity_wasm::elements::{Module, Deserialize};
+use parity_wasm::elements::{Module, Deserialize, Section};
 use wasm_bindgen_cli_support::Bindgen;
 
+mod headless;
 mod node;
 mod server;
+mod shell;
 
 fn main() {
+    env_logger::init();
     let err = match rmain() {
         Ok(()) => return,
         Err(e) => e,
@@ -31,6 +43,7 @@ fn main() {
 
 fn rmain() -> Result<(), Error> {
     let mut args = env::args_os().skip(1);
+    let shell = shell::Shell::new();
 
     // Currently no flags are supported, and assume there's only one argument
     // which is the wasm file to test. This'll want to improve over time!
@@ -61,10 +74,6 @@ fn rmain() -> Result<(), Error> {
     // Collect all tests that the test harness is supposed to run. We assume
     // that any exported function with the prefix `__wbg_test` is a test we need
     // to execute.
-    //
-    // Note that we're collecting *JS objects* that represent the functions to
-    // execute, and then those objects are passed into wasm for it to execute
-    // when it sees fit.
     let wasm = fs::read(&wasm_file_to_test)
         .context("failed to read wasm file")?;
     let wasm = Module::deserialize(&mut &wasm[..])
@@ -78,18 +87,34 @@ fn rmain() -> Result<(), Error> {
             tests.push(export.field().to_string());
         }
     }
+
+    // Right now there's a bug where if no tests are present then the
+    // `wasm-bindgen-test` runtime support isn't linked in, so just bail out
+    // early saying everything is ok.
     if tests.len() == 0 {
         println!("no tests to run!");
         return Ok(())
     }
 
-    let node = true;
+    // Figure out if this tests is supposed to execute in node.js or a browser.
+    // That's done on a per-test-binary basis with the
+    // `wasm_bindgen_test_configure` macro, which emits a custom section for us
+    // to read later on.
+    let mut node = true;
+    for section in wasm.sections() {
+        let custom = match section {
+            Section::Custom(section) => section,
+            _ => continue,
+        };
+        if custom.name() != "__wasm_bindgen_test_unstable" {
+            continue
+        }
+        node = !custom.payload().contains(&0x01);
+    }
+    let headless = env::var("CI").is_ok();
 
-    print!("Executing bindgen ...\r");
-    io::stdout().flush()?;
-
-    // For now unconditionally generate wasm-bindgen code tailored for node.js,
-    // but eventually we'll want more options here for browsers!
+    // Make the generated bindings available for the tests to execute against.
+    shell.status("Executing bindgen...");
     let mut b = Bindgen::new();
     b.debug(true)
         .nodejs(node)
@@ -97,13 +122,37 @@ fn rmain() -> Result<(), Error> {
         .keep_debug(false)
         .generate(&tmpdir)
         .context("executing `wasm-bindgen` over the wasm file")?;
+    shell.clear();
 
-    print!("                     \r");
-    io::stdout().flush()?;
-
+    // If we're executing in node.js, that module will take it from here.
     if node {
         return node::execute(&module, &tmpdir, &args.collect::>(), &tests)
     }
 
-    server::spawn(&module, &tmpdir, &args.collect::>(), &tests)
+    // Otherwise we're executing in a browser. Spawn a server which serves up
+    // the local generated files over an HTTP server.
+    let srv = server::spawn(
+        &if headless {
+            "127.0.0.1:0".parse().unwrap()
+        } else {
+            "127.0.0.1:8000".parse().unwrap()
+        },
+        headless,
+        &module,
+        &tmpdir,
+        &args.collect::>(),
+        &tests,
+    )?;
+    let addr = srv.server_addr();
+
+    // TODO: eventually we should provide the ability to exit at some point
+    // (gracefully) here, but for now this just runs forever.
+    if !headless {
+        println!("Running server on http://{}", addr);
+        return Ok(srv.run())
+    }
+
+    thread::spawn(|| srv.run());
+    headless::run(&addr, &shell)?;
+    Ok(())
 }
diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs
index 75c01d65..d35d500c 100644
--- a/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs
+++ b/crates/cli/src/bin/wasm-bindgen-test-runner/node.rs
@@ -66,7 +66,6 @@ pub fn execute(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String])
     for test in tests {
         js_to_execute.push_str(&format!("tests.push('{}')\n", test));
     }
-
     // And as a final addendum, exit with a nonzero code if any tests fail.
     js_to_execute.push_str("
         main(tests)
diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs
index 419a3f7a..0314e739 100644
--- a/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs
+++ b/crates/cli/src/bin/wasm-bindgen-test-runner/server.rs
@@ -1,14 +1,20 @@
 use std::ffi::OsString;
 use std::path::Path;
 use std::fs;
+use std::net::SocketAddr;
 
 use failure::{ResultExt, Error};
-use rouille::{self, Response, Request};
+use rouille::{self, Response, Request, Server};
 use wasm_bindgen_cli_support::wasm2es6js::Config;
 
-pub fn spawn(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String])
-    -> Result<(), Error>
-{
+pub fn spawn(
+    addr: &SocketAddr,
+    headless: bool,
+    module: &str,
+    tmpdir: &Path,
+    args: &[OsString],
+    tests: &[String],
+) -> Result Response + Send + Sync>, Error> {
     let mut js_to_execute = format!(r#"
         import {{ Context }} from './{0}';
         import * as wasm from './{0}_bg';
@@ -55,12 +61,16 @@ pub fn spawn(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String])
         .context("failed to write JS file")?;
 
     // For now, always run forever on this port. We may update this later!
-    println!("Listening on port 8000");
     let tmpdir = tmpdir.to_path_buf();
-    rouille::start_server("localhost:8000", move |request| {
+    let srv = Server::new(addr, move |request| {
         // The root path gets our canned `index.html`
         if request.url() == "/" {
-            return Response::from_data("text/html", include_str!("index.html"));
+            let s = if headless {
+                include_str!("index-headless.html")
+            } else {
+                include_str!("index.html")
+            };
+            return Response::from_data("text/html", s)
         }
 
         // Otherwise we need to find the asset here. It may either be in our
@@ -74,7 +84,8 @@ pub fn spawn(module: &str, tmpdir: &Path, args: &[OsString], tests: &[String])
         // header?)
         response.headers.retain(|(k, _)| k != "Cache-Control");
         return response
-    });
+    }).map_err(|e| format_err!("{}", e))?;
+    return Ok(srv);
 
     fn try_asset(request: &Request, dir: &Path) -> Response {
         let response = rouille::match_assets(request, dir);
diff --git a/crates/cli/src/bin/wasm-bindgen-test-runner/shell.rs b/crates/cli/src/bin/wasm-bindgen-test-runner/shell.rs
new file mode 100644
index 00000000..6f1d0f77
--- /dev/null
+++ b/crates/cli/src/bin/wasm-bindgen-test-runner/shell.rs
@@ -0,0 +1,32 @@
+const WIDTH: usize = 50;
+
+use std::io::{self, Write};
+
+pub struct Shell {
+}
+
+impl Shell {
+    pub fn new() -> Shell {
+        Shell {}
+    }
+
+    pub fn status(&self, s: &str) {
+        let s = if s.len() > WIDTH {
+            &s[..WIDTH]
+        } else {
+            s
+        };
+        print!("{:<1$}\r", s, WIDTH);
+        io::stdout().flush().unwrap();
+    }
+
+    pub fn clear(&self) {
+        self.status("");
+    }
+}
+
+impl Drop for Shell {
+    fn drop(&mut self) {
+        self.clear();
+    }
+}
diff --git a/crates/js-sys/tests/headless.js b/crates/js-sys/tests/headless.js
new file mode 100644
index 00000000..ba93a091
--- /dev/null
+++ b/crates/js-sys/tests/headless.js
@@ -0,0 +1,3 @@
+export function is_array_values_supported() {
+  return typeof Array.prototype.values === 'function';
+}
diff --git a/crates/js-sys/tests/headless.rs b/crates/js-sys/tests/headless.rs
index 6bc28f4a..139c1358 100755
--- a/crates/js-sys/tests/headless.rs
+++ b/crates/js-sys/tests/headless.rs
@@ -1,61 +1,48 @@
-#![cfg(not(target_arch = "wasm32"))]
-#![allow(non_snake_case)]
+#![feature(use_extern_macros)]
+#![cfg(target_arch = "wasm32")]
 
-extern crate wasm_bindgen_test_project_builder as project_builder;
+extern crate wasm_bindgen_test;
+extern crate wasm_bindgen;
+extern crate js_sys;
 
-fn project() -> project_builder::Project {
-    let mut p = project_builder::project();
-    p.add_local_dependency("js-sys", env!("CARGO_MANIFEST_DIR"));
-    return p
+use wasm_bindgen::prelude::*;
+use wasm_bindgen_test::*;
+use js_sys::Array;
+
+wasm_bindgen_test_configure!(run_in_browser);
+
+#[wasm_bindgen(module = "./tests/headless.js")]
+extern {
+    fn is_array_values_supported()-> bool;
 }
 
-// NB: currently this older test suite is only used for tests which require
-// headless browser support, otherwise all new tests should go in the `wasm`
-// test suite next to this one.
+#[wasm_bindgen]
+extern {
+    type ValuesIterator;
+    #[wasm_bindgen(method, structural)]
+    fn next(this: &ValuesIterator) -> IterNext;
 
-#[test]
-fn ArrayIterator_values() {
-    let mut project = project();
-    project.file(
-            "src/lib.rs",
-            r#"
-            #![feature(use_extern_macros)]
+    type IterNext;
 
-            extern crate wasm_bindgen;
-            extern crate js_sys;
-            use wasm_bindgen::prelude::*;
-
-            #[wasm_bindgen]
-            pub fn get_values(this: &js_sys::Array) -> js_sys::Iterator {
-                this.values()
-            }
-        "#,
-        )
-        .file(
-            "test.js",
-            r#"
-            import * as assert from "assert";
-            import * as wasm from "./out";
-
-            export function test() {
-                if (typeof Array.prototype.values !== "function") {
-                    return;
-                }
-
-                let numbers = [8, 3, 2];
-                let wasmIterator = wasm.get_values(numbers);
-
-                assert.equal(wasmIterator.next().value, 8);
-                assert.equal(wasmIterator.next().value, 3);
-                assert.equal(wasmIterator.next().value, 2);
-                assert.ok(wasmIterator.next().done);
-            }
-        "#,
-        );
-
-    let mut headless = project.clone();
-    headless.headless(true);
-
-    project.test();
-    headless.test();
+    #[wasm_bindgen(method, getter, structural)]
+    fn value(this: &IterNext) -> JsValue;
+    #[wasm_bindgen(method, getter, structural)]
+    fn done(this: &IterNext) -> bool;
+}
+
+#[wasm_bindgen_test]
+fn array_iterator_values() {
+    if !is_array_values_supported() {
+        return
+    }
+    let array = Array::new();
+    array.push(&8.into());
+    array.push(&3.into());
+    array.push(&2.into());
+    let iter = ValuesIterator::from(JsValue::from(array.values()));
+
+    assert_eq!(iter.next().value(), 8);
+    assert_eq!(iter.next().value(), 3);
+    assert_eq!(iter.next().value(), 2);
+    assert!(iter.next().done());
 }
diff --git a/crates/js-sys/tests/wasm/Function.rs b/crates/js-sys/tests/wasm/Function.rs
index 999debfd..02e9865f 100644
--- a/crates/js-sys/tests/wasm/Function.rs
+++ b/crates/js-sys/tests/wasm/Function.rs
@@ -11,7 +11,7 @@ extern {
     #[wasm_bindgen(method, getter, structural)]
     pub fn push(this: &ArrayPrototype) -> Function;
     #[wasm_bindgen(js_name = prototype, js_namespace = Array)]
-    static ARRAY_PROTOTYPE: ArrayPrototype;
+    static ARRAY_PROTOTYPE2: ArrayPrototype;
 }
 
 #[wasm_bindgen_test]
@@ -25,7 +25,7 @@ fn apply() {
     let arr = JsValue::from(Array::new());
     let args = Array::new();
     args.push(&1.into());
-    ARRAY_PROTOTYPE.push().apply(&arr, &args).unwrap();
+    ARRAY_PROTOTYPE2.push().apply(&arr, &args).unwrap();
     assert_eq!(Array::from(&arr).length(), 1);
 }
 
@@ -47,13 +47,13 @@ fn bind() {
 #[wasm_bindgen_test]
 fn length() {
     assert_eq!(MAX.length(), 2);
-    assert_eq!(ARRAY_PROTOTYPE.push().length(), 1);
+    assert_eq!(ARRAY_PROTOTYPE2.push().length(), 1);
 }
 
 #[wasm_bindgen_test]
 fn name() {
     assert_eq!(JsValue::from(MAX.name()), "max");
-    assert_eq!(JsValue::from(ARRAY_PROTOTYPE.push().name()), "push");
+    assert_eq!(JsValue::from(ARRAY_PROTOTYPE2.push().name()), "push");
 }
 
 #[wasm_bindgen_test]
diff --git a/crates/test/src/lib.rs b/crates/test/src/lib.rs
index 25191d28..10c4900c 100644
--- a/crates/test/src/lib.rs
+++ b/crates/test/src/lib.rs
@@ -20,6 +20,31 @@ macro_rules! console_log {
     )
 }
 
+/// A macro used to configured how this test is executed by the
+/// `wasm-bindgen-test-runner` harness.
+///
+/// This macro is invoked as:
+///
+///     wasm_bindgen_test_configure!(foo bar baz);
+///
+/// where all of `foo`, `bar`, and `baz`, would be recognized options to this
+/// macro. The currently known options to this macro are:
+///
+/// * `run_in_browser` - requires that this test is run in a browser rather than
+///   node.js, which is the default for executing tests.
+///
+/// This macro may be invoked at most one time per test suite.
+#[macro_export]
+macro_rules! wasm_bindgen_test_configure {
+    (run_in_browser $($others:tt)*) => (
+        #[link_section = "__wasm_bindgen_test_unstable"]
+        #[cfg(target_arch = "wasm32")]
+        pub static __WBG_TEST_RUN_IN_BROWSER: [u8; 1] = [0x01];
+        wasm_bindgen_test_configure!($($others)*);
+    );
+    () => ()
+}
+
 #[path = "rt/mod.rs"]
 #[doc(hidden)]
 pub mod __rt;