Skip to content

Commit

Permalink
[js] Add support for Firefox WebExtensions. (SeleniumHQ#3846)
Browse files Browse the repository at this point in the history
  • Loading branch information
tobli authored and jleyba committed Apr 24, 2017
1 parent 3706c07 commit 2f1df98
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 35 deletions.
106 changes: 72 additions & 34 deletions javascript/node/selenium-webdriver/firefox/extension.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ var RdfRoot;


/**
* Extracts the details needed to install an add-on.
* @param {string} addonPath Path to the extension directory.
* Parse an install.rdf for a Firefox add-on.
* @param {string} rdf The contents of install.rdf for the add-on.
* @return {!Promise<!AddonDetails>} A promise for the add-on details.
*/
function getDetails(addonPath) {
return readManifest(addonPath).then(function(doc) {
function parseInstallRdf(rdf) {
return parseXml(rdf).then(function(doc) {
var em = getNamespaceId(doc, 'http://www.mozilla.org/2004/em-rdf#');
var rdf = getNamespaceId(
doc, 'http://www.w3.org/1999/02/22-rdf-syntax-ns#');
Expand All @@ -110,6 +110,18 @@ function getDetails(addonPath) {
return details;
});

function parseXml(text) {
return new Promise((resolve, reject) => {
xml.parseString(text, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}

function getNodeText(node, name) {
return node[name] && node[name][0] || '';
}
Expand Down Expand Up @@ -138,46 +150,72 @@ function getDetails(addonPath) {


/**
* Reads the manifest for a Firefox add-on.
* @param {string} addonPath Path to a Firefox add-on as a xpi or an extension.
* @return {!Promise<!Object>} A promise for the parsed manifest.
* Parse a manifest for a Firefox WebExtension.
* @param {!Object} json JSON representation of the manifest.
* @return {!AddonDetails} The add-on details.
*/
function readManifest(addonPath) {
var manifest;
function parseManifestJson({name, version, applications}) {
if (!(applications && applications.gecko && applications.gecko.id)) {
throw new AddonFormatError('Could not find add-on ID for ' + addonPath);
}

if (addonPath.slice(-4) === '.xpi') {
manifest = new Promise((resolve, reject) => {
let zip = new AdmZip(addonPath);
return {id: applications.gecko.id, name, version, unpack: false};
}

if (!zip.getEntry('install.rdf')) {
reject(new AddonFormatError(
'Could not find install.rdf in ' + addonPath));
return;
}
/**
* Extracts the details needed to install an add-on.
* @param {string} addonPath Path to the extension directory.
* @return {!Promise<!AddonDetails>} A promise for the add-on details.
*/
function getDetails(addonPath) {
return io.stat(addonPath).then((stats) => {
if (stats.isDirectory()) {
return parseDirectory(addonPath);
} else if (addonPath.slice(-4) === '.xpi') {
return parseXpiFile(addonPath);
} else {
throw Error('Add-on path is not an xpi or a directory: ' + addonPath);
}
});

zip.readAsTextAsync('install.rdf', resolve);
});
} else {
manifest = io.stat(addonPath).then(function(stats) {
if (!stats.isDirectory()) {
throw Error(
'Add-on path is neither a xpi nor a directory: ' + addonPath);
}
return io.read(path.join(addonPath, 'install.rdf'));
});
function parseXpiFile(filePath) {
const zip = new AdmZip(filePath);

if (zip.getEntry('install.rdf')) {
return unzip(zip, 'install.rdf').then(parseInstallRdf);
} else if (zip.getEntry('manifest.json')) {
return unzip(zip, 'manifest.json').then(JSON.parse).then(parseManifestJson);
} else {
throw new AddonFormatError('Couldn\'t find install.rdf or manifest.json in ' + filePath);
}
}

return manifest.then(function(content) {
return new Promise((resolve, reject) => {
xml.parseString(content, (err, data) => {
if (err) {
reject(err);
function parseDirectory(dirPath) {
const rdfPath = path.join(dirPath, 'install.rdf');
const jsonPath = path.join(dirPath, 'manifest.json');

return Promise.all([io.exists(rdfPath), io.exists(jsonPath)])
.then(([rdfExists, jsonExists]) => {
if (rdfExists) {
return io.read(rdfPath).then(parseInstallRdf);
} else if (jsonExists) {
return io.read(jsonPath).then(JSON.parse).then(parseManifestJson);
} else {
resolve(data);
throw new AddonFormatError('Couldn\'t find install.rdf or manifest.json in ' + dirPath);
}
});
}

function unzip(zip, file) {
return new Promise((resolve, reject) => {
return zip.readAsTextAsync(file, (data, err) => {
if (data)
return resolve(data);
else
return reject(err);
});
});
});
}
}


Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ var extension = require('../../firefox/extension'),
var JETPACK_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/jetpack-sample.xpi');
var NORMAL_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/sample.xpi');
'../../lib/test/data/firefox/sample.xpi');
var WEBEXTENSION_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/webextension.xpi');

var JETPACK_EXTENSION_ID = 'jid1-EaXX7k0wwiZR7w@jetpack';
var NORMAL_EXTENSION_ID = 'sample@seleniumhq.org';
var WEBEXTENSION_EXTENSION_ID = 'webextensions-selenium-example@example.com';


describe('extension', function() {
Expand Down Expand Up @@ -75,6 +78,27 @@ describe('extension', function() {
});
});

it('can install a webextension xpi file', function() {
return io.tmpDir().then(function(dir) {
return extension.install(WEBEXTENSION_EXTENSION, dir).then(function(id) {
assert.equal(WEBEXTENSION_EXTENSION_ID, id);
var file = path.join(dir, id + '.xpi');
assert.ok(fs.existsSync(file), 'no such file: ' + file);
assert.ok(!fs.statSync(file).isDirectory());

var copiedSha1 = crypto.createHash('sha1')
.update(fs.readFileSync(file))
.digest('hex');

var goldenSha1 = crypto.createHash('sha1')
.update(fs.readFileSync(WEBEXTENSION_EXTENSION))
.digest('hex');

assert.equal(copiedSha1, goldenSha1);
});
});
});

it('can install an extension from a directory', function() {
return io.tmpDir().then(function(srcDir) {
var buf = fs.readFileSync(NORMAL_EXTENSION);
Expand Down
18 changes: 18 additions & 0 deletions javascript/node/selenium-webdriver/test/firefox/firefox_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ var JETPACK_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/jetpack-sample.xpi');
var NORMAL_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/sample.xpi');
var WEBEXTENSION_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/webextension.xpi');


test.suite(function(env) {
Expand Down Expand Up @@ -128,6 +130,22 @@ test.suite(function(env) {
});
});

test.it('can start Firefox with a webextension extension', function() {
let profile = new firefox.Profile();
profile.addExtension(WEBEXTENSION_EXTENSION);

let options = new firefox.Options().setProfile(profile);

return runWithFirefoxDev(options, function*() {
yield driver.get(test.Pages.echoPage);

let footer =
yield driver.findElement({id: 'webextensions-selenium-example'});
let text = yield footer.getText();
assert(text).equalTo('Content injected by webextensions-selenium-example');
});
});

test.it('can start Firefox with multiple extensions', function() {
let profile = new firefox.Profile();
profile.addExtension(JETPACK_EXTENSION);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,13 @@ var JETPACK_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/jetpack-sample.xpi');
var NORMAL_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/sample.xpi');
var WEBEXTENSION_EXTENSION = path.join(__dirname,
'../../lib/test/data/firefox/webextension.xpi');

var JETPACK_EXTENSION_ID = 'jid1-EaXX7k0wwiZR7w@jetpack.xpi';
var NORMAL_EXTENSION_ID = 'sample@seleniumhq.org';
var WEBDRIVER_EXTENSION_ID = 'fxdriver@googlecode.com';
var WEBEXTENSION_EXTENSION_ID = 'webextensions-selenium-example@example.com';



Expand Down Expand Up @@ -153,12 +156,14 @@ describe('Profile', function() {
var profile = new Profile();
profile.addExtension(JETPACK_EXTENSION);
profile.addExtension(NORMAL_EXTENSION);
profile.addExtension(WEBEXTENSION_EXTENSION);

return profile.writeToDisk().then(function(dir) {
dir = path.join(dir, 'extensions');
assert.ok(fs.existsSync(path.join(dir, JETPACK_EXTENSION_ID)));
assert.ok(fs.existsSync(path.join(dir, NORMAL_EXTENSION_ID)));
assert.ok(fs.existsSync(path.join(dir, WEBDRIVER_EXTENSION_ID)));
assert.ok(fs.existsSync(path.join(dir, WEBEXTENSION_EXTENSION_ID + '.xpi')));
});
});
});
Expand Down

0 comments on commit 2f1df98

Please sign in to comment.