Skip to content

Commit

Permalink
feat: Added remote Firefox reloading support
Browse files Browse the repository at this point in the history
  • Loading branch information
kumar303 committed Apr 22, 2016
1 parent 44b36a4 commit 4559654
Show file tree
Hide file tree
Showing 3 changed files with 374 additions and 0 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"fx-runner": "1.0.1",
"minimatch": "3.0.0",
"mz": "2.4.0",
"node-firefox-connect": "1.2.0",
"sign-addon": "0.0.1",
"source-map-support": "0.4.0",
"stream-to-promise": "1.1.0",
Expand Down
104 changes: 104 additions & 0 deletions src/firefox/remote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* @flow */
import {createLogger} from '../util/logger';
import {WebExtError} from '../errors';
import defaultFirefoxConnector from 'node-firefox-connect';

const log = createLogger(__filename);


export default function connect(
port: number = 6000,
{connectToFirefox=defaultFirefoxConnector}: Object = {}): Promise {
return connectToFirefox(port)
.then((client) => {
log.info('Connected to the Firefox remote debugger');
return new RemoteFirefox(client);
});
}


export class RemoteFirefox {
client: Object;
checkForAddonReloading: Function;
checkedForAddonReloading: boolean;
addonRequest: Function;
getInstalledAddon: Function;

constructor(client: Object) {
this.client = client;
this.checkedForAddonReloading = false;
}

disconnect() {
this.client.disconnect();
}

addonRequest(addon: Object, request: string): Promise {
return new Promise((resolve, reject) => {
this.client.client.makeRequest(
{to: addon.actor, type: request}, (response) => {
if (response.error) {
reject(
new WebExtError(`${request} response error: ${response.error}`));
} else {
resolve(response);
}
});
});
}

getInstalledAddon(addonId: string): Promise {
return new Promise(
(resolve, reject) => {
this.client.request('listAddons', (error, response) => {
if (error) {
reject(new WebExtError(
`Remote Firefox: listAddons() error: ${error}`));
} else {
resolve(response.addons);
}
});
})
.then((addons) => {
for (const addon of addons) {
if (addon.id === addonId) {
return addon;
}
}
log.debug(
`Remote Firefox has these addons: ${addons.map((a) => a.id)}`);
throw new WebExtError(
'The remote Firefox does not have your extension installed');
});
}

checkForAddonReloading(addon: Object): Promise {
if (this.checkedForAddonReloading) {
// We only need to check once if reload() is supported.
return Promise.resolve(addon);
} else {
return this.addonRequest(addon, 'requestTypes')
.then((response) => {
if (response.requestTypes.indexOf('reload') === -1) {
log.debug(
`Remote Firefox only supports: ${response.requestTypes}`);
throw new WebExtError(
'This Firefox version does not support addon.reload() yet');
} else {
this.checkedForAddonReloading = true;
return addon;
}
});
}
}

reloadAddon(addonId: string): Promise {
return this.getInstalledAddon(addonId)
.then((addon) => this.checkForAddonReloading(addon))
.then((addon) => {
log.info(
`${(new Date()).toTimeString()}: Reloaded extension: ${addon.id}`);
return this.addonRequest(addon, 'reload');
});
}
}
269 changes: 269 additions & 0 deletions tests/test-firefox/test.remote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
/* @flow */
import {describe, it} from 'mocha';
import {assert} from 'chai';
import sinon from 'sinon';

import {WebExtError, onlyInstancesOf} from '../../src/errors';
import {makeSureItFails} from '../helpers';
import {default as defaultConnector, RemoteFirefox}
from '../../src/firefox/remote';


describe('firefox.remote', () => {

describe('connect', () => {

function prepareConnection(port=undefined, options={}) {
options = {
connectToFirefox: sinon.spy(() => Promise.resolve({})),
...options,
};
const connect = defaultConnector(port, options);
return {options, connect};
}

it('resolves with a RemoteFirefox instance', () => {
return prepareConnection().connect.then((client) => {
assert.instanceOf(client, RemoteFirefox);
});
});

it('connects on the default port', () => {
const {connect, options} = prepareConnection();
return connect.then(() => {
assert.equal(options.connectToFirefox.firstCall.args[0], 6000);
});
});

it('lets you configure the port', () => {
const {connect, options} = prepareConnection(7000);
return connect.then(() => {
assert.equal(options.connectToFirefox.args[0], 7000);
});
});

});

describe('RemoteFirefox', () => {

function fakeClient(
{requestResult={}, requestError=null,
makeRequestResult={}, makeRequestError=null}: Object = {}) {
return {
disconnect: sinon.spy(() => {}),
request: sinon.spy(
(request, callback) => callback(requestError, requestResult)),
// This is client.client, the actual underlying connection.
client: {
makeRequest: sinon.spy((request, callback) => {
//
// The real function returns a response object that you
// use like this:
// if (response.error) {
// ...
// } else {
// response.something; // ...
// }
//
if (makeRequestError) {
callback({error: makeRequestError});
} else {
callback(makeRequestResult);
}
}),
},
};
}

function fakeAddon() {
return {id: 'some-id', actor: 'serv1.localhost'};
}

function makeInstance(client=fakeClient()) {
return new RemoteFirefox(client);
}

describe('disconnect', () => {
it('lets you disconnect', () => {
const client = fakeClient();
const conn = makeInstance(client);
conn.disconnect();
assert.equal(client.disconnect.called, true);
});
});

describe('addonRequest', () => {

it('makes requests to an add-on actor', () => {
const addon = fakeAddon();
const stubResponse = {requestTypes: ['reload']};
const client = fakeClient({
makeRequestResult: stubResponse,
});

const conn = makeInstance(client);
return conn.addonRequest(addon, 'requestTypes')
.then((response) => {

assert.equal(client.client.makeRequest.called, true);
const args = client.client.makeRequest.firstCall.args;
assert.equal(args[0].type, 'requestTypes');
assert.equal(args[0].to, 'serv1.localhost');

assert.deepEqual(response, stubResponse);
});
});

it('throws when add-on actor requests fail', () => {
const addon = fakeAddon();
const client = fakeClient({
makeRequestError: new Error('some actor request failure'),
});

const conn = makeInstance(client);
return conn.addonRequest(addon, 'requestTypes')
.then(makeSureItFails())
.catch(onlyInstancesOf(WebExtError, (error) => {
assert.equal(
error.message,
'requestTypes response error: Error: some actor request failure');
}));
});
});

describe('getInstalledAddon', () => {

it('gets an installed add-on by ID', () => {
const someAddonId = 'some-id';
const client = fakeClient({
requestResult: {
addons: [{id: 'another-id'}, {id: someAddonId}, {id: 'bazinga'}],
},
});
const conn = makeInstance(client);
return conn.getInstalledAddon(someAddonId)
.then((addon) => {
assert.equal(addon.id, someAddonId);
});
});

it('throws an error when the add-on is not installed', () => {
const client = fakeClient({
requestResult: {
addons: [{id: 'one-id'}, {id: 'other-id'}],
},
});
const conn = makeInstance(client);
return conn.getInstalledAddon('missing-id')
.then(makeSureItFails())
.catch(onlyInstancesOf(WebExtError, (error) => {
assert.match(error.message,
/does not have your extension installed/);
}));
});

it('throws an error when listAddons() fails', () => {
const client = fakeClient({
requestError: new Error('some internal error'),
});
const conn = makeInstance(client);
return conn.getInstalledAddon('some-id')
.then(makeSureItFails())
.catch(onlyInstancesOf(WebExtError, (error) => {
assert.equal(
error.message,
'Remote Firefox: listAddons() error: Error: some internal error');
}));
});
});

describe('checkForAddonReloading', () => {

it('checks for reload requestType in remote debugger', () => {
const addon = fakeAddon();
const stubResponse = {requestTypes: ['reload']};

const conn = makeInstance();
conn.addonRequest = sinon.spy(() => Promise.resolve(stubResponse));

return conn.checkForAddonReloading(addon)
.then((returnedAddon) => {
assert.equal(conn.addonRequest.called, true);
const args = conn.addonRequest.firstCall.args;

assert.equal(args[0].id, addon.id);
assert.equal(args[1], 'requestTypes');

assert.deepEqual(returnedAddon, addon);
});
});

it('throws an error if reload is not supported', () => {
const addon = fakeAddon();
const stubResponse = {requestTypes: ['install']};
const conn = makeInstance();
conn.addonRequest = () => Promise.resolve(stubResponse);

return conn.checkForAddonReloading(addon)
.then(makeSureItFails())
.catch(onlyInstancesOf(WebExtError, (error) => {
assert.match(error.message, /does not support addon\.reload/);
}));
});

it('only checks for reloading once', () => {
const addon = fakeAddon();
const conn = makeInstance();
conn.addonRequest =
sinon.spy(() => Promise.resolve({requestTypes: ['reload']}));
return conn.checkForAddonReloading(addon)
.then((addon) => conn.checkForAddonReloading(addon))
.then((returnedAddon) => {
// This should remember not to check a second time.
assert.equal(conn.addonRequest.callCount, 1);
assert.deepEqual(returnedAddon, addon);
});
});
});

describe('reloadAddon', () => {

it('asks the actor to reload the add-on', () => {
const addon = fakeAddon();
const conn = makeInstance();
conn.getInstalledAddon = sinon.spy(() => Promise.resolve(addon));
conn.checkForAddonReloading = (addon) => Promise.resolve(addon);
conn.addonRequest = sinon.spy(() => Promise.resolve({}));

return conn.reloadAddon('some-id')
.then(() => {
assert.equal(conn.getInstalledAddon.called, true);
assert.equal(conn.getInstalledAddon.firstCall.args[0], 'some-id');

assert.equal(conn.addonRequest.called, true);
const requestArgs = conn.addonRequest.firstCall.args;
assert.deepEqual(requestArgs[0], addon);
assert.equal(requestArgs[1], 'reload');
});
});

it('makes sure the addon can be reloaded', () => {
const addon = fakeAddon();
const conn = makeInstance();
conn.getInstalledAddon = () => Promise.resolve(addon);
conn.checkForAddonReloading =
sinon.spy((addon) => Promise.resolve(addon));

return conn.reloadAddon(addon.id)
.then(() => {
assert.equal(conn.checkForAddonReloading.called, true);
assert.deepEqual(conn.checkForAddonReloading.firstCall.args[0],
addon);
});
});

});

});

});

0 comments on commit 4559654

Please sign in to comment.