├── index.js ├── test ├── pages │ ├── test-network.json │ ├── stylesheet1.css │ ├── logs.html │ ├── stylesheets.html │ ├── index.html │ ├── dom.html │ └── network.html ├── server.js ├── test-raw.js ├── utils.js ├── README.md ├── test-console.js ├── test-network.js ├── test-logs.js ├── test-jsobject.js ├── test-stylesheets.js └── test-dom.js ├── lib ├── memory.js ├── extend.js ├── simulator.js ├── device.js ├── tab.js ├── jsobject.js ├── network.js ├── client-methods.js ├── console.js ├── dom.js ├── browser.js ├── stylesheets.js ├── domnode.js ├── webapps.js └── client.js ├── package.json ├── test.js └── README.md /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/browser"); -------------------------------------------------------------------------------- /test/pages/test-network.json: -------------------------------------------------------------------------------- 1 | { 2 | "a": 2, 3 | "b": "hello" 4 | } -------------------------------------------------------------------------------- /test/pages/stylesheet1.css: -------------------------------------------------------------------------------- 1 | main { 2 | font-family: Georgia, sans-serif; 3 | color: black; 4 | } 5 | 6 | * { 7 | padding: 0; 8 | margin: 0; 9 | } -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var path = require("path"), 2 | connect = require('connect'); 3 | 4 | var port = 3000; 5 | 6 | connect.createServer(connect.static(path.join(__dirname, "pages"))).listen(port); 7 | 8 | console.log("visit:\nhttp://127.0.0.1:" + port + "/index.html"); -------------------------------------------------------------------------------- /test/pages/logs.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logs tests 5 | 6 | 7 | 12 | 13 | 14 |
15 |
16 | 17 | -------------------------------------------------------------------------------- /test/pages/stylesheets.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Stylesheets tests 5 | 6 | 11 | 12 | 13 | 14 |
15 |
16 |
Stylesheet Tests
17 |
18 |
19 | 20 | -------------------------------------------------------------------------------- /lib/memory.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"); 3 | 4 | module.exports = Memory; 5 | 6 | function Memory(client, actor) { 7 | this.initialize(client, actor); 8 | } 9 | 10 | Memory.prototype = extend(ClientMethods, { 11 | measure: function(cb) { 12 | this.request('measure', function (err, resp) { 13 | cb(err, resp); 14 | }); 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /lib/extend.js: -------------------------------------------------------------------------------- 1 | module.exports = function extend(prototype, properties) { 2 | return Object.create(prototype, getOwnPropertyDescriptors(properties)); 3 | } 4 | 5 | function getOwnPropertyDescriptors(object) { 6 | var names = Object.getOwnPropertyNames(object); 7 | 8 | return names.reduce(function(descriptor, name) { 9 | descriptor[name] = Object.getOwnPropertyDescriptor(object, name); 10 | return descriptor; 11 | }, {}); 12 | } -------------------------------------------------------------------------------- /test/pages/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Firefox remote debugging client tests 5 | 6 | 14 | 15 | 16 |
17 | Firefox Client Tests 18 |
19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /test/pages/dom.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | DOM tests 5 | 6 | 7 | 8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 19 | -------------------------------------------------------------------------------- /test/pages/network.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Logs tests 5 | 6 | 7 | 17 | 18 | 19 |
20 |
21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "firefox-client", 3 | "description": "Firefox remote debugging client", 4 | "version": "0.3.0", 5 | "author": "Heather Arthur ", 6 | "main": "index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "http://github.com/harthur/firefox-client.git" 10 | }, 11 | "dependencies": { 12 | "colors": "0.5.x", 13 | "js-select": "~0.6.0" 14 | }, 15 | "devDependencies": { 16 | "connect": "~2.8.2", 17 | "mocha": "~1.12.0" 18 | }, 19 | "keywords": [ 20 | "firefox", 21 | "debugger", 22 | "remote debugging" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /lib/simulator.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"), 3 | Tab = require("./tab"); 4 | 5 | module.exports = SimulatorApps; 6 | 7 | function SimulatorApps(client, actor) { 8 | this.initialize(client, actor); 9 | } 10 | 11 | SimulatorApps.prototype = extend(ClientMethods, { 12 | listApps: function(cb) { 13 | this.request('listApps', function(resp) { 14 | var apps = []; 15 | for (var url in resp.apps) { 16 | var app = resp.apps[url]; 17 | apps.push(new Tab(this.client, app)); 18 | } 19 | return apps; 20 | }.bind(this), cb); 21 | } 22 | }) 23 | -------------------------------------------------------------------------------- /test/test-raw.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | FirefoxClient = require("../index"); 3 | 4 | var client = new FirefoxClient(); 5 | 6 | before(function(done) { 7 | client.connect(function() { 8 | done(); 9 | }) 10 | }); 11 | 12 | describe('makeRequest()', function() { 13 | it('should do listTabs request', function(done) { 14 | var message = { 15 | to: 'root', 16 | type: 'listTabs' 17 | }; 18 | 19 | client.client.makeRequest(message, function(resp) { 20 | assert.equal(resp.from, "root"); 21 | assert.ok(resp.tabs); 22 | assert.ok(resp.profilerActor) 23 | done(); 24 | }) 25 | }) 26 | }) -------------------------------------------------------------------------------- /lib/device.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"); 3 | 4 | module.exports = Device; 5 | 6 | function Device(client, tab) { 7 | this.initialize(client, tab.deviceActor); 8 | } 9 | 10 | Device.prototype = extend(ClientMethods, { 11 | getDescription: function(cb) { 12 | this.request("getDescription", function(err, resp) { 13 | if (err) { 14 | return cb(err); 15 | } 16 | 17 | cb(null, resp.value); 18 | }); 19 | }, 20 | getRawPermissionsTable: function(cb) { 21 | this.request("getRawPermissionsTable", function(err, resp) { 22 | if (err) { 23 | return cb(err); 24 | } 25 | 26 | cb(null, resp.value.rawPermissionsTable); 27 | }); 28 | } 29 | }) 30 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | FirefoxClient = require("../index"); 3 | 4 | var tab; 5 | 6 | exports.loadTab = function(url, callback) { 7 | getFirstTab(function(tab) { 8 | tab.navigateTo(url); 9 | 10 | tab.once("navigate", function() { 11 | callback(tab); 12 | }); 13 | }) 14 | }; 15 | 16 | 17 | function getFirstTab(callback) { 18 | if (tab) { 19 | return callback(tab); 20 | } 21 | var client = new FirefoxClient({log: true}); 22 | 23 | client.connect(function() { 24 | client.listTabs(function(err, tabs) { 25 | if (err) throw err; 26 | 27 | tab = tabs[0]; 28 | 29 | tab.attach(function(err) { 30 | if (err) throw err; 31 | callback(tab); 32 | }) 33 | }); 34 | }); 35 | } -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # testing 2 | 3 | ### dependencies 4 | To run the tests in this directory, first install the dev dependencies with this command from the top-level directory: 5 | 6 | ``` 7 | npm install --dev 8 | ``` 9 | 10 | You'll also have to globally install [mocha](http://visionmedia.github.io/mocha). `npm install mocha -g`. 11 | 12 | ### running 13 | First open up a [Firefox Nightly build](http://nightly.mozilla.org/) and serve the test files up: 14 | 15 | ``` 16 | node server.js & 17 | ``` 18 | 19 | visit the url the server tells you to visit. 20 | 21 | Finally, run the tests with: 22 | 23 | ``` 24 | mocha test-dom.js --timeout 10000 25 | ```` 26 | 27 | The increased timeout is to give you enough time to manually verify the incoming connection in Firefox. 28 | 29 | Right now you have to run each test individually, until Firefox [bug 891003](https://bugzilla.mozilla.org/show_bug.cgi?id=891003) is fixed. 30 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'), 2 | FirefoxClient = require("./index"); 3 | 4 | 5 | var url = "http://harthur.github.io/bugzilla-todos"; 6 | 7 | getFirstTab(function(tab) { 8 | tab.DOM.querySelector("#title", function(err, node) { 9 | console.log("got node:", node.tagName); 10 | tab.DOM.getUsedFontFaces(node, { includePreviews: true }, function(err, fonts) { 11 | for (var i in fonts) { 12 | var font = fonts[i]; 13 | console.log("first font", font); 14 | } 15 | }); 16 | }) 17 | }); 18 | 19 | /* 20 | loadUrl(url, function(tab) { 21 | tab.DOM.querySelector("#title", function(err, node) { 22 | console.log("got node:", node.tagName); 23 | tab.DOM.getUsedFontFaces(node, function(err, fonts) { 24 | for (var i in fonts) { 25 | var font = fonts[i]; 26 | console.log("first font", font); 27 | } 28 | }); 29 | }) 30 | }) */ 31 | 32 | /** 33 | { fromFontGroup: true, 34 | fromLanguagePrefs: false, 35 | fromSystemFallback: false, 36 | name: 'Georgia', 37 | CSSFamilyName: 'Georgia', 38 | rule: null, 39 | srcIndex: -1, 40 | URI: '', 41 | localName: '', 42 | format: '', 43 | metadata: '' } 44 | */ 45 | 46 | 47 | /** 48 | * Helper functions 49 | */ 50 | function loadUrl(url, callback) { 51 | getFirstTab(function(tab) { 52 | console.log("GOT TAB"); 53 | tab.navigateTo(url); 54 | 55 | tab.once("navigate", function() { 56 | console.log("NAVIGATED"); 57 | callback(tab); 58 | }); 59 | }); 60 | } 61 | 62 | function getFirstTab(callback) { 63 | var client = new FirefoxClient({log: true}); 64 | 65 | client.connect(function() { 66 | client.listTabs(function(err, tabs) { 67 | if (err) throw err; 68 | 69 | var tab = tabs[0]; 70 | 71 | // attach so we can receive load events 72 | tab.attach(function(err) { 73 | if (err) throw err; 74 | callback(tab); 75 | }) 76 | }); 77 | }); 78 | } -------------------------------------------------------------------------------- /test/test-console.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | utils = require("./utils"); 3 | 4 | var Console; 5 | 6 | before(function(done) { 7 | utils.loadTab('dom.html', function(aTab) { 8 | Console = aTab.Console; 9 | done(); 10 | }); 11 | }); 12 | 13 | // Console - evaluateJS() 14 | 15 | describe('evaluateJS()', function() { 16 | it('should evaluate expr to number', function(done) { 17 | Console.evaluateJS('6 + 7', function(err, resp) { 18 | assert.strictEqual(err, null); 19 | assert.equal(resp.result, 13); 20 | done(); 21 | }) 22 | }) 23 | 24 | it('should evaluate expr to boolean', function(done) { 25 | Console.evaluateJS('!!window', function(err, resp) { 26 | assert.strictEqual(err, null); 27 | assert.strictEqual(resp.result, true); 28 | done(); 29 | }) 30 | }) 31 | 32 | it('should evaluate expr to string', function(done) { 33 | Console.evaluateJS('"hello"', function(err, resp) { 34 | assert.strictEqual(err, null); 35 | assert.equal(resp.result, "hello"); 36 | done(); 37 | }) 38 | }) 39 | 40 | it('should evaluate expr to JSObject', function(done) { 41 | Console.evaluateJS('x = {a: 2, b: "hello"}', function(err, resp) { 42 | assert.strictEqual(err, null); 43 | assert.ok(resp.result.ownPropertyNames, "result has JSObject methods"); 44 | done(); 45 | }) 46 | }) 47 | 48 | it('should evaluate to undefined', function(done) { 49 | Console.evaluateJS('undefined', function(err, resp) { 50 | assert.strictEqual(err, null); 51 | assert.ok(resp.result.type, "undefined"); 52 | done(); 53 | }) 54 | }) 55 | 56 | it('should have exception in response', function(done) { 57 | Console.evaluateJS('blargh', function(err, resp) { 58 | assert.strictEqual(err, null); 59 | assert.equal(resp.exception.class, "Error"); // TODO: error should be JSObject 60 | assert.equal(resp.exceptionMessage, "ReferenceError: blargh is not defined"); 61 | done(); 62 | }) 63 | }) 64 | }) 65 | -------------------------------------------------------------------------------- /lib/tab.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"), 3 | Console = require("./console"), 4 | Memory = require("./memory"), 5 | DOM = require("./dom"), 6 | Network = require("./network"), 7 | StyleSheets = require("./stylesheets"); 8 | 9 | module.exports = Tab; 10 | 11 | function Tab(client, tab) { 12 | this.initialize(client, tab.actor); 13 | 14 | this.tab = tab; 15 | this.updateInfo(tab); 16 | 17 | this.on("tabNavigated", this.onTabNavigated.bind(this)); 18 | } 19 | 20 | Tab.prototype = extend(ClientMethods, { 21 | updateInfo: function(form) { 22 | this.url = form.url; 23 | this.title = form.title; 24 | }, 25 | 26 | get StyleSheets() { 27 | if (!this._StyleSheets) { 28 | this._StyleSheets = new StyleSheets(this.client, this.tab.styleSheetsActor); 29 | } 30 | return this._StyleSheets; 31 | }, 32 | 33 | get DOM() { 34 | if (!this._DOM) { 35 | this._DOM = new DOM(this.client, this.tab.inspectorActor); 36 | } 37 | return this._DOM; 38 | }, 39 | 40 | get Network() { 41 | if (!this._Network) { 42 | this._Network = new Network(this.client, this.tab.consoleActor); 43 | } 44 | return this._Network; 45 | }, 46 | 47 | get Console() { 48 | if (!this._Console) { 49 | this._Console = new Console(this.client, this.tab.consoleActor); 50 | } 51 | return this._Console; 52 | }, 53 | 54 | get Memory() { 55 | if (!this._Memory) { 56 | this._Memory = new Memory(this.client, this.tab.memoryActor); 57 | } 58 | return this._Memory; 59 | }, 60 | 61 | onTabNavigated: function(event) { 62 | if (event.state == "start") { 63 | this.emit("before-navigate", { url: event.url }); 64 | } 65 | else if (event.state == "stop") { 66 | this.updateInfo(event); 67 | 68 | this.emit("navigate", { url: event.url, title: event.title }); 69 | } 70 | }, 71 | 72 | attach: function(cb) { 73 | this.request("attach", cb); 74 | }, 75 | 76 | detach: function(cb) { 77 | this.request("detach", cb); 78 | }, 79 | 80 | reload: function(cb) { 81 | this.request("reload", cb); 82 | }, 83 | 84 | navigateTo: function(url, cb) { 85 | this.request("navigateTo", { url: url }, cb); 86 | } 87 | }) 88 | -------------------------------------------------------------------------------- /lib/jsobject.js: -------------------------------------------------------------------------------- 1 | var select = require("js-select"), 2 | extend = require("./extend"), 3 | ClientMethods = require("./client-methods"); 4 | 5 | module.exports = JSObject; 6 | 7 | function JSObject(client, obj) { 8 | this.initialize(client, obj.actor); 9 | this.obj = obj; 10 | } 11 | 12 | JSObject.prototype = extend(ClientMethods, { 13 | type: "object", 14 | 15 | get class() { 16 | return this.obj.class; 17 | }, 18 | 19 | get name() { 20 | return this.obj.name; 21 | }, 22 | 23 | get displayName() { 24 | return this.obj.displayName; 25 | }, 26 | 27 | ownPropertyNames: function(cb) { 28 | this.request('ownPropertyNames', function(resp) { 29 | return resp.ownPropertyNames; 30 | }, cb); 31 | }, 32 | 33 | ownPropertyDescriptor: function(name, cb) { 34 | this.request('property', { name: name }, function(resp) { 35 | return this.transformDescriptor(resp.descriptor); 36 | }.bind(this), cb); 37 | }, 38 | 39 | ownProperties: function(cb) { 40 | this.request('prototypeAndProperties', function(resp) { 41 | return this.transformProperties(resp.ownProperties); 42 | }.bind(this), cb); 43 | }, 44 | 45 | prototype: function(cb) { 46 | this.request('prototype', function(resp) { 47 | return this.createJSObject(resp.prototype); 48 | }.bind(this), cb); 49 | }, 50 | 51 | ownPropertiesAndPrototype: function(cb) { 52 | this.request('prototypeAndProperties', function(resp) { 53 | resp.ownProperties = this.transformProperties(resp.ownProperties); 54 | resp.safeGetterValues = this.transformGetters(resp.safeGetterValues); 55 | resp.prototype = this.createJSObject(resp.prototype); 56 | 57 | return resp; 58 | }.bind(this), cb); 59 | }, 60 | 61 | /* helpers */ 62 | transformProperties: function(props) { 63 | var transformed = {}; 64 | for (var prop in props) { 65 | transformed[prop] = this.transformDescriptor(props[prop]); 66 | } 67 | return transformed; 68 | }, 69 | 70 | transformGetters: function(getters) { 71 | var transformed = {}; 72 | for (var prop in getters) { 73 | transformed[prop] = this.transformGetter(getters[prop]); 74 | } 75 | return transformed; 76 | }, 77 | 78 | transformDescriptor: function(descriptor) { 79 | descriptor.value = this.createJSObject(descriptor.value); 80 | return descriptor; 81 | }, 82 | 83 | transformGetter: function(getter) { 84 | return { 85 | value: this.createJSObject(getter.getterValue), 86 | prototypeLevel: getter.getterPrototypeLevel, 87 | enumerable: getter.enumerable, 88 | writable: getter.writable 89 | } 90 | } 91 | }) -------------------------------------------------------------------------------- /test/test-network.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | path = require("path"), 3 | utils = require("./utils"); 4 | 5 | var tab; 6 | var Network; 7 | var Console; 8 | 9 | before(function(done) { 10 | utils.loadTab('network.html', function(aTab) { 11 | tab = aTab; 12 | Network = aTab.Network; 13 | Console = aTab.Console; 14 | 15 | Network.startLogging(function(err) { 16 | assert.strictEqual(err, null); 17 | done(); 18 | }) 19 | }); 20 | }); 21 | 22 | // Network - startLogging(), stopLogging(), sendHTTPRequest(), event:network-event 23 | 24 | describe('"network-event" event', function() { 25 | it('should receive "network-event" event with message', function(done) { 26 | Network.once('network-event', function(event) { 27 | assert.equal(event.method, "GET"); 28 | assert.equal(path.basename(event.url), "test-network.json"); 29 | assert.ok(event.isXHR); 30 | assert.ok(event.getResponseHeaders, "event has NetworkEvent methods") 31 | done(); 32 | }); 33 | 34 | Console.evaluateJS("sendRequest()") 35 | }) 36 | }) 37 | 38 | describe('sendHTTPRequest()', function() { 39 | it('should send a new XHR request from page', function(done) { 40 | var request = { 41 | url: "test-network.json", 42 | method: "GET", 43 | headers: [{name: "test-header", value: "test-value"}] 44 | }; 45 | 46 | Network.sendHTTPRequest(request, function(err, netEvent) { 47 | assert.strictEqual(err, null); 48 | assert.ok(netEvent.getResponseHeaders, "event has NetworkEvent methods"); 49 | done(); 50 | }); 51 | }) 52 | }) 53 | 54 | // NetworkEvent - getRequestHeaders(), getRequestCookies(), getRequestPostData(), 55 | // getResponseHeaders(), getResponseCookies(), getResponseContent(), getEventTimings() 56 | // event:update 57 | 58 | 59 | describe('getRequestHeaders(', function() { 60 | it('should get request headers', function(done) { 61 | Network.on('network-event', function(netEvent) { 62 | netEvent.on("request-headers", function(event) { 63 | assert.ok(event.headers); 64 | assert.ok(event.headersSize); 65 | 66 | netEvent.getRequestHeaders(function(err, resp) { 67 | assert.strictEqual(err, null); 68 | 69 | var found = resp.headers.some(function(header) { 70 | return header.name == "test-header" && 71 | header.value == "test-value"; 72 | }); 73 | assert.ok(found, "contains that header we sent"); 74 | done(); 75 | }) 76 | }) 77 | }); 78 | Console.evaluateJS("sendRequest()"); 79 | }) 80 | }) 81 | 82 | // TODO: NetworkEvent tests 83 | 84 | after(function() { 85 | Network.stopLogging(function(err) { 86 | assert.strictEqual(err, null); 87 | }); 88 | }) 89 | -------------------------------------------------------------------------------- /lib/network.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"); 2 | var ClientMethods = require("./client-methods"); 3 | 4 | module.exports = Network; 5 | 6 | function Network(client, actor) { 7 | this.initialize(client, actor); 8 | 9 | this.on("networkEvent", this.onNetworkEvent.bind(this)); 10 | } 11 | 12 | Network.prototype = extend(ClientMethods, { 13 | types: ["NetworkActivity"], 14 | 15 | startLogging: function(cb) { 16 | this.request('startListeners', { listeners: this.types }, cb); 17 | }, 18 | 19 | stopLogging: function(cb) { 20 | this.request('stopListeners', { listeners: this.types }, cb); 21 | }, 22 | 23 | onNetworkEvent: function(event) { 24 | var networkEvent = new NetworkEvent(this.client, event.eventActor); 25 | 26 | this.emit("network-event", networkEvent); 27 | }, 28 | 29 | sendHTTPRequest: function(request, cb) { 30 | this.request('sendHTTPRequest', { request: request }, function(resp) { 31 | return new NetworkEvent(this.client, resp.eventActor); 32 | }.bind(this), cb); 33 | } 34 | }) 35 | 36 | function NetworkEvent(client, event) { 37 | this.initialize(client, event.actor); 38 | this.event = event; 39 | 40 | this.on("networkEventUpdate", this.onUpdate.bind(this)); 41 | } 42 | 43 | NetworkEvent.prototype = extend(ClientMethods, { 44 | get url() { 45 | return this.event.url; 46 | }, 47 | 48 | get method() { 49 | return this.event.method; 50 | }, 51 | 52 | get isXHR() { 53 | return this.event.isXHR; 54 | }, 55 | 56 | getRequestHeaders: function(cb) { 57 | this.request('getRequestHeaders', cb); 58 | }, 59 | 60 | getRequestCookies: function(cb) { 61 | this.request('getRequestCookies', this.pluck('cookies'), cb); 62 | }, 63 | 64 | getRequestPostData: function(cb) { 65 | this.request('getRequestPostData', cb); 66 | }, 67 | 68 | getResponseHeaders: function(cb) { 69 | this.request('getResponseHeaders', cb); 70 | }, 71 | 72 | getResponseCookies: function(cb) { 73 | this.request('getResponseCookies', this.pluck('cookies'), cb); 74 | }, 75 | 76 | getResponseContent: function(cb) { 77 | this.request('getResponseContent', cb); 78 | }, 79 | 80 | getEventTimings: function(cb) { 81 | this.request('getEventTimings', cb); 82 | }, 83 | 84 | onUpdate: function(event) { 85 | var types = { 86 | "requestHeaders": "request-headers", 87 | "requestCookies": "request-cookies", 88 | "requestPostData": "request-postdata", 89 | "responseStart": "response-start", 90 | "responseHeaders": "response-headers", 91 | "responseCookies": "response-cookies", 92 | "responseContent": "response-content", 93 | "eventTimings": "event-timings" 94 | } 95 | 96 | var type = types[event.updateType]; 97 | delete event.updateType; 98 | 99 | this.emit(type, event); 100 | } 101 | }) 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /lib/client-methods.js: -------------------------------------------------------------------------------- 1 | var events = require("events"), 2 | extend = require("./extend"); 3 | 4 | // to be instantiated later - to avoid circular dep resolution 5 | var JSObject; 6 | 7 | var ClientMethods = extend(events.EventEmitter.prototype, { 8 | /** 9 | * Intialize this client object. 10 | * 11 | * @param {object} client 12 | * Client to send requests on. 13 | * @param {string} actor 14 | * Actor id to set as 'from' field on requests 15 | */ 16 | initialize: function(client, actor) { 17 | this.client = client; 18 | this.actor = actor; 19 | 20 | this.client.on('message', function(message) { 21 | if (message.from == this.actor) { 22 | this.emit(message.type, message); 23 | } 24 | }.bind(this)); 25 | }, 26 | 27 | /** 28 | * Make request to our actor on the server. 29 | * 30 | * @param {string} type 31 | * Method name of the request 32 | * @param {object} message 33 | * Optional extra properties (arguments to method) 34 | * @param {Function} transform 35 | * Optional tranform for response object. Takes response object 36 | * and returns object to send on. 37 | * @param {Function} callback 38 | * Callback to call with (maybe transformed) response 39 | */ 40 | request: function(type, message, transform, callback) { 41 | if (typeof message == "function") { 42 | if (typeof transform == "function") { 43 | // (type, trans, cb) 44 | callback = transform; 45 | transform = message; 46 | } 47 | else { 48 | // (type, cb) 49 | callback = message; 50 | } 51 | message = {}; 52 | } 53 | else if (!callback) { 54 | if (!message) { 55 | // (type) 56 | message = {}; 57 | } 58 | // (type, message, cb) 59 | callback = transform; 60 | transform = null; 61 | } 62 | 63 | message.to = this.actor; 64 | message.type = type; 65 | 66 | this.client.makeRequest(message, function(resp) { 67 | delete resp.from; 68 | 69 | if (resp.error) { 70 | var err = new Error(resp.message); 71 | err.name = resp.error; 72 | 73 | callback(err); 74 | return; 75 | } 76 | 77 | if (transform) { 78 | resp = transform(resp); 79 | } 80 | 81 | if (callback) { 82 | callback(null, resp); 83 | } 84 | }); 85 | }, 86 | 87 | /* 88 | * Transform obj response into a JSObject 89 | */ 90 | createJSObject: function(obj) { 91 | if (obj == null) { 92 | return; 93 | } 94 | if (!JSObject) { 95 | // circular dependencies 96 | JSObject = require("./jsobject"); 97 | } 98 | if (obj.type == "object") { 99 | return new JSObject(this.client, obj); 100 | } 101 | return obj; 102 | }, 103 | 104 | /** 105 | * Create function that plucks out only one value from an object. 106 | * Used as the transform function for some responses. 107 | */ 108 | pluck: function(prop) { 109 | return function(obj) { 110 | return obj[prop]; 111 | } 112 | } 113 | }) 114 | 115 | module.exports = ClientMethods; -------------------------------------------------------------------------------- /test/test-logs.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | utils = require("./utils"); 3 | 4 | var tab; 5 | var Console; 6 | 7 | before(function(done) { 8 | utils.loadTab('logs.html', function(aTab) { 9 | tab = aTab; 10 | Console = aTab.Console; 11 | 12 | Console.startListening(function() { 13 | done(); 14 | }) 15 | }); 16 | }); 17 | 18 | // Console - startLogging(), stopLogging(), getCachedMessages(), 19 | // clearCachedMessages(), event:page-error, event:console-api-call 20 | 21 | describe('getCachedMessages()', function() { 22 | it('should get messages from before listening', function(done) { 23 | Console.getCachedLogs(function(err, messages) { 24 | assert.strictEqual(err, null); 25 | 26 | var hasLog = messages.some(function(message) { 27 | return message.level == "log"; 28 | }) 29 | assert.ok(hasLog); 30 | 31 | var hasDir = messages.some(function(message) { 32 | return message.level == "dir"; 33 | }) 34 | assert.ok(hasDir); 35 | 36 | var hasError = messages.some(function(message) { 37 | return message.errorMessage == "ReferenceError: foo is not defined"; 38 | }) 39 | assert.ok(hasError); 40 | done(); 41 | }); 42 | }) 43 | }) 44 | 45 | describe('clearCachedMessages()', function() { 46 | it('should clear cached messages', function(done) { 47 | Console.clearCachedLogs(function() { 48 | Console.getCachedLogs(function(err, messages) { 49 | assert.strictEqual(err, null); 50 | // The error message should be left 51 | assert.equal(messages.length, 1); 52 | assert.equal(messages[0].errorMessage, "ReferenceError: foo is not defined") 53 | done(); 54 | }) 55 | }); 56 | }) 57 | }) 58 | 59 | describe('"page-error" event', function() { 60 | it('should receive "page-error" event with message', function(done) { 61 | Console.once('page-error', function(event) { 62 | assert.equal(event.errorMessage, "ReferenceError: foo is not defined"); 63 | assert.ok(event.sourceName.indexOf("logs.html") > 0); 64 | assert.equal(event.lineNumber, 10); 65 | assert.equal(event.columnNumber, 0); 66 | assert.ok(event.exception); 67 | 68 | done(); 69 | }); 70 | 71 | tab.reload(); 72 | }) 73 | }) 74 | 75 | describe('"console-api-call" event', function() { 76 | it('should receive "console-api-call" for console.log', function(done) { 77 | Console.on('console-api-call', function(event) { 78 | if (event.level == "log") { 79 | assert.deepEqual(event.arguments, ["hi"]); 80 | 81 | Console.removeAllListeners('console-api-call'); 82 | done(); 83 | } 84 | }); 85 | 86 | tab.reload(); 87 | }) 88 | 89 | it('should receive "console-api-call" for console.dir', function(done) { 90 | Console.on('console-api-call', function(event) { 91 | if (event.level == "dir") { 92 | var obj = event.arguments[0]; 93 | assert.ok(obj.ownPropertyNames, "dir argument has JSObject methods"); 94 | 95 | Console.removeAllListeners('console-api-call'); 96 | done(); 97 | } 98 | }); 99 | 100 | tab.reload(); 101 | }) 102 | }) 103 | 104 | after(function() { 105 | Console.stopListening(); 106 | }) 107 | -------------------------------------------------------------------------------- /lib/console.js: -------------------------------------------------------------------------------- 1 | var select = require("js-select"), 2 | extend = require("./extend"), 3 | ClientMethods = require("./client-methods"), 4 | JSObject = require("./jsobject"); 5 | 6 | module.exports = Console; 7 | 8 | function Console(client, actor) { 9 | this.initialize(client, actor); 10 | 11 | this.on("consoleAPICall", this.onConsoleAPI.bind(this)); 12 | this.on("pageError", this.onPageError.bind(this)); 13 | } 14 | 15 | Console.prototype = extend(ClientMethods, { 16 | types: ["PageError", "ConsoleAPI"], 17 | 18 | /** 19 | * Response object: 20 | * -empty- 21 | */ 22 | startListening: function(cb) { 23 | this.request('startListeners', { listeners: this.types }, cb); 24 | }, 25 | 26 | /** 27 | * Response object: 28 | * -empty- 29 | */ 30 | stopListening: function(cb) { 31 | this.request('stopListeners', { listeners: this.types }, cb); 32 | }, 33 | 34 | /** 35 | * Event object: 36 | * level - "log", etc. 37 | * filename - file with call 38 | * lineNumber - line number of call 39 | * functionName - function log called from 40 | * timeStamp - ms timestamp of call 41 | * arguments - array of the arguments to log call 42 | * private - 43 | */ 44 | onConsoleAPI: function(event) { 45 | var message = this.transformConsoleCall(event.message); 46 | 47 | this.emit("console-api-call", message); 48 | }, 49 | 50 | /** 51 | * Event object: 52 | * errorMessage - string error message 53 | * sourceName - file error 54 | * lineText 55 | * lineNumber - line number of error 56 | * columnNumber - column number of error 57 | * category - usually "content javascript", 58 | * timeStamp - time in ms of error occurance 59 | * warning - whether it's a warning 60 | * error - whether it's an error 61 | * exception - whether it's an exception 62 | * strict - 63 | * private - 64 | */ 65 | onPageError: function(event) { 66 | this.emit("page-error", event.pageError); 67 | }, 68 | 69 | /** 70 | * Response object: array of page error or console call objects. 71 | */ 72 | getCachedLogs: function(cb) { 73 | var message = { 74 | messageTypes: this.types 75 | }; 76 | this.request('getCachedMessages', message, function(resp) { 77 | select(resp, ".messages > *").update(this.transformConsoleCall.bind(this)); 78 | return resp.messages; 79 | }.bind(this), cb); 80 | }, 81 | 82 | /** 83 | * Response object: 84 | * -empty- 85 | */ 86 | clearCachedLogs: function(cb) { 87 | this.request('clearMessagesCache', cb); 88 | }, 89 | 90 | /** 91 | * Response object: 92 | * input - original input 93 | * result - result of the evaluation, a value or JSObject 94 | * timestamp - timestamp in ms of the evaluation 95 | * exception - any exception as a result of the evaluation 96 | */ 97 | evaluateJS: function(text, cb) { 98 | this.request('evaluateJS', { text: text }, function(resp) { 99 | return select(resp, ".result, .exception") 100 | .update(this.createJSObject.bind(this)); 101 | }.bind(this), cb) 102 | }, 103 | 104 | transformConsoleCall: function(message) { 105 | return select(message, ".arguments > *").update(this.createJSObject.bind(this)); 106 | } 107 | }) 108 | -------------------------------------------------------------------------------- /lib/dom.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"), 3 | Node = require("./domnode"); 4 | 5 | module.exports = DOM; 6 | 7 | function DOM(client, actor) { 8 | this.initialize(client, actor); 9 | this.walker = null; 10 | } 11 | 12 | DOM.prototype = extend(ClientMethods, { 13 | document: function(cb) { 14 | this.walkerRequest("document", function(err, resp) { 15 | if (err) return cb(err); 16 | 17 | var node = new Node(this.client, this.walker, resp.node); 18 | cb(null, node); 19 | }.bind(this)) 20 | }, 21 | 22 | documentElement: function(cb) { 23 | this.walkerRequest("documentElement", function(err, resp) { 24 | var node = new Node(this.client, this.walker, resp.node); 25 | cb(err, node); 26 | }.bind(this)) 27 | }, 28 | 29 | querySelector: function(selector, cb) { 30 | this.document(function(err, node) { 31 | if (err) return cb(err); 32 | 33 | node.querySelector(selector, cb); 34 | }) 35 | }, 36 | 37 | querySelectorAll: function(selector, cb) { 38 | this.document(function(err, node) { 39 | if (err) return cb(err); 40 | 41 | node.querySelectorAll(selector, cb); 42 | }) 43 | }, 44 | 45 | getComputedStyle: function(node, cb) { 46 | this.styleRequest("getComputed", { node: node.actor }, 47 | this.pluck('computed'), cb); 48 | }, 49 | 50 | getUsedFontFaces: function(node, options, cb) { 51 | var message = { 52 | node: node.actor, 53 | includePreviews: options.includePreviews, 54 | previewText: options.previewText, 55 | previewFontSize: options.previewFontSize 56 | }; 57 | 58 | this.styleRequest("getUsedFontFaces", message, 59 | this.pluck('fontFaces'), cb); 60 | }, 61 | 62 | getFontPreview: function(node, font, cb) { 63 | this.styleRequest("getFontPreview", { node: node.actor, font: font }, cb); 64 | }, 65 | 66 | walkerRequest: function(type, message, cb) { 67 | this.getWalker(function(err, walker) { 68 | walker.request(type, message, cb); 69 | }); 70 | }, 71 | 72 | getWalker: function(cb) { 73 | if (this.walker) { 74 | return cb(null, this.walker); 75 | } 76 | this.request('getWalker', function(err, resp) { 77 | this.walker = new Walker(this.client, resp.walker); 78 | cb(err, this.walker); 79 | }.bind(this)) 80 | }, 81 | 82 | styleRequest: function(type, message, transform, cb) { 83 | this.getStyle(function(err, style) { 84 | if (err) throw err; 85 | 86 | style.request(type, message, transform, cb); 87 | }) 88 | }, 89 | 90 | getStyle: function(cb) { 91 | if (this.style) { 92 | return cb(null, this.style); 93 | } 94 | this.request('getPageStyle', function(err, resp) { 95 | this.style = new Style(this.client, resp.pageStyle); 96 | cb(err, this.style); 97 | }.bind(this)) 98 | } 99 | }) 100 | 101 | function Walker(client, walker) { 102 | this.initialize(client, walker.actor); 103 | 104 | this.root = new Node(client, this, walker.root); 105 | } 106 | 107 | Walker.prototype = extend(ClientMethods, {}); 108 | 109 | function Style(client, style) { 110 | this.initialize(client, style.actor); 111 | } 112 | 113 | Style.prototype = extend(ClientMethods, {}); 114 | -------------------------------------------------------------------------------- /lib/browser.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"), 3 | Client = require("./client"), 4 | Tab = require("./tab"), 5 | Webapps = require("./webapps"), 6 | Device = require("./device"), 7 | SimulatorApps = require("./simulator"); 8 | 9 | const DEFAULT_PORT = 6000; 10 | const DEFAULT_HOST = "localhost"; 11 | 12 | module.exports = FirefoxClient; 13 | 14 | function FirefoxClient(options) { 15 | var client = new Client(options); 16 | var actor = 'root'; 17 | 18 | client.on("error", this.onError.bind(this)); 19 | client.on("end", this.onEnd.bind(this)); 20 | client.on("timeout", this.onTimeout.bind(this)); 21 | 22 | this.initialize(client, actor); 23 | } 24 | 25 | FirefoxClient.prototype = extend(ClientMethods, { 26 | connect: function(port, host, cb) { 27 | if (typeof port == "function") { 28 | // (cb) 29 | cb = port; 30 | port = DEFAULT_PORT; 31 | host = DEFAULT_HOST; 32 | 33 | } 34 | if (typeof host == "function") { 35 | // (port, cb) 36 | cb = host; 37 | host = DEFAULT_HOST; 38 | } 39 | // (port, host, cb) 40 | 41 | this.client.connect(port, host, cb); 42 | 43 | this.client.expectReply(this.actor, function(packet) { 44 | // root message 45 | }); 46 | }, 47 | 48 | disconnect: function() { 49 | this.client.disconnect(); 50 | }, 51 | 52 | onError: function(error) { 53 | this.emit("error", error); 54 | }, 55 | 56 | onEnd: function() { 57 | this.emit("end"); 58 | }, 59 | 60 | onTimeout: function() { 61 | this.emit("timeout"); 62 | }, 63 | 64 | selectedTab: function(cb) { 65 | this.request("listTabs", function(resp) { 66 | var tab = resp.tabs[resp.selected]; 67 | return new Tab(this.client, tab); 68 | }.bind(this), cb); 69 | }, 70 | 71 | listTabs: function(cb) { 72 | this.request("listTabs", function(err, resp) { 73 | if (err) { 74 | return cb(err); 75 | } 76 | 77 | if (resp.simulatorWebappsActor) { 78 | // the server is the Firefox OS Simulator, return apps as "tabs" 79 | var apps = new SimulatorApps(this.client, resp.simulatorWebappsActor); 80 | apps.listApps(cb); 81 | } 82 | else { 83 | var tabs = resp.tabs.map(function(tab) { 84 | return new Tab(this.client, tab); 85 | }.bind(this)); 86 | cb(null, tabs); 87 | } 88 | }.bind(this)); 89 | }, 90 | 91 | getWebapps: function(cb) { 92 | this.request("listTabs", (function(err, resp) { 93 | if (err) { 94 | return cb(err); 95 | } 96 | var webapps = new Webapps(this.client, resp); 97 | cb(null, webapps); 98 | }).bind(this)); 99 | }, 100 | 101 | getDevice: function(cb) { 102 | this.request("listTabs", (function(err, resp) { 103 | if (err) { 104 | return cb(err); 105 | } 106 | var device = new Device(this.client, resp); 107 | cb(null, device); 108 | }).bind(this)); 109 | }, 110 | 111 | getRoot: function(cb) { 112 | this.request("listTabs", (function(err, resp) { 113 | if (err) { 114 | return cb(err); 115 | } 116 | if (!resp.consoleActor) { 117 | return cb("No root actor being available."); 118 | } 119 | var root = new Tab(this.client, resp); 120 | cb(null, root); 121 | }).bind(this)); 122 | } 123 | }) 124 | -------------------------------------------------------------------------------- /lib/stylesheets.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"); 2 | var ClientMethods = require("./client-methods"); 3 | 4 | module.exports = StyleSheets; 5 | 6 | function StyleSheets(client, actor) { 7 | this.initialize(client, actor); 8 | } 9 | 10 | StyleSheets.prototype = extend(ClientMethods, { 11 | getStyleSheets: function(cb) { 12 | this.request('getStyleSheets', function(resp) { 13 | return resp.styleSheets.map(function(sheet) { 14 | return new StyleSheet(this.client, sheet); 15 | }.bind(this)); 16 | }.bind(this), cb); 17 | }, 18 | 19 | addStyleSheet: function(text, cb) { 20 | this.request('addStyleSheet', { text: text }, function(resp) { 21 | return new StyleSheet(this.client, resp.styleSheet); 22 | }.bind(this), cb); 23 | } 24 | }) 25 | 26 | function StyleSheet(client, sheet) { 27 | this.initialize(client, sheet.actor); 28 | this.sheet = sheet; 29 | 30 | this.on("propertyChange", this.onPropertyChange.bind(this)); 31 | } 32 | 33 | StyleSheet.prototype = extend(ClientMethods, { 34 | get href() { 35 | return this.sheet.href; 36 | }, 37 | 38 | get disabled() { 39 | return this.sheet.disabled; 40 | }, 41 | 42 | get ruleCount() { 43 | return this.sheet.ruleCount; 44 | }, 45 | 46 | onPropertyChange: function(event) { 47 | this.sheet[event.property] = event.value; 48 | this.emit(event.property + "-changed", event.value); 49 | }, 50 | 51 | toggleDisabled: function(cb) { 52 | this.request('toggleDisabled', function(err, resp) { 53 | if (err) return cb(err); 54 | 55 | this.sheet.disabled = resp.disabled; 56 | cb(null, resp.disabled); 57 | }.bind(this)); 58 | }, 59 | 60 | getOriginalSources: function(cb) { 61 | this.request('getOriginalSources', function(resp) { 62 | if (resp.originalSources === null) { 63 | return []; 64 | } 65 | return resp.originalSources.map(function(form) { 66 | return new OriginalSource(this.client, form); 67 | }.bind(this)); 68 | }.bind(this), cb); 69 | }, 70 | 71 | getMediaRules: function(cb) { 72 | this.request('getMediaRules', function(resp) { 73 | return resp.mediaRules.map(function(form) { 74 | return new MediaRule(this.client, form); 75 | }.bind(this)); 76 | }.bind(this), cb); 77 | }, 78 | 79 | update: function(text, cb) { 80 | this.request('update', { text: text, transition: true }, cb); 81 | }, 82 | 83 | getText: function(cb) { 84 | this.request('getText', this.pluck('text'), cb); 85 | } 86 | }); 87 | 88 | function MediaRule(client, rule) { 89 | this.initialize(client, rule.actor); 90 | this.rule = rule; 91 | 92 | this.on("matchesChange", function(event) { 93 | this.emit("matches-change", event.matches); 94 | }.bind(this)); 95 | } 96 | MediaRule.prototype = extend(ClientMethods, { 97 | get mediaText() { 98 | return this.rule.mediaText; 99 | }, 100 | 101 | get matches() { 102 | return this.rule.matches; 103 | } 104 | }) 105 | 106 | function OriginalSource(client, source) { 107 | console.log("source", source); 108 | this.initialize(client, source.actor); 109 | 110 | this.source = source; 111 | } 112 | 113 | OriginalSource.prototype = extend(ClientMethods, { 114 | get url() { 115 | return this.source.url 116 | }, 117 | 118 | getText: function(cb) { 119 | this.request('getText', this.pluck('text'), cb); 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /test/test-jsobject.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | utils = require("./utils"); 3 | 4 | var Console; 5 | var obj; 6 | var func; 7 | 8 | before(function(done) { 9 | utils.loadTab('dom.html', function(aTab) { 10 | Console = aTab.Console; 11 | Console.evaluateJS('x = {a: 2, b: {c: 3}, get d() {return 4;}}', function(err, resp) { 12 | obj = resp.result; 13 | 14 | var input = 'y = function testfunc(a, b) { return a + b; }'; 15 | Console.evaluateJS(input, function(err, resp) { 16 | func = resp.result; 17 | done(); 18 | }) 19 | }); 20 | }); 21 | }); 22 | 23 | // JSObject - ownPropertyNames(), ownPropertyDescriptor(), prototype(), properties() 24 | 25 | describe('ownPropertyNames()', function() { 26 | it('should fetch property names', function(done) { 27 | obj.ownPropertyNames(function(err, names) { 28 | assert.strictEqual(err, null); 29 | assert.deepEqual(names, ['a', 'b', 'd']); 30 | done(); 31 | }) 32 | }) 33 | }); 34 | 35 | describe('ownPropertyDescriptor()', function() { 36 | it('should fetch descriptor for property', function(done) { 37 | obj.ownPropertyDescriptor('a', function(err, desc) { 38 | assert.strictEqual(err, null); 39 | testDescriptor(desc); 40 | assert.equal(desc.value, 2); 41 | done(); 42 | }) 43 | }) 44 | 45 | /* TODO: doesn't call callback if not defined property - Server side problem 46 | it('should be undefined for nonexistent property', function(done) { 47 | obj.ownPropertyDescriptor('g', function(desc) { 48 | console.log("desc", desc); 49 | done(); 50 | }) 51 | }) */ 52 | }) 53 | 54 | describe('ownProperties()', function() { 55 | it('should fetch all own properties and descriptors', function(done) { 56 | obj.ownProperties(function(err, props) { 57 | assert.strictEqual(err, null); 58 | testDescriptor(props.a); 59 | assert.equal(props.a.value, 2); 60 | 61 | testDescriptor(props.b); 62 | assert.ok(props.b.value.ownProperties, "prop value has JSObject methods"); 63 | done(); 64 | }) 65 | }) 66 | }) 67 | 68 | describe('prototype()', function() { 69 | it('should fetch prototype as an object', function(done) { 70 | obj.prototype(function(err, proto) { 71 | assert.strictEqual(err, null); 72 | assert.ok(proto.ownProperties, "prototype has JSObject methods"); 73 | done(); 74 | }) 75 | }) 76 | }) 77 | 78 | describe('ownPropertiesAndPrototype()', function() { 79 | it('should fetch properties, prototype, and getters', function(done) { 80 | obj.ownPropertiesAndPrototype(function(err, resp) { 81 | assert.strictEqual(err, null); 82 | 83 | // own properties 84 | var props = resp.ownProperties; 85 | assert.equal(Object.keys(props).length, 3); 86 | 87 | testDescriptor(props.a); 88 | assert.equal(props.a.value, 2); 89 | 90 | // prototype 91 | assert.ok(resp.prototype.ownProperties, 92 | "prototype has JSObject methods"); 93 | 94 | // getters 95 | var getters = resp.safeGetterValues; 96 | assert.equal(Object.keys(getters).length, 0); 97 | 98 | done(); 99 | }) 100 | }) 101 | }) 102 | 103 | describe('Function objects', function() { 104 | it('sould have correct properties', function() { 105 | assert.equal(func.class, "Function"); 106 | assert.equal(func.name, "testfunc"); 107 | assert.ok(func.ownProperties, "function has JSObject methods") 108 | }) 109 | }) 110 | 111 | function testDescriptor(desc) { 112 | assert.strictEqual(desc.configurable, true); 113 | assert.strictEqual(desc.enumerable, true); 114 | assert.strictEqual(desc.writable, true); 115 | } -------------------------------------------------------------------------------- /test/test-stylesheets.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | path = require("path"), 3 | utils = require("./utils"); 4 | 5 | var StyleSheets; 6 | var styleSheet; 7 | 8 | var SS_TEXT = [ 9 | "main {", 10 | " font-family: Georgia, sans-serif;", 11 | " color: black;", 12 | "}", 13 | "", 14 | "* {", 15 | " padding: 0;", 16 | " margin: 0;", 17 | "}" 18 | ].join("\n"); 19 | 20 | before(function(done) { 21 | utils.loadTab('stylesheets.html', function(aTab) { 22 | StyleSheets = aTab.StyleSheets; 23 | StyleSheets.getStyleSheets(function(err, sheets) { 24 | assert.strictEqual(err, null); 25 | styleSheet = sheets[1]; 26 | done(); 27 | }) 28 | }); 29 | }); 30 | 31 | // Stylesheets - getStyleSheets(), addStyleSheet() 32 | 33 | describe('getStyleSheets()', function() { 34 | it('should list all the stylesheets', function(done) { 35 | StyleSheets.getStyleSheets(function(err, sheets) { 36 | assert.strictEqual(err, null); 37 | 38 | var hrefs = sheets.map(function(sheet) { 39 | assert.ok(sheet.update, "sheet has Stylesheet methods"); 40 | return path.basename(sheet.href); 41 | }); 42 | assert.deepEqual(hrefs, ["null", "stylesheet1.css"]); 43 | done(); 44 | }) 45 | }) 46 | }) 47 | 48 | describe('addStyleSheet()', function() { 49 | it('should add a new stylesheet', function(done) { 50 | var text = "div { font-weight: bold; }"; 51 | 52 | StyleSheets.addStyleSheet(text, function(err, sheet) { 53 | assert.strictEqual(err, null); 54 | assert.ok(sheet.update, "sheet has Stylesheet methods"); 55 | assert.equal(sheet.ruleCount, 1); 56 | done(); 57 | }) 58 | }) 59 | }) 60 | 61 | // StyleSheet - update(), toggleDisabled() 62 | 63 | describe('StyleSheet', function() { 64 | it('should have the correct properties', function() { 65 | assert.equal(path.basename(styleSheet.href), "stylesheet1.css"); 66 | assert.strictEqual(styleSheet.disabled, false); 67 | assert.equal(styleSheet.ruleCount, 2); 68 | }) 69 | }) 70 | 71 | describe('StyleSheet.getText()', function() { 72 | it('should get the text of the style sheet', function(done) { 73 | styleSheet.getText(function(err, resp) { 74 | assert.strictEqual(err, null); 75 | assert.equal(resp, SS_TEXT); 76 | done(); 77 | }) 78 | }) 79 | }); 80 | 81 | describe('StyleSheet.update()', function() { 82 | it('should update stylesheet', function(done) { 83 | var text = "main { color: red; }"; 84 | 85 | styleSheet.update(text, function(err, resp) { 86 | assert.strictEqual(err, null); 87 | // TODO: assert.equal(styleSheet.ruleCount, 1); 88 | done(); 89 | }) 90 | }) 91 | }) 92 | 93 | describe('StyleSheet.toggleDisabled()', function() { 94 | it('should toggle disabled attribute', function(done) { 95 | assert.deepEqual(styleSheet.disabled, false); 96 | 97 | styleSheet.toggleDisabled(function(err, resp) { 98 | assert.strictEqual(err, null); 99 | assert.deepEqual(styleSheet.disabled, true); 100 | done(); 101 | }) 102 | }) 103 | 104 | it('should fire disabled-changed event', function(done) { 105 | styleSheet.toggleDisabled(function(err, resp) { 106 | assert.strictEqual(err, null); 107 | assert.deepEqual(styleSheet.disabled, false); 108 | }) 109 | styleSheet.on("disabled-changed", function(disabled) { 110 | assert.strictEqual(disabled, false); 111 | done(); 112 | }) 113 | }) 114 | }) 115 | 116 | describe('StyleSheet.getOriginalSources()', function() { 117 | it('should get no original sources', function(done) { 118 | styleSheet.getOriginalSources(function(err, resp) { 119 | assert.strictEqual(err, null); 120 | assert.deepEqual(resp, []); 121 | done(); 122 | }) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /lib/domnode.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"); 3 | 4 | module.exports = Node; 5 | 6 | function Node(client, walker, node) { 7 | this.initialize(client, node.actor); 8 | this.walker = walker; 9 | 10 | this.getNode = this.getNode.bind(this); 11 | this.getNodeArray = this.getNodeArray.bind(this); 12 | this.getNodeList = this.getNodeList.bind(this); 13 | 14 | walker.on('newMutations', function(event) { 15 | //console.log("on new mutations! ", JSON.stringify(event)); 16 | }); 17 | 18 | ['nodeType', 'nodeName', 'namespaceURI', 'attrs'] 19 | .forEach(function(attr) { 20 | this[attr] = node[attr]; 21 | }.bind(this)); 22 | } 23 | 24 | Node.prototype = extend(ClientMethods, { 25 | getAttribute: function(name) { 26 | for (var i in this.attrs) { 27 | var attr = this.attrs[i]; 28 | if (attr.name == name) { 29 | return attr.value; 30 | } 31 | } 32 | }, 33 | 34 | setAttribute: function(name, value, cb) { 35 | var mods = [{ 36 | attributeName: name, 37 | newValue: value 38 | }]; 39 | this.request('modifyAttributes', { modifications: mods }, cb); 40 | }, 41 | 42 | parentNode: function(cb) { 43 | this.parents(function(err, nodes) { 44 | if (err) { 45 | return cb(err); 46 | } 47 | var node = null; 48 | if (nodes.length) { 49 | node = nodes[0]; 50 | } 51 | cb(null, node); 52 | }) 53 | }, 54 | 55 | parents: function(cb) { 56 | this.nodeRequest('parents', this.getNodeArray, cb); 57 | }, 58 | 59 | children: function(cb) { 60 | this.nodeRequest('children', this.getNodeArray, cb); 61 | }, 62 | 63 | siblings: function(cb) { 64 | this.nodeRequest('siblings', this.getNodeArray, cb); 65 | }, 66 | 67 | nextSibling: function(cb) { 68 | this.nodeRequest('nextSibling', this.getNode, cb); 69 | }, 70 | 71 | previousSibling: function(cb) { 72 | this.nodeRequest('previousSibling', this.getNode, cb); 73 | }, 74 | 75 | querySelector: function(selector, cb) { 76 | this.nodeRequest('querySelector', { selector: selector }, 77 | this.getNode, cb); 78 | }, 79 | 80 | querySelectorAll: function(selector, cb) { 81 | this.nodeRequest('querySelectorAll', { selector: selector }, 82 | this.getNodeList, cb); 83 | }, 84 | 85 | getUniqueSelector: function(cb) { 86 | this.request('getUniqueSelector', cb); 87 | }, 88 | 89 | innerHTML: function(cb) { 90 | this.nodeRequest('innerHTML', function(resp) { 91 | return resp.value; 92 | }, cb) 93 | }, 94 | 95 | outerHTML: function(cb) { 96 | this.nodeRequest('outerHTML', function(resp) { 97 | return resp.value; 98 | }, cb) 99 | }, 100 | 101 | remove: function(cb) { 102 | this.nodeRequest('removeNode', function(resp) { 103 | return new Node(this.client, this.walker, resp.nextSibling); 104 | }.bind(this), cb); 105 | }, 106 | 107 | highlight: function(cb) { 108 | this.nodeRequest('highlight', cb); 109 | }, 110 | 111 | release: function(cb) { 112 | this.nodeRequest('releaseNode', cb); 113 | }, 114 | 115 | getNode: function(resp) { 116 | if (resp.node) { 117 | return new Node(this.client, this.walker, resp.node); 118 | } 119 | return null; 120 | }, 121 | 122 | getNodeArray: function(resp) { 123 | return resp.nodes.map(function(form) { 124 | return new Node(this.client, this.walker, form); 125 | }.bind(this)); 126 | }, 127 | 128 | getNodeList: function(resp) { 129 | return new NodeList(this.client, this.walker, resp.list); 130 | }, 131 | 132 | nodeRequest: function(type, message, transform, cb) { 133 | if (!cb) { 134 | cb = transform; 135 | transform = message; 136 | message = {}; 137 | } 138 | message.node = this.actor; 139 | 140 | this.walker.request(type, message, transform, cb); 141 | } 142 | }); 143 | 144 | function NodeList(client, walker, list) { 145 | this.client = client; 146 | this.walker = walker; 147 | this.actor = list.actor; 148 | 149 | this.length = list.length; 150 | } 151 | 152 | NodeList.prototype = extend(ClientMethods, { 153 | items: function(start, end, cb) { 154 | if (typeof start == "function") { 155 | cb = start; 156 | start = 0; 157 | end = this.length; 158 | } 159 | else if (typeof end == "function") { 160 | cb = end; 161 | end = this.length; 162 | } 163 | this.request('items', { start: start, end: end }, 164 | this.getNodeArray.bind(this), cb); 165 | }, 166 | 167 | // TODO: add this function to ClientMethods 168 | getNodeArray: function(resp) { 169 | return resp.nodes.map(function(form) { 170 | return new Node(this.client, this.walker, form); 171 | }.bind(this)); 172 | } 173 | }); 174 | -------------------------------------------------------------------------------- /lib/webapps.js: -------------------------------------------------------------------------------- 1 | var extend = require("./extend"), 2 | ClientMethods = require("./client-methods"), 3 | Tab = require("./tab"), 4 | fs = require("fs"), 5 | spawn = require("child_process").spawn; 6 | 7 | module.exports = Webapps; 8 | 9 | var CHUNK_SIZE = 20480; 10 | 11 | // Also dispatch appOpen/appClose, appInstall/appUninstall events 12 | function Webapps(client, tab) { 13 | this.initialize(client, tab.webappsActor); 14 | } 15 | 16 | Webapps.prototype = extend(ClientMethods, { 17 | watchApps: function(cb) { 18 | this.request("watchApps", cb); 19 | }, 20 | unwatchApps: function(cb) { 21 | this.request("unwatchApps", cb); 22 | }, 23 | launch: function(manifestURL, cb) { 24 | this.request("launch", {manifestURL: manifestURL}, cb); 25 | }, 26 | close: function(manifestURL, cb) { 27 | this.request("close", {manifestURL: manifestURL}, cb); 28 | }, 29 | getInstalledApps: function(cb) { 30 | this.request("getAll", function (err, resp) { 31 | if (err) { 32 | cb(err); 33 | return; 34 | } 35 | cb(null, resp.apps); 36 | }); 37 | }, 38 | listRunningApps: function(cb) { 39 | this.request("listRunningApps", function (err, resp) { 40 | if (err) { 41 | cb(err); 42 | return; 43 | } 44 | cb(null, resp.apps); 45 | }); 46 | }, 47 | getApp: function(manifestURL, cb) { 48 | this.request("getAppActor", {manifestURL: manifestURL}, (function (err, resp) { 49 | if (err) { 50 | cb(err); 51 | return; 52 | } 53 | var actor = new Tab(this.client, resp.actor); 54 | cb(null, actor); 55 | }).bind(this)); 56 | }, 57 | installHosted: function(options, cb) { 58 | this.request( 59 | "install", 60 | { appId: options.appId, 61 | metadata: options.metadata, 62 | manifest: options.manifest }, 63 | function (err, resp) { 64 | if (err || resp.error) { 65 | cb(err || resp.error); 66 | return; 67 | } 68 | cb(null, resp.appId); 69 | }); 70 | }, 71 | _upload: function (path, cb) { 72 | // First create an upload actor 73 | this.request("uploadPackage", function (err, resp) { 74 | var actor = resp.actor; 75 | fs.readFile(path, function(err, data) { 76 | chunk(actor, data); 77 | }); 78 | }); 79 | // Send push the file chunk by chunk 80 | var self = this; 81 | var step = 0; 82 | function chunk(actor, data) { 83 | var i = step++ * CHUNK_SIZE; 84 | var m = Math.min(i + CHUNK_SIZE, data.length); 85 | var c = ""; 86 | for(; i < m; i++) 87 | c += String.fromCharCode(data[i]); 88 | var message = { 89 | to: actor, 90 | type: "chunk", 91 | chunk: c 92 | }; 93 | self.client.makeRequest(message, function(resp) { 94 | if (resp.error) { 95 | cb(resp); 96 | return; 97 | } 98 | if (i < data.length) { 99 | setTimeout(chunk, 0, actor, data); 100 | } else { 101 | done(actor); 102 | } 103 | }); 104 | } 105 | // Finally close the upload 106 | function done(actor) { 107 | var message = { 108 | to: actor, 109 | type: "done" 110 | }; 111 | self.client.makeRequest(message, function(resp) { 112 | if (resp.error) { 113 | cb(resp); 114 | } else { 115 | cb(null, actor, cleanup.bind(null, actor)); 116 | } 117 | }); 118 | } 119 | 120 | // Remove the temporary uploaded file from the server: 121 | function cleanup(actor) { 122 | var message = { 123 | to: actor, 124 | type: "remove" 125 | }; 126 | self.client.makeRequest(message, function () {}); 127 | } 128 | }, 129 | installPackaged: function(path, appId, cb) { 130 | this._upload(path, (function (err, actor, cleanup) { 131 | this.request("install", {appId: appId, upload: actor}, 132 | function (err, resp) { 133 | if (err) { 134 | cb(err); 135 | return; 136 | } 137 | cb(null, resp.appId); 138 | cleanup(); 139 | }); 140 | }).bind(this)); 141 | }, 142 | installPackagedWithADB: function(path, appId, cb) { 143 | var self = this; 144 | // First ensure the temporary folder exists 145 | function createTemporaryFolder() { 146 | var c = spawn("adb", ["shell", "mkdir -p /data/local/tmp/b2g/" + appId], {stdio:"inherit"}); 147 | c.on("close", uploadPackage); 148 | } 149 | // then upload the package to the temporary directory 150 | function uploadPackage() { 151 | var child = spawn("adb", ["push", path, "/data/local/tmp/b2g/" + appId + "/application.zip"], {stdio:"inherit"}); 152 | child.on("close", installApp); 153 | } 154 | // finally order the webapps actor to install the app 155 | function installApp() { 156 | self.request("install", {appId: appId}, 157 | function (err, resp) { 158 | if (err) { 159 | cb(err); 160 | return; 161 | } 162 | cb(null, resp.appId); 163 | }); 164 | } 165 | createTemporaryFolder(); 166 | }, 167 | uninstall: function(manifestURL, cb) { 168 | this.request("uninstall", {manifestURL: manifestURL}, cb); 169 | } 170 | }) 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # firefox-client 2 | `firefox-client` is a [node](nodejs.org) library for remote debugging Firefox. You can use it to make things like [fxconsole](https://github.com/harthur/fxconsole), a remote JavaScript REPL. 3 | 4 | ```javascript 5 | var FirefoxClient = require("firefox-client"); 6 | 7 | var client = new FirefoxClient(); 8 | 9 | client.connect(6000, function() { 10 | client.listTabs(function(err, tabs) { 11 | console.log("first tab:", tabs[0].url); 12 | }); 13 | }); 14 | ``` 15 | 16 | ## Install 17 | With [node.js](http://nodejs.org/) npm package manager: 18 | 19 | ```bash 20 | npm install firefox-client 21 | ``` 22 | 23 | ## Connecting 24 | 25 | ### Desktop Firefox 26 | 1. Enable remote debugging (You'll only have to do this once) 27 | 1. Open the DevTools. **Web Developer** > **Toggle Tools** 28 | 2. Visit the settings panel (gear icon) 29 | 3. Check "Enable remote debugging" under Advanced Settings 30 | 31 | 2. Listen for a connection 32 | 1. Open the Firefox command line with **Tools** > **Web Developer** > **Developer Toolbar**. 33 | 2. Start a server by entering this command: `listen 6000` (where `6000` is the port number) 34 | 35 | ### Firefox for Android 36 | Follow the instructions in [this Hacks video](https://www.youtube.com/watch?v=Znj_8IFeTVs) 37 | 38 | ### Firefox OS 1.1 Simulator 39 | A limited set of the API (`Console`, `StyleSheets`) is compatible with the [Simulator 4.0](https://addons.mozilla.org/en-US/firefox/addon/firefox-os-simulator/). See the [wiki instructions](https://github.com/harthur/firefox-client/wiki/Firefox-OS-Simulator-Instructions) for connecting. 40 | 41 | `client.listTabs()` will list the currently open apps in the Simulator. 42 | 43 | ### Firefox OS 1.2+ Simulator and devices 44 | 45 | `client.getWebapps()` will expose the webapps in the Simulator, where each app implements the `Tab` API. 46 | 47 | ``` 48 | client.getWebapps(function(err, webapps) { 49 | webapps.getApp("app://homescreen.gaiamobile.org/manifest.webapp", function (err, app) { 50 | console.log("homescreen:", actor.url); 51 | app.Console.evaluateJS("alert('foo')", function(err, resp) { 52 | console.log("alert dismissed"); 53 | }); 54 | }); 55 | }); 56 | ``` 57 | 58 | ## Compatibility 59 | 60 | This latest version of the library will stay compatible with [Firefox Nightly](http://nightly.mozilla.org/). Almost all of it will be compatible with [Firefox Aurora](http://www.mozilla.org/en-US/firefox/aurora/) as well. 61 | 62 | ## API 63 | 64 | A `FirefoxClient` is the entry point to the API. After connecting, get a `Tab` object with `listTabs()` or `selectedTab()`. Once you have a `Tab`, you can call methods and listen to events from the tab's modules, `Console` or `Network`. There are also experimental `DOM` and `StyleSheets` tab modules, and an upcoming `Debugger` module. 65 | 66 | #### Methods 67 | Almost all API calls take a callback that will get called with an error as the first argument (or `null` if there is no error), and a return value as the second: 68 | 69 | ```javascript 70 | tab.Console.evaluateJS("6 + 7", function(err, resp) { 71 | if (err) throw err; 72 | 73 | console.log(resp.result); 74 | }); 75 | ``` 76 | 77 | #### Events 78 | 79 | The modules are `EventEmitter`s, listen for events with `on` or `once`, and stop listening with `off`: 80 | 81 | ```javascript 82 | tab.Console.on("page-error", function(event) { 83 | console.log("new error from tab:", event.errorMessage); 84 | }); 85 | ``` 86 | 87 | Summary of the offerings of the modules and objects: 88 | 89 | #### [FirefoxClient](http://github.com/harthur/firefox-client/wiki/FirefoxClient) 90 | Methods: `connect()`, `disconnect()`, `listTabs()`, `selectedTab()`, `getWebapps()`, `getRoot()` 91 | 92 | Events: `"error"`, `"timeout"`, `"end"` 93 | 94 | #### [Tab](https://github.com/harthur/firefox-client/wiki/Tab) 95 | Properties: `url`, `title` 96 | 97 | Methods: `reload()`, `navigateTo()`, `attach()`, `detach()` 98 | 99 | Events: `"navigate"`, `"before-navigate"` 100 | 101 | #### [Tab.Console](https://github.com/harthur/firefox-client/wiki/Console) 102 | Methods: `evaluateJS()`, `startListening()`, `stopListening()`, `getCachedLogs()` 103 | 104 | Events: `"page-error"`, `"console-api-call"` 105 | 106 | #### [JSObject](https://github.com/harthur/firefox-client/wiki/JSObject) 107 | Properties: `class`, `name`, `displayName` 108 | 109 | Methods: `ownPropertyNames()`, `ownPropertyDescriptor()`, `ownProperties()`, `prototype()` 110 | 111 | #### [Tab.Network](https://github.com/harthur/firefox-client/wiki/Network) 112 | Methods: `startLogging()`, `stopLogging()`, `sendHTTPRequest()` 113 | 114 | Events: `"network-event"` 115 | 116 | #### [NetworkEvent](https://github.com/harthur/firefox-client/wiki/NetworkEvent) 117 | Properties: `url`, `method`, `isXHR` 118 | 119 | Methods: `getRequestHeaders()`, `getRequestCookies()`, `getRequestPostData()`, `getResponseHeaders()`, `getResponseCookies()`, `getResponseContent()`, `getEventTimings()` 120 | 121 | Events: `"request-headers"`, `"request-cookies"`, `"request-postdata"`, `"response-start"`, `"response-headers"`, `"response-cookies"`, `"event-timings"` 122 | 123 | #### [Tab.DOM](https://github.com/harthur/firefox-client/wiki/DOM) 124 | Methods: `document()`, `documentElement()`, `querySelector()`, `querySelectorAll()` 125 | 126 | #### [DOMNode](https://github.com/harthur/firefox-client/wiki/DOMNode) 127 | Properties: `nodeValue`, `nodeName`, `namespaceURI` 128 | 129 | Methods: `parentNode()`, `parents()`, `siblings()`, `nextSibling()`, `previousSibling()`, `querySelector()`, `querySelectorAll()`, `innerHTML()`, `outerHTML()`, `setAttribute()`, `remove()`, `release()` 130 | 131 | #### [Tab.StyleSheets](https://github.com/harthur/firefox-client/wiki/StyleSheets) 132 | Methods: `getStyleSheets()`, `addStyleSheet()` 133 | 134 | #### [StyleSheet](https://github.com/harthur/firefox-client/wiki/StyleSheet) 135 | Properties: `href`, `disabled`, `ruleCount` 136 | 137 | Methods: `getText()`, `update()`, `toggleDisabled()`, `getOriginalSources()` 138 | 139 | Events: `"disabled-changed"`, `"ruleCount-changed"` 140 | 141 | #### Tab.Memory 142 | Methods: `measure()` 143 | 144 | #### Webapps 145 | Methods: `listRunningApps()`, `getInstalledApps()`, `watchApps()`, `unwatchApps()`, `launch()`, `close()`, `getApp()`, `installHosted()`, `installPackaged()`, `installPackagedWithADB()`, `uninstall()` 146 | 147 | Events: `"appOpen"`, `"appClose"`, `"appInstall"`, `"appUninstall"` 148 | 149 | ## Examples 150 | 151 | [fxconsole](https://github.com/harthur/fxconsole) - a remote JavaScript console for Firefox 152 | 153 | [webapps test script](https://pastebin.mozilla.org/5094843) - a sample usage of all webapps features 154 | 155 | ## Feedback 156 | 157 | What do you need from the API? [File an issue](https://github.com/harthur/firefox-client/issues/new). 158 | -------------------------------------------------------------------------------- /lib/client.js: -------------------------------------------------------------------------------- 1 | var net = require("net"), 2 | events = require("events"), 3 | extend = require("./extend"); 4 | 5 | var colors = require("colors"); 6 | 7 | module.exports = Client; 8 | 9 | // this is very unfortunate! and temporary. we can't 10 | // rely on 'type' property to signify an event, and we 11 | // need to write clients for each actor to handle differences 12 | // in actor protocols 13 | var unsolicitedEvents = { 14 | "tabNavigated": "tabNavigated", 15 | "styleApplied": "styleApplied", 16 | "propertyChange": "propertyChange", 17 | "networkEventUpdate": "networkEventUpdate", 18 | "networkEvent": "networkEvent", 19 | "propertyChange": "propertyChange", 20 | "newMutations": "newMutations", 21 | "appOpen": "appOpen", 22 | "appClose": "appClose", 23 | "appInstall": "appInstall", 24 | "appUninstall": "appUninstall", 25 | "frameUpdate": "frameUpdate" 26 | }; 27 | 28 | /** 29 | * a Client object handles connecting with a Firefox remote debugging 30 | * server instance (e.g. a Firefox instance), plus sending and receiving 31 | * packets on that conection using the Firefox remote debugging protocol. 32 | * 33 | * Important methods: 34 | * connect - Create the connection to the server. 35 | * makeRequest - Make a request to the server with a JSON message, 36 | * and a callback to call with the response. 37 | * 38 | * Important events: 39 | * 'message' - An unsolicited (e.g. not a response to a prior request) 40 | * packet has been received. These packets usually describe events. 41 | */ 42 | function Client(options) { 43 | this.options = options || {}; 44 | 45 | this.incoming = new Buffer(""); 46 | 47 | this._pendingRequests = []; 48 | this._activeRequests = {}; 49 | } 50 | 51 | Client.prototype = extend(events.EventEmitter.prototype, { 52 | connect: function(port, host, cb) { 53 | this.client = net.createConnection({ 54 | port: port, 55 | host: host 56 | }); 57 | 58 | this.client.on("connect", cb); 59 | this.client.on("data", this.onData.bind(this)); 60 | this.client.on("error", this.onError.bind(this)); 61 | this.client.on("end", this.onEnd.bind(this)); 62 | this.client.on("timeout", this.onTimeout.bind(this)); 63 | }, 64 | 65 | disconnect: function() { 66 | if (this.client) { 67 | this.client.end(); 68 | } 69 | }, 70 | 71 | /** 72 | * Set a request to be sent to an actor on the server. If the actor 73 | * is already handling a request, queue this request until the actor 74 | * has responded to the previous request. 75 | * 76 | * @param {object} request 77 | * Message to be JSON-ified and sent to server. 78 | * @param {function} callback 79 | * Function that's called with the response from the server. 80 | */ 81 | makeRequest: function(request, callback) { 82 | this.log("request: " + JSON.stringify(request).green); 83 | 84 | if (!request.to) { 85 | var type = request.type || ""; 86 | throw new Error(type + " request packet has no destination."); 87 | } 88 | this._pendingRequests.push({ to: request.to, 89 | message: request, 90 | callback: callback }); 91 | this._flushRequests(); 92 | }, 93 | 94 | /** 95 | * Activate (send) any pending requests to actors that don't have an 96 | * active request. 97 | */ 98 | _flushRequests: function() { 99 | this._pendingRequests = this._pendingRequests.filter(function(request) { 100 | // only one active request per actor at a time 101 | if (this._activeRequests[request.to]) { 102 | return true; 103 | } 104 | 105 | // no active requests for this actor, so activate this one 106 | this.sendMessage(request.message); 107 | this.expectReply(request.to, request.callback); 108 | 109 | // remove from pending requests 110 | return false; 111 | }.bind(this)); 112 | }, 113 | 114 | /** 115 | * Send a JSON message over the connection to the server. 116 | */ 117 | sendMessage: function(message) { 118 | if (!message.to) { 119 | throw new Error("No actor specified in request"); 120 | } 121 | if (!this.client) { 122 | throw new Error("Not connected, connect() before sending requests"); 123 | } 124 | var str = JSON.stringify(message); 125 | 126 | // message is preceded by byteLength(message): 127 | str = (new Buffer(str).length) + ":" + str; 128 | 129 | this.client.write(str); 130 | }, 131 | 132 | /** 133 | * Arrange to hand the next reply from |actor| to |handler|. 134 | */ 135 | expectReply: function(actor, handler) { 136 | if (this._activeRequests[actor]) { 137 | throw Error("clashing handlers for next reply from " + uneval(actor)); 138 | } 139 | this._activeRequests[actor] = handler; 140 | }, 141 | 142 | /** 143 | * Handler for a new message coming in. It's either an unsolicited event 144 | * from the server, or a response to a previous request from the client. 145 | */ 146 | handleMessage: function(message) { 147 | if (!message.from) { 148 | if (message.error) { 149 | throw new Error(message.message); 150 | } 151 | throw new Error("Server didn't specify an actor: " + JSON.stringify(message)); 152 | } 153 | 154 | if (!(message.type in unsolicitedEvents) 155 | && this._activeRequests[message.from]) { 156 | this.log("response: " + JSON.stringify(message).yellow); 157 | 158 | var callback = this._activeRequests[message.from]; 159 | delete this._activeRequests[message.from]; 160 | 161 | callback(message); 162 | 163 | this._flushRequests(); 164 | } 165 | else if (message.type) { 166 | // this is an unsolicited event from the server 167 | this.log("unsolicited event: ".grey + JSON.stringify(message).grey); 168 | 169 | this.emit('message', message); 170 | return; 171 | } 172 | else { 173 | throw new Error("Unexpected packet from actor " + message.from 174 | + JSON.stringify(message)); 175 | } 176 | }, 177 | 178 | /** 179 | * Called when a new data chunk is received on the connection. 180 | * Parse data into message(s) and call message handler for any full 181 | * messages that are read in. 182 | */ 183 | onData: function(data) { 184 | this.incoming = Buffer.concat([this.incoming, data]); 185 | 186 | while(this.readMessage()) {}; 187 | }, 188 | 189 | /** 190 | * Parse out and process the next message from the data read from 191 | * the connection. Returns true if a full meassage was parsed, false 192 | * otherwise. 193 | */ 194 | readMessage: function() { 195 | var sep = this.incoming.toString().indexOf(':'); 196 | if (sep < 0) { 197 | return false; 198 | } 199 | 200 | // beginning of a message is preceded by byteLength(message) + ":" 201 | var count = parseInt(this.incoming.slice(0, sep)); 202 | 203 | if (this.incoming.length - (sep + 1) < count) { 204 | this.log("no complete response yet".grey); 205 | return false; 206 | } 207 | this.incoming = this.incoming.slice(sep + 1); 208 | 209 | var packet = this.incoming.slice(0, count); 210 | 211 | this.incoming = this.incoming.slice(count); 212 | 213 | var message; 214 | try { 215 | message = JSON.parse(packet.toString()); 216 | } catch(e) { 217 | throw new Error("Couldn't parse packet from server as JSON " + e 218 | + ", message:\n" + packet); 219 | } 220 | this.handleMessage(message); 221 | 222 | return true; 223 | }, 224 | 225 | onError: function(error) { 226 | var code = error.code ? error.code : error; 227 | this.log("connection error: ".red + code.red); 228 | this.emit("error", error); 229 | }, 230 | 231 | onEnd: function() { 232 | this.log("connection closed by server".red); 233 | this.emit("end"); 234 | }, 235 | 236 | onTimeout: function() { 237 | this.log("connection timeout".red); 238 | this.emit("timeout"); 239 | }, 240 | 241 | log: function(str) { 242 | if (this.options.log) { 243 | console.log(str); 244 | } 245 | } 246 | }) 247 | -------------------------------------------------------------------------------- /test/test-dom.js: -------------------------------------------------------------------------------- 1 | var assert = require("assert"), 2 | utils = require("./utils"); 3 | 4 | var doc; 5 | var DOM; 6 | var node; 7 | var firstNode; 8 | var lastNode; 9 | 10 | before(function(done) { 11 | utils.loadTab('dom.html', function(aTab) { 12 | DOM = aTab.DOM; 13 | DOM.document(function(err, aDoc) { 14 | doc = aDoc; 15 | DOM.querySelectorAll(".item", function(err, list) { 16 | list.items(function(err, items) { 17 | firstNode = items[0]; 18 | node = items[1]; 19 | lastNode = items[2]; 20 | done(); 21 | }) 22 | }) 23 | }) 24 | }); 25 | }); 26 | 27 | // DOM - document(), documentElement() 28 | 29 | describe('document()', function() { 30 | it('should get document node', function(done) { 31 | DOM.document(function(err, doc) { 32 | assert.strictEqual(err, null); 33 | assert.equal(doc.nodeName, "#document"); 34 | assert.equal(doc.nodeType, 9); 35 | done(); 36 | }) 37 | }) 38 | }) 39 | 40 | 41 | describe('documentElement()', function() { 42 | it('should get documentElement node', function(done) { 43 | DOM.documentElement(function(err, elem) { 44 | assert.strictEqual(err, null); 45 | assert.equal(elem.nodeName, "HTML"); 46 | assert.equal(elem.nodeType, 1); 47 | done(); 48 | }) 49 | }) 50 | }) 51 | 52 | describe('querySelector()', function() { 53 | it('should get first item node', function(done) { 54 | DOM.querySelector(".item", function(err, child) { 55 | assert.strictEqual(err, null); 56 | assert.equal(child.getAttribute("id"), "test1"); 57 | assert.ok(child.querySelector, "node has node methods"); 58 | done(); 59 | }) 60 | }) 61 | }) 62 | 63 | describe('querySelector()', function() { 64 | it('should get all item nodes', function(done) { 65 | DOM.querySelectorAll(".item", function(err, list) { 66 | assert.strictEqual(err, null); 67 | assert.equal(list.length, 3); 68 | 69 | list.items(function(err, children) { 70 | assert.strictEqual(err, null); 71 | var ids = children.map(function(child) { 72 | assert.ok(child.querySelector, "list item has node methods"); 73 | return child.getAttribute("id"); 74 | }) 75 | assert.deepEqual(ids, ["test1","test2","test3"]); 76 | done(); 77 | }) 78 | }) 79 | }) 80 | }) 81 | 82 | // Node - parentNode(), parent(), siblings(), nextSibling(), previousSibling(), 83 | // querySelector(), querySelectorAll(), innerHTML(), outerHTML(), getAttribute(), 84 | // setAttribute() 85 | 86 | describe('parentNode()', function() { 87 | it('should get parent node', function(done) { 88 | node.parentNode(function(err, parent) { 89 | assert.strictEqual(err, null); 90 | assert.equal(parent.nodeName, "SECTION"); 91 | assert.ok(parent.querySelector, "parent has node methods"); 92 | done(); 93 | }) 94 | }) 95 | 96 | it('should be null for document parentNode', function(done) { 97 | doc.parentNode(function(err, parent) { 98 | assert.strictEqual(err, null); 99 | assert.strictEqual(parent, null); 100 | done(); 101 | }) 102 | }) 103 | }) 104 | 105 | describe('parents()', function() { 106 | it('should get ancestor nodes', function(done) { 107 | node.parents(function(err, ancestors) { 108 | assert.strictEqual(err, null); 109 | var names = ancestors.map(function(ancestor) { 110 | assert.ok(ancestor.querySelector, "ancestor has node methods"); 111 | return ancestor.nodeName; 112 | }) 113 | assert.deepEqual(names, ["SECTION","MAIN","BODY","HTML","#document"]); 114 | done(); 115 | }) 116 | }) 117 | }) 118 | 119 | describe('children()', function() { 120 | it('should get child nodes', function(done) { 121 | node.children(function(err, children) { 122 | assert.strictEqual(err, null); 123 | var ids = children.map(function(child) { 124 | assert.ok(child.querySelector, "child has node methods"); 125 | return child.getAttribute("id"); 126 | }) 127 | assert.deepEqual(ids, ["child1","child2"]); 128 | done(); 129 | }) 130 | }) 131 | }) 132 | 133 | describe('siblings()', function() { 134 | it('should get sibling nodes', function(done) { 135 | node.siblings(function(err, siblings) { 136 | assert.strictEqual(err, null); 137 | var ids = siblings.map(function(sibling) { 138 | assert.ok(sibling.querySelector, "sibling has node methods"); 139 | return sibling.getAttribute("id"); 140 | }) 141 | assert.deepEqual(ids, ["test1","test2","test3"]); 142 | done(); 143 | }) 144 | }) 145 | }) 146 | 147 | describe('nextSibling()', function() { 148 | it('should get next sibling node', function(done) { 149 | node.nextSibling(function(err, sibling) { 150 | assert.strictEqual(err, null); 151 | assert.equal(sibling.getAttribute("id"), "test3"); 152 | assert.ok(sibling.querySelector, "next sibling has node methods"); 153 | done(); 154 | }) 155 | }) 156 | 157 | it('should be null if no next sibling', function(done) { 158 | lastNode.nextSibling(function(err, sibling) { 159 | assert.strictEqual(err, null); 160 | assert.strictEqual(sibling, null); 161 | done(); 162 | }) 163 | }) 164 | }) 165 | 166 | describe('previousSibling()', function() { 167 | it('should get next sibling node', function(done) { 168 | node.previousSibling(function(err, sibling) { 169 | assert.strictEqual(err, null); 170 | assert.equal(sibling.getAttribute("id"), "test1"); 171 | assert.ok(sibling.querySelector, "next sibling has node methods"); 172 | done(); 173 | }) 174 | }) 175 | 176 | it('should be null if no prev sibling', function(done) { 177 | firstNode.previousSibling(function(err, sibling) { 178 | assert.strictEqual(err, null); 179 | assert.strictEqual(sibling, null); 180 | done(); 181 | }) 182 | }) 183 | }) 184 | 185 | describe('querySelector()', function() { 186 | it('should get first child node', function(done) { 187 | node.querySelector("*", function(err, child) { 188 | assert.strictEqual(err, null); 189 | assert.equal(child.getAttribute("id"), "child1"); 190 | assert.ok(child.querySelector, "node has node methods"); 191 | done(); 192 | }) 193 | }) 194 | 195 | it('should be null if no nodes with selector', function(done) { 196 | node.querySelector("blarg", function(err, resp) { 197 | assert.strictEqual(err, null); 198 | assert.strictEqual(resp, null); 199 | done(); 200 | }) 201 | }) 202 | }) 203 | 204 | describe('querySelectorAll()', function() { 205 | it('should get all child nodes', function(done) { 206 | node.querySelectorAll("*", function(err, list) { 207 | assert.strictEqual(err, null); 208 | assert.equal(list.length, 2); 209 | 210 | list.items(function(err, children) { 211 | assert.strictEqual(err, null); 212 | var ids = children.map(function(child) { 213 | assert.ok(child.querySelector, "list item has node methods"); 214 | return child.getAttribute("id"); 215 | }) 216 | assert.deepEqual(ids, ["child1", "child2"]); 217 | done(); 218 | }) 219 | }) 220 | }) 221 | 222 | it('should get nodes from "start" to "end"', function(done) { 223 | doc.querySelectorAll(".item", function(err, list) { 224 | assert.strictEqual(err, null); 225 | assert.equal(list.length, 3); 226 | 227 | list.items(1, 2, function(err, items) { 228 | assert.strictEqual(err, null); 229 | assert.equal(items.length, 1); 230 | assert.deepEqual(items[0].getAttribute("id"), "test2") 231 | done(); 232 | }) 233 | }) 234 | }) 235 | 236 | it('should get nodes from "start"', function(done) { 237 | doc.querySelectorAll(".item", function(err, list) { 238 | assert.strictEqual(err, null); 239 | assert.equal(list.length, 3); 240 | 241 | list.items(1, function(err, items) { 242 | assert.strictEqual(err, null); 243 | assert.equal(items.length, 2); 244 | var ids = items.map(function(item) { 245 | assert.ok(item.querySelector, "list item has node methods"); 246 | return item.getAttribute("id"); 247 | }) 248 | assert.deepEqual(ids, ["test2","test3"]); 249 | done(); 250 | }) 251 | }) 252 | }) 253 | 254 | it('should be empty list if no nodes with selector', function(done) { 255 | node.querySelectorAll("blarg", function(err, list) { 256 | assert.strictEqual(err, null); 257 | assert.equal(list.length, 0); 258 | 259 | list.items(function(err, items) { 260 | assert.strictEqual(err, null); 261 | assert.deepEqual(items, []); 262 | done(); 263 | }) 264 | }) 265 | }) 266 | }) 267 | 268 | describe('innerHTML()', function() { 269 | it('should get innerHTML of node', function(done) { 270 | node.innerHTML(function(err, text) { 271 | assert.strictEqual(err, null); 272 | assert.equal(text, '\n
\n' 273 | + '
\n '); 274 | done(); 275 | }) 276 | }) 277 | }) 278 | 279 | describe('outerHTML()', function() { 280 | it('should get outerHTML of node', function(done) { 281 | node.outerHTML(function(err, text) { 282 | assert.strictEqual(err, null); 283 | assert.equal(text, '
\n' 284 | + '
\n' 285 | + '
\n ' 286 | + '
'); 287 | done(); 288 | }) 289 | }) 290 | }) 291 | 292 | describe('highlight()', function() { 293 | it('should highlight node', function(done) { 294 | node.highlight(function(err, resp) { 295 | assert.strictEqual(err, null); 296 | done(); 297 | }) 298 | }) 299 | }) 300 | 301 | /* MUST BE LAST */ 302 | describe('remove()', function() { 303 | it('should remove node', function(done) { 304 | node.remove(function(err, nextSibling) { 305 | assert.strictEqual(err, null); 306 | assert.equal(nextSibling.getAttribute("id"), "test3"); 307 | 308 | doc.querySelectorAll(".item", function(err, list) { 309 | assert.strictEqual(err, null); 310 | assert.equal(list.length, 2); 311 | done(); 312 | }) 313 | }) 314 | }) 315 | 316 | it("should err if performing further operations after release()", function(done) { 317 | node.release(function(err) { 318 | assert.strictEqual(err, null); 319 | 320 | node.innerHTML(function(err, text) { 321 | assert.equal(err.message, "TypeError: node is null") 322 | assert.equal(err.toString(), "unknownError: TypeError: node is null"); 323 | done(); 324 | }) 325 | }) 326 | }) 327 | }) 328 | 329 | 330 | --------------------------------------------------------------------------------