├── README.md ├── driverclient.js └── driver.js /README.md: -------------------------------------------------------------------------------- 1 | # Self-webdriver 2 | 3 | A nodejs shim and client-side library from driving a webdriver[1] from 4 | the browser. 5 | 6 | [1]: http://seleniumhq.org/projects/webdriver/ 7 | 8 | Why'd anybody want to do that? 9 | 10 | It makes testing client-side libraries somewhat less cumbersome. 11 | Testing logic and set-up can live client-side, calling out to the 12 | webdriver when it needs to test a click or other hard-to-fake input. 13 | 14 | Use-case is CodeMirror[2], which has to do some truly scary things to 15 | properly capture input. Testing that cross-browser has been a bit of a 16 | nightmare, this library is step one in getting automated tests off the 17 | ground. 18 | 19 | [2]: http://codemirror.net 20 | 21 | The API of this library is undocumented and in flux. Its current state 22 | is "You *might* be able to use this or take inspiration from it, but 23 | please don't expect it to be mature or stable." 24 | -------------------------------------------------------------------------------- /driverclient.js: -------------------------------------------------------------------------------- 1 | var driver = function() { 2 | function req(method, path, data, c, fail) { 3 | if (!fail) fail = function(msg) { console.log("Request " + method + " " + path + " failed: " + msg); }; 4 | var xhr = new XMLHttpRequest(); 5 | xhr.open(method, "/driver" + path, true); 6 | xhr.setRequestHeader("Content-Type", "application/json; charset=UTF-8"); 7 | console.log("send " + method + " " + path); 8 | xhr.onreadystatechange = function() { 9 | if (xhr.readyState == 4) { 10 | console.log("recv " + method + " " + path); 11 | if (xhr.status < 400) { 12 | var data = xhr.responseText, end = data.length; 13 | while (data.charAt(end - 1) == "\x00") --end; 14 | if (end != data.length) data = data.slice(0, end); 15 | c(data.length ? JSON.parse(data) : null); 16 | } else { 17 | var text = "No response"; 18 | try {text = xhr.responseText;} catch(e){} 19 | fail(text); 20 | } 21 | } 22 | }; 23 | xhr.send(data ? JSON.stringify(data) : null); 24 | } 25 | 26 | function get(path, c) { req("GET", path, null, c); } 27 | function post(path, data, c) { req("POST", path, data, c); } 28 | function del(path, c) { req("DELETE", path, null, c); } 29 | 30 | function asElement(elt, c) { 31 | if (elt == null) return c(null); 32 | if (elt.driverID) return c(elt.driverID); 33 | var id = elt.id; 34 | if (!id) id = elt.id = "tag_" + Math.floor(Math.random() * 0xffffffff).toString(16); 35 | post("/element", {using: "id", value: id}, function(data) { 36 | elt.driverID = data.value.ELEMENT; 37 | c(data.value.ELEMENT); 38 | }); 39 | } 40 | 41 | function driver() { 42 | if (this == window) return new driver(); 43 | this.queue = []; 44 | } 45 | driver.prototype = { 46 | run: function(c) { 47 | var cur = this.queue.shift(), self = this; 48 | if (cur) cur(function() { self.run(c); }); 49 | else if (c) c(); 50 | }, 51 | add: function(f) { this.queue.push(f); return this; }, 52 | then: function(f) { 53 | return this.add(function(c) { f(); c(); }); 54 | }, 55 | keys: function(seq) { 56 | return this.add(function(c) { post("/keys", {value: [seq]}, c); }); 57 | }, 58 | moveTo: function(x, y, elt) { 59 | return this.add(function(c) { 60 | asElement(elt, function(id) { 61 | post("/moveto", {element: id, xoffset: x, yoffset: y}, c); 62 | }); 63 | }); 64 | }, 65 | click: function(button) { 66 | return this.add(function(c) { post("/click", {button: button || 0}, c); }); 67 | }, 68 | mouseDown: function() { 69 | return this.add(function(c) { post("/buttondown", {}, c); }); 70 | }, 71 | mouseUp: function() { 72 | return this.add(function(c) { post("/buttonup", {}, c); }); 73 | }, 74 | doubleClick: function() { 75 | return this.add(function(c) { post("/doubleclick", {}, c); }); 76 | } 77 | }; 78 | 79 | return driver; 80 | }(); 81 | -------------------------------------------------------------------------------- /driver.js: -------------------------------------------------------------------------------- 1 | var child = require("child_process"); 2 | var httpProxy = require("http-proxy"); 3 | var http = require("http"); 4 | 5 | var driverPort = Math.ceil(Math.random() * 20000) + 10000; 6 | var usePort = null; 7 | var useBrowser = "firefox"; 8 | var proxyPort = Math.ceil(Math.random() * 20000) + 10000; 9 | var targetFile = "index.html"; 10 | 11 | for (var i = 2; i < process.argv.length; ++i) { 12 | var arg = process.argv[i]; 13 | if (arg == "--port" && ++i < process.argv.length) proxyPort = Number(process.argv[i]); 14 | else if (arg == "--rundriver" && ++i < process.argv.length) driverPort = Number(process.argv[i]); 15 | else if (arg == "--driver" && ++i < process.argv.length) usePort = Number(process.argv[i]); 16 | else if (arg == "--browser" && ++i < process.argv.length) useBrowser = process.argv[i]; 17 | else targetFile = arg; 18 | } 19 | 20 | if (usePort != null) 21 | initSession(usePort, useBrowser, "/wd/hub"); 22 | else 23 | startOwnDriver(driverPort); 24 | 25 | function startOwnDriver(port) { 26 | var driver = child.spawn("./chromedriver", ["--port=" + port]); 27 | driver.stdout.on("data", function(x) { console.log("driver: " + x); }); 28 | driver.on("exit", function(msg) { 29 | abort("failed to start driver: " + msg); 30 | }); 31 | process.on("exit", function() { driver.kill(); }); 32 | setTimeout(initSession.bind(null, port, "chrome"), 300); 33 | } 34 | 35 | function initSession(port, browserName, prefix) { 36 | var args = {desiredCapabilities: {browserName: browserName, javascriptEnabled: true}, 37 | sessionId: null}; 38 | sendToDriver(port, (prefix || "") + "/session", args, function(resp) { 39 | startServing(port, resp.headers.location); 40 | }); 41 | } 42 | 43 | function sendToDriver(port, path, data, c) { 44 | data = JSON.stringify(data); 45 | var opts = {port: port, 46 | path: path, 47 | headers: {"Content-Type": "application/json; charset=UTF-8", 48 | "Content-Length": data.length}, 49 | method: "POST"}; 50 | var req = http.request(opts, function(resp) { 51 | var data = ""; 52 | resp.on("data", function(d) { data += d.toString(); }); 53 | resp.on("end", function() { 54 | if (resp.statusCode >= 400) 55 | abort("request to " + path + " failed: " + resp.statusCode + ": " + data); 56 | c(resp, data); 57 | }); 58 | }); 59 | req.on("error", function(msg) { 60 | abort("could not init session: " + msg); 61 | }); 62 | req.write(data); 63 | req.end(); 64 | } 65 | 66 | function startServing(port, sessionPath) { 67 | var files = new (require("node-static").Server)("."); 68 | var proxy = new httpProxy.RoutingProxy(); 69 | http.createServer(function(req, resp) { 70 | var url = require("url").parse(req.url); 71 | var m = url.path.match(/^\/driver\b(.*)$/); 72 | if (m) { 73 | req.url = req.url.replace(/\/driver\b/, sessionPath); 74 | // Kludge to work around bad interpretation of headers by Chrome driver 75 | req.headers["Content-Length"] = req.headers["content-length"]; 76 | proxy.proxyRequest(req, resp, {host: "localhost", port: port}); 77 | } else { 78 | req.addListener("end", function() {files.serve(req, resp);}); 79 | } 80 | }).listen(proxyPort); 81 | moveToPage(port, sessionPath); 82 | } 83 | 84 | function moveToPage(port, sessionPath) { 85 | sendToDriver(port, sessionPath + "/url", 86 | {url: "http://localhost:" + proxyPort + "/" + targetFile}, function() {}); 87 | } 88 | 89 | function abort(msg) { 90 | console.log("abort: " + msg); 91 | process.exit(1); 92 | } 93 | --------------------------------------------------------------------------------