├── 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 |
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 |
--------------------------------------------------------------------------------