Skip to content

Commit 4559654

Browse files
committed
feat: Added remote Firefox reloading support
1 parent 44b36a4 commit 4559654

File tree

3 files changed

+374
-0
lines changed

3 files changed

+374
-0
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"fx-runner": "1.0.1",
4545
"minimatch": "3.0.0",
4646
"mz": "2.4.0",
47+
"node-firefox-connect": "1.2.0",
4748
"sign-addon": "0.0.1",
4849
"source-map-support": "0.4.0",
4950
"stream-to-promise": "1.1.0",

src/firefox/remote.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/* @flow */
2+
import {createLogger} from '../util/logger';
3+
import {WebExtError} from '../errors';
4+
import defaultFirefoxConnector from 'node-firefox-connect';
5+
6+
const log = createLogger(__filename);
7+
8+
9+
export default function connect(
10+
port: number = 6000,
11+
{connectToFirefox=defaultFirefoxConnector}: Object = {}): Promise {
12+
return connectToFirefox(port)
13+
.then((client) => {
14+
log.info('Connected to the Firefox remote debugger');
15+
return new RemoteFirefox(client);
16+
});
17+
}
18+
19+
20+
export class RemoteFirefox {
21+
client: Object;
22+
checkForAddonReloading: Function;
23+
checkedForAddonReloading: boolean;
24+
addonRequest: Function;
25+
getInstalledAddon: Function;
26+
27+
constructor(client: Object) {
28+
this.client = client;
29+
this.checkedForAddonReloading = false;
30+
}
31+
32+
disconnect() {
33+
this.client.disconnect();
34+
}
35+
36+
addonRequest(addon: Object, request: string): Promise {
37+
return new Promise((resolve, reject) => {
38+
this.client.client.makeRequest(
39+
{to: addon.actor, type: request}, (response) => {
40+
if (response.error) {
41+
reject(
42+
new WebExtError(`${request} response error: ${response.error}`));
43+
} else {
44+
resolve(response);
45+
}
46+
});
47+
});
48+
}
49+
50+
getInstalledAddon(addonId: string): Promise {
51+
return new Promise(
52+
(resolve, reject) => {
53+
this.client.request('listAddons', (error, response) => {
54+
if (error) {
55+
reject(new WebExtError(
56+
`Remote Firefox: listAddons() error: ${error}`));
57+
} else {
58+
resolve(response.addons);
59+
}
60+
});
61+
})
62+
.then((addons) => {
63+
for (const addon of addons) {
64+
if (addon.id === addonId) {
65+
return addon;
66+
}
67+
}
68+
log.debug(
69+
`Remote Firefox has these addons: ${addons.map((a) => a.id)}`);
70+
throw new WebExtError(
71+
'The remote Firefox does not have your extension installed');
72+
});
73+
}
74+
75+
checkForAddonReloading(addon: Object): Promise {
76+
if (this.checkedForAddonReloading) {
77+
// We only need to check once if reload() is supported.
78+
return Promise.resolve(addon);
79+
} else {
80+
return this.addonRequest(addon, 'requestTypes')
81+
.then((response) => {
82+
if (response.requestTypes.indexOf('reload') === -1) {
83+
log.debug(
84+
`Remote Firefox only supports: ${response.requestTypes}`);
85+
throw new WebExtError(
86+
'This Firefox version does not support addon.reload() yet');
87+
} else {
88+
this.checkedForAddonReloading = true;
89+
return addon;
90+
}
91+
});
92+
}
93+
}
94+
95+
reloadAddon(addonId: string): Promise {
96+
return this.getInstalledAddon(addonId)
97+
.then((addon) => this.checkForAddonReloading(addon))
98+
.then((addon) => {
99+
log.info(
100+
`${(new Date()).toTimeString()}: Reloaded extension: ${addon.id}`);
101+
return this.addonRequest(addon, 'reload');
102+
});
103+
}
104+
}

tests/test-firefox/test.remote.js

Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
/* @flow */
2+
import {describe, it} from 'mocha';
3+
import {assert} from 'chai';
4+
import sinon from 'sinon';
5+
6+
import {WebExtError, onlyInstancesOf} from '../../src/errors';
7+
import {makeSureItFails} from '../helpers';
8+
import {default as defaultConnector, RemoteFirefox}
9+
from '../../src/firefox/remote';
10+
11+
12+
describe('firefox.remote', () => {
13+
14+
describe('connect', () => {
15+
16+
function prepareConnection(port=undefined, options={}) {
17+
options = {
18+
connectToFirefox: sinon.spy(() => Promise.resolve({})),
19+
...options,
20+
};
21+
const connect = defaultConnector(port, options);
22+
return {options, connect};
23+
}
24+
25+
it('resolves with a RemoteFirefox instance', () => {
26+
return prepareConnection().connect.then((client) => {
27+
assert.instanceOf(client, RemoteFirefox);
28+
});
29+
});
30+
31+
it('connects on the default port', () => {
32+
const {connect, options} = prepareConnection();
33+
return connect.then(() => {
34+
assert.equal(options.connectToFirefox.firstCall.args[0], 6000);
35+
});
36+
});
37+
38+
it('lets you configure the port', () => {
39+
const {connect, options} = prepareConnection(7000);
40+
return connect.then(() => {
41+
assert.equal(options.connectToFirefox.args[0], 7000);
42+
});
43+
});
44+
45+
});
46+
47+
describe('RemoteFirefox', () => {
48+
49+
function fakeClient(
50+
{requestResult={}, requestError=null,
51+
makeRequestResult={}, makeRequestError=null}: Object = {}) {
52+
return {
53+
disconnect: sinon.spy(() => {}),
54+
request: sinon.spy(
55+
(request, callback) => callback(requestError, requestResult)),
56+
// This is client.client, the actual underlying connection.
57+
client: {
58+
makeRequest: sinon.spy((request, callback) => {
59+
//
60+
// The real function returns a response object that you
61+
// use like this:
62+
// if (response.error) {
63+
// ...
64+
// } else {
65+
// response.something; // ...
66+
// }
67+
//
68+
if (makeRequestError) {
69+
callback({error: makeRequestError});
70+
} else {
71+
callback(makeRequestResult);
72+
}
73+
}),
74+
},
75+
};
76+
}
77+
78+
function fakeAddon() {
79+
return {id: 'some-id', actor: 'serv1.localhost'};
80+
}
81+
82+
function makeInstance(client=fakeClient()) {
83+
return new RemoteFirefox(client);
84+
}
85+
86+
describe('disconnect', () => {
87+
it('lets you disconnect', () => {
88+
const client = fakeClient();
89+
const conn = makeInstance(client);
90+
conn.disconnect();
91+
assert.equal(client.disconnect.called, true);
92+
});
93+
});
94+
95+
describe('addonRequest', () => {
96+
97+
it('makes requests to an add-on actor', () => {
98+
const addon = fakeAddon();
99+
const stubResponse = {requestTypes: ['reload']};
100+
const client = fakeClient({
101+
makeRequestResult: stubResponse,
102+
});
103+
104+
const conn = makeInstance(client);
105+
return conn.addonRequest(addon, 'requestTypes')
106+
.then((response) => {
107+
108+
assert.equal(client.client.makeRequest.called, true);
109+
const args = client.client.makeRequest.firstCall.args;
110+
assert.equal(args[0].type, 'requestTypes');
111+
assert.equal(args[0].to, 'serv1.localhost');
112+
113+
assert.deepEqual(response, stubResponse);
114+
});
115+
});
116+
117+
it('throws when add-on actor requests fail', () => {
118+
const addon = fakeAddon();
119+
const client = fakeClient({
120+
makeRequestError: new Error('some actor request failure'),
121+
});
122+
123+
const conn = makeInstance(client);
124+
return conn.addonRequest(addon, 'requestTypes')
125+
.then(makeSureItFails())
126+
.catch(onlyInstancesOf(WebExtError, (error) => {
127+
assert.equal(
128+
error.message,
129+
'requestTypes response error: Error: some actor request failure');
130+
}));
131+
});
132+
});
133+
134+
describe('getInstalledAddon', () => {
135+
136+
it('gets an installed add-on by ID', () => {
137+
const someAddonId = 'some-id';
138+
const client = fakeClient({
139+
requestResult: {
140+
addons: [{id: 'another-id'}, {id: someAddonId}, {id: 'bazinga'}],
141+
},
142+
});
143+
const conn = makeInstance(client);
144+
return conn.getInstalledAddon(someAddonId)
145+
.then((addon) => {
146+
assert.equal(addon.id, someAddonId);
147+
});
148+
});
149+
150+
it('throws an error when the add-on is not installed', () => {
151+
const client = fakeClient({
152+
requestResult: {
153+
addons: [{id: 'one-id'}, {id: 'other-id'}],
154+
},
155+
});
156+
const conn = makeInstance(client);
157+
return conn.getInstalledAddon('missing-id')
158+
.then(makeSureItFails())
159+
.catch(onlyInstancesOf(WebExtError, (error) => {
160+
assert.match(error.message,
161+
/does not have your extension installed/);
162+
}));
163+
});
164+
165+
it('throws an error when listAddons() fails', () => {
166+
const client = fakeClient({
167+
requestError: new Error('some internal error'),
168+
});
169+
const conn = makeInstance(client);
170+
return conn.getInstalledAddon('some-id')
171+
.then(makeSureItFails())
172+
.catch(onlyInstancesOf(WebExtError, (error) => {
173+
assert.equal(
174+
error.message,
175+
'Remote Firefox: listAddons() error: Error: some internal error');
176+
}));
177+
});
178+
});
179+
180+
describe('checkForAddonReloading', () => {
181+
182+
it('checks for reload requestType in remote debugger', () => {
183+
const addon = fakeAddon();
184+
const stubResponse = {requestTypes: ['reload']};
185+
186+
const conn = makeInstance();
187+
conn.addonRequest = sinon.spy(() => Promise.resolve(stubResponse));
188+
189+
return conn.checkForAddonReloading(addon)
190+
.then((returnedAddon) => {
191+
assert.equal(conn.addonRequest.called, true);
192+
const args = conn.addonRequest.firstCall.args;
193+
194+
assert.equal(args[0].id, addon.id);
195+
assert.equal(args[1], 'requestTypes');
196+
197+
assert.deepEqual(returnedAddon, addon);
198+
});
199+
});
200+
201+
it('throws an error if reload is not supported', () => {
202+
const addon = fakeAddon();
203+
const stubResponse = {requestTypes: ['install']};
204+
const conn = makeInstance();
205+
conn.addonRequest = () => Promise.resolve(stubResponse);
206+
207+
return conn.checkForAddonReloading(addon)
208+
.then(makeSureItFails())
209+
.catch(onlyInstancesOf(WebExtError, (error) => {
210+
assert.match(error.message, /does not support addon\.reload/);
211+
}));
212+
});
213+
214+
it('only checks for reloading once', () => {
215+
const addon = fakeAddon();
216+
const conn = makeInstance();
217+
conn.addonRequest =
218+
sinon.spy(() => Promise.resolve({requestTypes: ['reload']}));
219+
return conn.checkForAddonReloading(addon)
220+
.then((addon) => conn.checkForAddonReloading(addon))
221+
.then((returnedAddon) => {
222+
// This should remember not to check a second time.
223+
assert.equal(conn.addonRequest.callCount, 1);
224+
assert.deepEqual(returnedAddon, addon);
225+
});
226+
});
227+
});
228+
229+
describe('reloadAddon', () => {
230+
231+
it('asks the actor to reload the add-on', () => {
232+
const addon = fakeAddon();
233+
const conn = makeInstance();
234+
conn.getInstalledAddon = sinon.spy(() => Promise.resolve(addon));
235+
conn.checkForAddonReloading = (addon) => Promise.resolve(addon);
236+
conn.addonRequest = sinon.spy(() => Promise.resolve({}));
237+
238+
return conn.reloadAddon('some-id')
239+
.then(() => {
240+
assert.equal(conn.getInstalledAddon.called, true);
241+
assert.equal(conn.getInstalledAddon.firstCall.args[0], 'some-id');
242+
243+
assert.equal(conn.addonRequest.called, true);
244+
const requestArgs = conn.addonRequest.firstCall.args;
245+
assert.deepEqual(requestArgs[0], addon);
246+
assert.equal(requestArgs[1], 'reload');
247+
});
248+
});
249+
250+
it('makes sure the addon can be reloaded', () => {
251+
const addon = fakeAddon();
252+
const conn = makeInstance();
253+
conn.getInstalledAddon = () => Promise.resolve(addon);
254+
conn.checkForAddonReloading =
255+
sinon.spy((addon) => Promise.resolve(addon));
256+
257+
return conn.reloadAddon(addon.id)
258+
.then(() => {
259+
assert.equal(conn.checkForAddonReloading.called, true);
260+
assert.deepEqual(conn.checkForAddonReloading.firstCall.args[0],
261+
addon);
262+
});
263+
});
264+
265+
});
266+
267+
});
268+
269+
});

0 commit comments

Comments
 (0)