This commit is contained in:
Fredrik Wollsén 2018-02-15 18:05:06 +02:00
Родитель 445c8266a1
Коммит 16d823e68e
10 изменённых файлов: 228 добавлений и 160 удалений

Просмотреть файл

@ -7,18 +7,17 @@
*/ */
module.exports = { module.exports = {
"parserOptions": { parserOptions: {
"ecmaVersion": 8, ecmaVersion: 8,
"sourceType": "module", sourceType: "module",
"ecmaFeatures": { ecmaFeatures: {
"jsx": false, jsx: false,
"experimentalObjectRestSpread": true, experimentalObjectRestSpread: true,
}, },
}, },
env: { env: {
"es6": true, es6: true,
// 'browser-window': false // 'browser-window': false
}, },
extends: [ extends: [
"eslint:recommended", "eslint:recommended",
@ -28,16 +27,13 @@ module.exports = {
"plugin:mozilla/recommended", "plugin:mozilla/recommended",
], ],
plugins: [ plugins: ["json", "mozilla"],
"json",
"mozilla",
],
rules: { rules: {
"babel/new-cap": "off", "babel/new-cap": "off",
"comma-dangle": ["error", "always-multiline"], "comma-dangle": ["error", "always-multiline"],
"eqeqeq": "error", eqeqeq: "error",
"indent": ["warn", 2, { SwitchCase: 1 }], indent: ["warn", 2, { SwitchCase: 1 }],
"mozilla/no-aArgs": "warn", "mozilla/no-aArgs": "warn",
"mozilla/balanced-listeners": 0, "mozilla/balanced-listeners": 0,
"no-console": "warn", "no-console": "warn",
@ -45,6 +41,6 @@ module.exports = {
"no-unused-vars": "error", "no-unused-vars": "error",
"prefer-const": "warn", "prefer-const": "warn",
"prefer-spread": "error", "prefer-spread": "error",
"semi": ["error", "always"], semi: ["error", "always"],
}, },
}; };

65
addon/bootstrap.js поставляемый
Просмотреть файл

@ -5,17 +5,29 @@
const { utils: Cu } = Components; const { utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm"); Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services", XPCOMUtils.defineLazyModuleGetter(
"resource://gre/modules/Services.jsm"); this,
"Services",
"resource://gre/modules/Services.jsm",
);
const STUDY = "button-icon-preference"; const STUDY = "button-icon-preference";
XPCOMUtils.defineLazyModuleGetter(this, "config", XPCOMUtils.defineLazyModuleGetter(
`resource://${STUDY}/Config.jsm`); this,
XPCOMUtils.defineLazyModuleGetter(this, "studyUtils", "config",
`resource://${STUDY}/StudyUtils.jsm`); `resource://${STUDY}/Config.jsm`,
XPCOMUtils.defineLazyModuleGetter(this, "Feature", );
`resource://${STUDY}/lib/Feature.jsm`); XPCOMUtils.defineLazyModuleGetter(
this,
"studyUtils",
`resource://${STUDY}/StudyUtils.jsm`,
);
XPCOMUtils.defineLazyModuleGetter(
this,
"Feature",
`resource://${STUDY}/lib/Feature.jsm`,
);
/* Example addon-specific module imports. Remember to Unload during shutdown() below. /* Example addon-specific module imports. Remember to Unload during shutdown() below.
@ -32,7 +44,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "Feature",
*/ */
this.Bootstrap = { this.Bootstrap = {
VARIATION_OVERRIDE_PREF: "extensions.button_icon_preference.variation", VARIATION_OVERRIDE_PREF: "extensions.button_icon_preference.variation",
/** /**
@ -41,7 +52,6 @@ this.Bootstrap = {
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
async startup(addonData, reason) { async startup(addonData, reason) {
this.REASONS = studyUtils.REASONS; this.REASONS = studyUtils.REASONS;
this.initLog(); this.initLog();
@ -58,7 +68,10 @@ this.Bootstrap = {
// Check if the user is eligible to run this study using the |isEligible| // Check if the user is eligible to run this study using the |isEligible|
// function when the study is initialized (install or upgrade, the latter // function when the study is initialized (install or upgrade, the latter
// being interpreted as a new install). // being interpreted as a new install).
if (reason === this.REASONS.ADDON_INSTALL || reason === this.REASONS.ADDON_UPGRADE) { if (
reason === this.REASONS.ADDON_INSTALL ||
reason === this.REASONS.ADDON_UPGRADE
) {
// telemetry "enter" ONCE // telemetry "enter" ONCE
studyUtils.firstSeen(); studyUtils.firstSeen();
const eligible = await config.isEligible(); const eligible = await config.isEligible();
@ -82,7 +95,12 @@ this.Bootstrap = {
this.log.debug(`info ${JSON.stringify(studyUtils.info())}`); this.log.debug(`info ${JSON.stringify(studyUtils.info())}`);
// initiate the chrome-privileged part of the study add-on // initiate the chrome-privileged part of the study add-on
this.feature = new Feature(variation, studyUtils, this.REASONS[reason], this.log); this.feature = new Feature(
variation,
studyUtils,
this.REASONS[reason],
this.log,
);
// if you have code to handle expiration / long-timers, it could go here // if you have code to handle expiration / long-timers, it could go here
/* /*
@ -101,14 +119,15 @@ this.Bootstrap = {
/** spec for messages intended for Shield => /** spec for messages intended for Shield =>
* {shield:true,msg=[info|endStudy|telemetry],data=data} * {shield:true,msg=[info|endStudy|telemetry],data=data}
*/ */
browser.runtime.onMessage.addListener(studyUtils.respondToWebExtensionMessage); browser.runtime.onMessage.addListener(
studyUtils.respondToWebExtensionMessage,
);
// other browser.runtime.onMessage handlers for your addon, if any // other browser.runtime.onMessage handlers for your addon, if any
}); });
} }
// start up the chrome-privileged part of the study // start up the chrome-privileged part of the study
this.feature.start(); this.feature.start();
}, },
/* /*
@ -117,8 +136,8 @@ this.Bootstrap = {
*/ */
initLog() { initLog() {
XPCOMUtils.defineLazyGetter(this, "log", () => { XPCOMUtils.defineLazyGetter(this, "log", () => {
const ConsoleAPI = const ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {})
Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI; .ConsoleAPI;
const consoleOptions = { const consoleOptions = {
maxLogLevel: config.log.bootstrap.level, maxLogLevel: config.log.bootstrap.level,
prefix: "TPStudy", prefix: "TPStudy",
@ -136,8 +155,9 @@ this.Bootstrap = {
// choose the variation for this particular user, then set it. // choose the variation for this particular user, then set it.
async selectVariation() { async selectVariation() {
const variation = this.getVariationFromPref(config.weightedVariations) || const variation =
await studyUtils.deterministicVariation(config.weightedVariations); this.getVariationFromPref(config.weightedVariations) ||
(await studyUtils.deterministicVariation(config.weightedVariations));
studyUtils.setVariation(variation); studyUtils.setVariation(variation);
this.log.debug(`studyUtils has config and variation.name: ${variation.name}. this.log.debug(`studyUtils has config and variation.name: ${variation.name}.
Ready to send telemetry`); Ready to send telemetry`);
@ -150,7 +170,9 @@ this.Bootstrap = {
if (name !== "") { if (name !== "") {
const variation = weightedVariations.filter(x => x.name === name)[0]; const variation = weightedVariations.filter(x => x.name === name)[0];
if (!variation) { if (!variation) {
throw new Error(`about:config => ${this.VARIATION_OVERRIDE_PREF} set to ${name}, throw new Error(`about:config => ${
this.VARIATION_OVERRIDE_PREF
} set to ${name},
but no variation with that name exists.`); but no variation with that name exists.`);
} }
return variation; return variation;
@ -167,8 +189,9 @@ this.Bootstrap = {
async shutdown(addonData, reason) { async shutdown(addonData, reason) {
this.log.debug("shutdown", this.REASONS[reason] || reason); this.log.debug("shutdown", this.REASONS[reason] || reason);
const isUninstall = (reason === this.REASONS.ADDON_UNINSTALL const isUninstall =
|| reason === this.REASONS.ADDON_DISABLE); reason === this.REASONS.ADDON_UNINSTALL ||
reason === this.REASONS.ADDON_DISABLE;
if (isUninstall) { if (isUninstall) {
this.log.debug("uninstall or disable"); this.log.debug("uninstall or disable");
} }

Просмотреть файл

@ -126,7 +126,7 @@ class Feature {
}); });
feature.studyUtils.endStudy("introduction-leave-study"); feature.studyUtils.endStudy("introduction-leave-study");
}, },
} },
], ],
// callback for nb events // callback for nb events
null null

Просмотреть файл

@ -1,13 +1,11 @@
{ {
"env": { "env": {
"browser": true, "browser": true,
"es6": true, "es6": true,
"webextensions": true "webextensions": true
}, },
"extends": [ "extends": ["eslint:recommended"],
"eslint:recommended" "rules": {
], "no-console": "warn"
"rules": { }
"no-console": "warn"
}
} }

Просмотреть файл

@ -13,7 +13,6 @@
* - Only the webExtension can initiate messages. see `msgStudyUtils("info")` below. * - Only the webExtension can initiate messages. see `msgStudyUtils("info")` below.
*/ */
/** Re-usable code for talking to `studyUtils` using `browser.runtime.sendMessage` /** Re-usable code for talking to `studyUtils` using `browser.runtime.sendMessage`
* - Host listens and responds at `bootstrap.js`: * - Host listens and responds at `bootstrap.js`:
* *
@ -27,7 +26,8 @@
*/ */
async function msgStudyUtils(msg, data) { async function msgStudyUtils(msg, data) {
const allowed = ["endStudy", "telemetry", "info"]; const allowed = ["endStudy", "telemetry", "info"];
if (!allowed.includes(msg)) throw new Error(`shieldUtils doesn't know ${msg}, only knows ${allowed}`); if (!allowed.includes(msg))
throw new Error(`shieldUtils doesn't know ${msg}, only knows ${allowed}`);
try { try {
// the "shield" key is how the Host listener knows it's for shield. // the "shield" key is how the Host listener knows it's for shield.
return await browser.runtime.sendMessage({ shield: true, msg, data }); return await browser.runtime.sendMessage({ shield: true, msg, data });
@ -56,28 +56,31 @@ function telemetry(data) {
function throwIfInvalid(obj) { function throwIfInvalid(obj) {
// Check: all keys and values must be strings, // Check: all keys and values must be strings,
for (const k in obj) { for (const k in obj) {
if (typeof k !== 'string') throw new Error(`key ${k} not a string`); if (typeof k !== "string") throw new Error(`key ${k} not a string`);
if (typeof obj[k] !== 'string') throw new Error(`value ${k} ${obj[k]} not a string`); if (typeof obj[k] !== "string")
throw new Error(`value ${k} ${obj[k]} not a string`);
} }
return true return true;
} }
throwIfInvalid(data); throwIfInvalid(data);
return msgStudyUtils("telemetry", data); return msgStudyUtils("telemetry", data);
} }
class BrowserActionButtonChoiceFeature { class BrowserActionButtonChoiceFeature {
/** /**
* - set image, text, click handler (telemetry) * - set image, text, click handler (telemetry)
* - tell Legacy Addon to send * - tell Legacy Addon to send
*/ */
constructor(variation) { constructor(variation) {
console.log("initilizing BrowserActionButtonChoiceFeature:", variation.name); console.log(
"initilizing BrowserActionButtonChoiceFeature:",
variation.name,
);
this.timesClickedInSession = 0; this.timesClickedInSession = 0;
// modify BrowserAction (button) ui for this particular {variation} // modify BrowserAction (button) ui for this particular {variation}
console.log("path:", `icons/${variation.name}.svg`) console.log("path:", `icons/${variation.name}.svg`);
browser.browserAction.setIcon({ path: `icons/${variation.name}.svg` }); browser.browserAction.setIcon({ path: `icons/${variation.name}.svg` });
browser.browserAction.setTitle({ title: variation.name }); browser.browserAction.setTitle({ title: variation.name });
browser.browserAction.onClicked.addListener(() => this.handleButtonClick()); browser.browserAction.onClicked.addListener(() => this.handleButtonClick());
@ -92,15 +95,20 @@ class BrowserActionButtonChoiceFeature {
// note: doesn't persist across a session, unless you use localStorage or similar. // note: doesn't persist across a session, unless you use localStorage or similar.
this.timesClickedInSession += 1; this.timesClickedInSession += 1;
console.log("got a click", this.timesClickedInSession); console.log("got a click", this.timesClickedInSession);
browser.browserAction.setBadgeText({ text: this.timesClickedInSession.toString() }); browser.browserAction.setBadgeText({
text: this.timesClickedInSession.toString(),
});
// telemetry: FIRST CLICK // telemetry: FIRST CLICK
if (this.timesClickedInSession == 1) { if (this.timesClickedInSession == 1) {
telemetry({ "event": "button-first-click-in-session" }); telemetry({ event: "button-first-click-in-session" });
} }
// telemetry EVERY CLICK // telemetry EVERY CLICK
telemetry({ "event": "button-click", timesClickedInSession: "" + this.timesClickedInSession }); telemetry({
event: "button-click",
timesClickedInSession: "" + this.timesClickedInSession,
});
// webExtension-initiated ending for "used-often" // webExtension-initiated ending for "used-often"
// //
@ -119,13 +127,15 @@ class BrowserActionButtonChoiceFeature {
* 3. initialize the feature, using our specific variation * 3. initialize the feature, using our specific variation
*/ */
function runOnce() { function runOnce() {
msgStudyUtils("info").then( msgStudyUtils("info")
({ variation }) => new BrowserActionButtonChoiceFeature(variation) .then(({ variation }) => new BrowserActionButtonChoiceFeature(variation))
).catch(function defaultSetup() { .catch(function defaultSetup() {
// Errors here imply that this is NOT embedded. // Errors here imply that this is NOT embedded.
console.log("you must be running as part of `web-ext`. You get 'corn dog'!"); console.log(
new BrowserActionButtonChoiceFeature({ "name": "isolatedcorndog" }) "you must be running as part of `web-ext`. You get 'corn dog'!",
}); );
new BrowserActionButtonChoiceFeature({ name: "isolatedcorndog" });
});
} }
// actually start // actually start

Просмотреть файл

@ -68,7 +68,7 @@
"eslint": "eslint . --ext jsm --ext js --ext json", "eslint": "eslint . --ext jsm --ext js --ext json",
"eslint-fix": "eslint . --ext jsm --ext js --ext json --fix", "eslint-fix": "eslint . --ext jsm --ext js --ext json --fix",
"firefox": "export XPI=dist/linked-addon.xpi && npm run build && node run-firefox.js", "firefox": "export XPI=dist/linked-addon.xpi && npm run build && node run-firefox.js",
"format": "prettier '**/*.{css,js,json}' --trailing-comma=all --ignore-path=.eslintignore --print-width 160 --write", "format": "prettier '**/*.{css,js,json}' --trailing-comma=all --ignore-path=.eslintignore --write && npm run eslint-fix",
"harness_test": "export XPI=dist/linked-addon.xpi && mocha test/functional_tests.js --retry 2 --reporter json", "harness_test": "export XPI=dist/linked-addon.xpi && mocha test/functional_tests.js --retry 2 --reporter json",
"lint": "npm-run-all lint:*", "lint": "npm-run-all lint:*",
"lint-build:addons-linter": "# actually a post build test: bin/addonLintTest ' + require('./package.json').name", "lint-build:addons-linter": "# actually a post build test: bin/addonLintTest ' + require('./package.json').name",

Просмотреть файл

@ -24,7 +24,6 @@ const {
MODIFIER_KEY, MODIFIER_KEY,
} = require("./test/utils"); } = require("./test/utils");
const HELP = ` const HELP = `
env vars: env vars:
@ -44,12 +43,11 @@ Future will clean up this interface a bit!
`; `;
const minimistHandler = { const minimistHandler = {
boolean: [ "help" ], boolean: ["help"],
alias: { h: "help", v: "version" }, alias: { h: "help", v: "version" },
"--": true, "--": true,
}; };
(async() => { (async() => {
const minimist = require("minimist"); const minimist = require("minimist");
const parsedArgs = minimist(process.argv.slice(2), minimistHandler); const parsedArgs = minimist(process.argv.slice(2), minimistHandler);
@ -80,8 +78,9 @@ const minimistHandler = {
const openBrowserConsole = Key.chord(MODIFIER_KEY, Key.SHIFT, "j"); const openBrowserConsole = Key.chord(MODIFIER_KEY, Key.SHIFT, "j");
await urlBar.sendKeys(openBrowserConsole); await urlBar.sendKeys(openBrowserConsole);
console.log("The addon should now be loaded and you should be able to interact with the addon in the newly opened Firefox instance."); console.log(
"The addon should now be loaded and you should be able to interact with the addon in the newly opened Firefox instance.",
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} }

Просмотреть файл

@ -17,14 +17,16 @@ const utils = require("./utils");
/* Part 1: Utilities */ /* Part 1: Utilities */
async function getShieldPingsAfterTimestamp(driver, ts) { async function getShieldPingsAfterTimestamp(driver, ts) {
return utils.getTelemetryPings(driver, { type: ["shield-study", "shield-study-addon"], timestamp: ts }); return utils.getTelemetryPings(driver, {
type: ["shield-study", "shield-study-addon"],
timestamp: ts,
});
} }
function summarizePings(pings) { function summarizePings(pings) {
return pings.map(p => [p.payload.type, p.payload.data]); return pings.map(p => [p.payload.type, p.payload.data]);
} }
async function getNotification(driver) { async function getNotification(driver) {
return utils.getChromeElementBy.tagName(driver, "notification"); return utils.getChromeElementBy.tagName(driver, "notification");
} }
@ -33,7 +35,6 @@ async function getFirstButton(driver) {
return utils.getChromeElementBy.className(driver, "notification-button"); return utils.getChromeElementBy.className(driver, "notification-button");
} }
/* Part 2: The Tests */ /* Part 2: The Tests */
describe("basic functional tests", function() { describe("basic functional tests", function() {
@ -58,17 +59,14 @@ describe("basic functional tests", function() {
// collect sent pings // collect sent pings
pings = await getShieldPingsAfterTimestamp(driver, beginTime); pings = await getShieldPingsAfterTimestamp(driver, beginTime);
// console.log(pingsReport(pings).report); // console.log(pingsReport(pings).report);
}); });
after(async() => { after(async() => {
driver.quit(); driver.quit();
}); });
beforeEach(async() => { beforeEach(async() => {});
}); afterEach(async() => {});
afterEach(async() => {
});
/* Expected behaviour: /* Expected behaviour:
@ -85,17 +83,33 @@ describe("basic functional tests", function() {
}); });
it("at least one shield-study telemetry ping with study_state=installed", async() => { it("at least one shield-study telemetry ping with study_state=installed", async() => {
const foundPings = utils.searchTelemetry([ const foundPings = utils.searchTelemetry(
ping => ping.type === "shield-study" && ping.payload.data.study_state === "installed", [
], pings); ping =>
assert(foundPings.length > 0, "at least one shield-study telemetry ping with study_state=installed"); ping.type === "shield-study" &&
ping.payload.data.study_state === "installed",
],
pings,
);
assert(
foundPings.length > 0,
"at least one shield-study telemetry ping with study_state=installed",
);
}); });
it("at least one shield-study telemetry ping with study_state=enter", async() => { it("at least one shield-study telemetry ping with study_state=enter", async() => {
const foundPings = utils.searchTelemetry([ const foundPings = utils.searchTelemetry(
ping => ping.type === "shield-study" && ping.payload.data.study_state === "enter", [
], pings); ping =>
assert(foundPings.length > 0, "at least one shield-study telemetry ping with study_state=enter"); ping.type === "shield-study" &&
ping.payload.data.study_state === "enter",
],
pings,
);
assert(
foundPings.length > 0,
"at least one shield-study telemetry ping with study_state=enter",
);
}); });
it("telemetry: has entered, installed, etc", function() { it("telemetry: has entered, installed, etc", function() {
@ -105,21 +119,21 @@ describe("basic functional tests", function() {
[ [
"shield-study-addon", "shield-study-addon",
{ {
"attributes": { attributes: {
"event": "introduction-shown", event: "introduction-shown",
}, },
}, },
], ],
[ [
"shield-study", "shield-study",
{ {
"study_state": "installed", study_state: "installed",
}, },
], ],
[ [
"shield-study", "shield-study",
{ {
"study_state": "enter", study_state: "enter",
}, },
], ],
]; ];
@ -129,7 +143,9 @@ describe("basic functional tests", function() {
describe("introduction / orientation bar", function() { describe("introduction / orientation bar", function() {
it("exists, carries study config", async() => { it("exists, carries study config", async() => {
const notice = await getNotification(driver); const notice = await getNotification(driver);
const noticeConfig = JSON.parse(await notice.getAttribute("data-study-config")); const noticeConfig = JSON.parse(
await notice.getAttribute("data-study-config"),
);
assert(noticeConfig.name); assert(noticeConfig.name);
assert(noticeConfig.weight); assert(noticeConfig.weight);
}); });
@ -153,15 +169,14 @@ describe("basic functional tests", function() {
[ [
"shield-study-addon", "shield-study-addon",
{ {
"attributes": { attributes: {
"event": "introduction-accept", event: "introduction-accept",
}, },
}, },
], ],
]; ];
// this would add new telemetry // this would add new telemetry
assert.deepEqual(expected, observed, "telemetry pings do not match"); assert.deepEqual(expected, observed, "telemetry pings do not match");
}); });
it("TBD click on NO uninstalls addon", async() => { it("TBD click on NO uninstalls addon", async() => {

Просмотреть файл

@ -13,21 +13,21 @@ const { spawn } = require("child_process");
// Promise wrapper around childProcess.spawn() // Promise wrapper around childProcess.spawn()
function spawnProcess(command, args) { function spawnProcess(command, args) {
return new Promise((resolve) => { return new Promise(resolve => {
const childProcess = spawn(command, args); const childProcess = spawn(command, args);
const stderrArray = []; const stderrArray = [];
const stdoutArray = []; const stdoutArray = [];
childProcess.stdout.on("data", (data) => { childProcess.stdout.on("data", data => {
stdoutArray.push(data.toString()); // data is of type Buffer stdoutArray.push(data.toString()); // data is of type Buffer
}); });
childProcess.stderr.on("data", (data) => { childProcess.stderr.on("data", data => {
// TODO reject upon error? // TODO reject upon error?
stderrArray.push(data.toString()); // data is of type Buffer stderrArray.push(data.toString()); // data is of type Buffer
}); });
childProcess.on("close", (code) => { childProcess.on("close", code => {
// TODO reject upon error? // TODO reject upon error?
console.log("Test suite completed."); console.log("Test suite completed.");
resolve({ code, stdoutArray, stderrArray }); resolve({ code, stdoutArray, stderrArray });
@ -44,7 +44,9 @@ async function main() {
console.log(`Currently running test suite #${i}.`); console.log(`Currently running test suite #${i}.`);
const childProcesses = []; const childProcesses = [];
// NOTE Parallel tests seem to introduce more errors. // NOTE Parallel tests seem to introduce more errors.
childProcesses.push(spawnProcess("npm", ["run", "--silent", "harness_test"])); childProcesses.push(
spawnProcess("npm", ["run", "--silent", "harness_test"]),
);
// TODO Promise.all() will reject upon a single error, is this an issue? // TODO Promise.all() will reject upon a single error, is this an issue?
try { try {
@ -57,11 +59,13 @@ async function main() {
const mochaOutput = JSON.parse(rawOutput.join("")); const mochaOutput = JSON.parse(rawOutput.join(""));
for (const failedTest of mochaOutput.failures) { for (const failedTest of mochaOutput.failures) {
console.log(failedTest.err); console.log(failedTest.err);
if (!(failedTestCounts.has(failedTest.fullTitle))) { if (!failedTestCounts.has(failedTest.fullTitle)) {
failedTestCounts.set(failedTest.fullTitle, 0); failedTestCounts.set(failedTest.fullTitle, 0);
} }
failedTestCounts.set(failedTest.fullTitle, failedTestCounts.set(
failedTestCounts.get(failedTest.fullTitle) + 1); failedTest.fullTitle,
failedTestCounts.get(failedTest.fullTitle) + 1,
);
} }
} catch (e) { } catch (e) {
console.log(`JSON parsing error: ${e}`); console.log(`JSON parsing error: ${e}`);
@ -74,12 +78,15 @@ async function main() {
} }
console.log(failedTestCounts); console.log(failedTestCounts);
for (const pair of failedTestCounts) { for (const pair of failedTestCounts) {
fs.appendFile(`test_harness_output_${new Date().toISOString()}.txt`, `${pair[0]}: ${pair[1]}\n`, fs.appendFile(
(err) => { `test_harness_output_${new Date().toISOString()}.txt`,
`${pair[0]}: ${pair[1]}\n`,
err => {
if (err) { if (err) {
console.log(`fs.writeFile errror: ${err}`); console.log(`fs.writeFile errror: ${err}`);
} }
}); },
);
} }
} }

Просмотреть файл

@ -42,12 +42,12 @@ const FIREFOX_PREFERENCES = {
"extensions.button_icon_preference.variation": "kittens", "extensions.button_icon_preference.variation": "kittens",
/** WARNING: gecko webdriver sets many additional prefs at: /** WARNING: gecko webdriver sets many additional prefs at:
* https://dxr.mozilla.org/mozilla-central/source/testing/geckodriver/src/prefs.rs * https://dxr.mozilla.org/mozilla-central/source/testing/geckodriver/src/prefs.rs
* *
* In, particular, this DISABLES actual telemetry uploading * In, particular, this DISABLES actual telemetry uploading
* ("toolkit.telemetry.server", Pref::new("https://%(server)s/dummy/telemetry/")), * ("toolkit.telemetry.server", Pref::new("https://%(server)s/dummy/telemetry/")),
* *
*/ */
}; };
// useful if we need to test on a specific version of Firefox // useful if we need to test on a specific version of Firefox
@ -66,14 +66,14 @@ async function promiseActualBinary(binary) {
} }
/** /**
* Uses process.env.FIREFOX_BINARY * Uses process.env.FIREFOX_BINARY
* *
*/ */
module.exports.promiseSetupDriver = async() => { module.exports.promiseSetupDriver = async() => {
const profile = new firefox.Profile(); const profile = new firefox.Profile();
// TODO, allow 'actually send telemetry' here. // TODO, allow 'actually send telemetry' here.
Object.keys(FIREFOX_PREFERENCES).forEach((key) => { Object.keys(FIREFOX_PREFERENCES).forEach(key => {
profile.setPreference(key, FIREFOX_PREFERENCES[key]); profile.setPreference(key, FIREFOX_PREFERENCES[key]);
}); });
@ -85,7 +85,9 @@ module.exports.promiseSetupDriver = async() => {
.forBrowser("firefox") .forBrowser("firefox")
.setFirefoxOptions(options); .setFirefoxOptions(options);
const binaryLocation = await promiseActualBinary(process.env.FIREFOX_BINARY || "nightly"); const binaryLocation = await promiseActualBinary(
process.env.FIREFOX_BINARY || "nightly",
);
// console.log(binaryLocation); // console.log(binaryLocation);
await options.setBinary(new firefox.Binary(binaryLocation)); await options.setBinary(new firefox.Binary(binaryLocation));
@ -96,25 +98,24 @@ module.exports.promiseSetupDriver = async() => {
return driver; return driver;
}; };
/* let's actually just make this a constant */ /* let's actually just make this a constant */
const MODIFIER_KEY = (function getModifierKey() { const MODIFIER_KEY = (function getModifierKey() {
const modifierKey = process.platform === "darwin" ? const modifierKey =
webdriver.Key.COMMAND : webdriver.Key.CONTROL; process.platform === "darwin"
? webdriver.Key.COMMAND
: webdriver.Key.CONTROL;
return modifierKey; return modifierKey;
})(); })();
module.exports.MODIFIER_KEY = MODIFIER_KEY; module.exports.MODIFIER_KEY = MODIFIER_KEY;
// TODO glind general wrapper for 'async with callback'? // TODO glind general wrapper for 'async with callback'?
/* Firefox UI helper functions */ /* Firefox UI helper functions */
// such as: "social-share-button" // such as: "social-share-button"
module.exports.addButtonFromCustomizePanel = async(driver, buttonId) => module.exports.addButtonFromCustomizePanel = async(driver, buttonId) =>
driver.executeAsyncScript((callback) => { driver.executeAsyncScript(callback => {
// see https://dxr.mozilla.org/mozilla-central/rev/211d4dd61025c0a40caea7a54c9066e051bdde8c/browser/base/content/browser-social.js#193 // see https://dxr.mozilla.org/mozilla-central/rev/211d4dd61025c0a40caea7a54c9066e051bdde8c/browser/base/content/browser-social.js#193
Components.utils.import("resource:///modules/CustomizableUI.jsm"); Components.utils.import("resource:///modules/CustomizableUI.jsm");
CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_NAVBAR); CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_NAVBAR);
@ -123,7 +124,7 @@ module.exports.addButtonFromCustomizePanel = async(driver, buttonId) =>
module.exports.removeButtonFromNavbar = async(driver, buttonId) => { module.exports.removeButtonFromNavbar = async(driver, buttonId) => {
try { try {
await driver.executeAsyncScript((callback) => { await driver.executeAsyncScript(callback => {
Components.utils.import("resource:///modules/CustomizableUI.jsm"); Components.utils.import("resource:///modules/CustomizableUI.jsm");
CustomizableUI.removeWidgetFromArea(buttonId); CustomizableUI.removeWidgetFromArea(buttonId);
callback(); callback();
@ -136,7 +137,7 @@ module.exports.removeButtonFromNavbar = async(driver, buttonId) => {
if (e.name === "TimeoutError") { if (e.name === "TimeoutError") {
return false; return false;
} }
throw (e); throw e;
} }
}; };
@ -147,17 +148,29 @@ module.exports.installAddon = async(driver, fileLocation) => {
fileLocation = fileLocation || path.join(process.cwd(), process.env.XPI); fileLocation = fileLocation || path.join(process.cwd(), process.env.XPI);
const executor = driver.getExecutor(); const executor = driver.getExecutor();
executor.defineCommand("installAddon", "POST", "/session/:sessionId/moz/addon/install"); executor.defineCommand(
"installAddon",
"POST",
"/session/:sessionId/moz/addon/install",
);
const installCmd = new cmd.Command("installAddon"); const installCmd = new cmd.Command("installAddon");
const session = await driver.getSession(); const session = await driver.getSession();
installCmd.setParameters({ sessionId: session.getId(), path: fileLocation, temporary: true }); installCmd.setParameters({
sessionId: session.getId(),
path: fileLocation,
temporary: true,
});
return executor.execute(installCmd); return executor.execute(installCmd);
}; };
module.exports.uninstallAddon = async(driver, id) => { module.exports.uninstallAddon = async(driver, id) => {
const executor = driver.getExecutor(); const executor = driver.getExecutor();
executor.defineCommand("uninstallAddon", "POST", "/session/:sessionId/moz/addon/uninstall"); executor.defineCommand(
"uninstallAddon",
"POST",
"/session/:sessionId/moz/addon/uninstall",
);
const uninstallCmd = new cmd.Command("uninstallAddon"); const uninstallCmd = new cmd.Command("uninstallAddon");
const session = await driver.getSession(); const session = await driver.getSession();
@ -165,7 +178,6 @@ module.exports.uninstallAddon = async(driver, id) => {
await executor.execute(uninstallCmd); await executor.execute(uninstallCmd);
}; };
/* this is NOT WORKING FOR UNKNOWN HARD TO EXLAIN REASONS /* this is NOT WORKING FOR UNKNOWN HARD TO EXLAIN REASONS
=> Uncaught WebDriverError: InternalError: too much recursion => Uncaught WebDriverError: InternalError: too much recursion
module.exports.allAddons = async(driver) => { module.exports.allAddons = async(driver) => {
@ -179,20 +191,20 @@ module.exports.allAddons = async(driver) => {
*/ */
/** Returns array of pings of type `type` in reverse sorted order by timestamp /** Returns array of pings of type `type` in reverse sorted order by timestamp
* first element is most recent ping * first element is most recent ping
* *
* as seen in shield-study-addon-util's `utils.jsm` * as seen in shield-study-addon-util's `utils.jsm`
* options * options
* - type: string or array of ping types * - type: string or array of ping types
* - n: positive integer. at most n pings. * - n: positive integer. at most n pings.
* - timestamp: only pings after this timestamp. * - timestamp: only pings after this timestamp.
* - headersOnly: boolean, just the 'headers' for the pings, not the full bodies. * - headersOnly: boolean, just the 'headers' for the pings, not the full bodies.
*/ */
module.exports.getTelemetryPings = async(driver, passedOptions) => { module.exports.getTelemetryPings = async(driver, passedOptions) => {
// callback is how you get the return back from the script // callback is how you get the return back from the script
return driver.executeAsyncScript(async(options, callback) => { return driver.executeAsyncScript(async(options, callback) => {
let {type} = options; let { type } = options;
const { n, timestamp, headersOnly} = options; const { n, timestamp, headersOnly } = options;
Components.utils.import("resource://gre/modules/TelemetryArchive.jsm"); Components.utils.import("resource://gre/modules/TelemetryArchive.jsm");
// {type, id, timestampCreated} // {type, id, timestampCreated}
let pings = await TelemetryArchive.promiseArchivedPingList(); let pings = await TelemetryArchive.promiseArchivedPingList();
@ -207,49 +219,57 @@ module.exports.getTelemetryPings = async(driver, passedOptions) => {
pings.sort((a, b) => b.timestampCreated - a.timestampCreated); pings.sort((a, b) => b.timestampCreated - a.timestampCreated);
if (n) pings = pings.slice(0, n); if (n) pings = pings.slice(0, n);
const pingData = headersOnly ? pings : pings.map(ping => TelemetryArchive.promiseArchivedPingById(ping.id)); const pingData = headersOnly
? pings
: pings.map(ping => TelemetryArchive.promiseArchivedPingById(ping.id));
callback(await Promise.all(pingData)); callback(await Promise.all(pingData));
}, passedOptions); }, passedOptions);
}; };
// TODO glind, this interface feels janky // TODO glind, this interface feels janky
// this feels like it wants to be $ like. // this feels like it wants to be $ like.
// not obvious right now, moving on! // not obvious right now, moving on!
class getChromeElementBy { class getChromeElementBy {
static async _get1(driver, method, selector ) { static async _get1(driver, method, selector) {
driver.setContext(Context.CHROME); driver.setContext(Context.CHROME);
try { try {
return await driver.wait(until.elementLocated( return await driver.wait(
By[method](selector)), 1000); until.elementLocated(By[method](selector)),
1000,
);
} catch (e) { } catch (e) {
// if there an error, the button was not found // if there an error, the button was not found
console.error(e); console.error(e);
return null; return null;
} }
} }
static async id(driver, id) { return this._get1(driver, "id", id); } static async id(driver, id) {
return this._get1(driver, "id", id);
}
static async className(driver, className) { return this._get1(driver, "className", className); } static async className(driver, className) {
return this._get1(driver, "className", className);
}
static async tagName(driver, tagName) { return this._get1(driver, "tagName", tagName); } static async tagName(driver, tagName) {
return this._get1(driver, "tagName", tagName);
}
} }
module.exports.getChromeElementBy = getChromeElementBy; module.exports.getChromeElementBy = getChromeElementBy;
module.exports.promiseUrlBar = (driver) => { module.exports.promiseUrlBar = driver => {
driver.setContext(Context.CHROME); driver.setContext(Context.CHROME);
return driver.wait(until.elementLocated( return driver.wait(until.elementLocated(By.id("urlbar")), 1000);
By.id("urlbar")), 1000);
}; };
module.exports.takeScreenshot = async(driver, filepath = "./screenshot.png") => { module.exports.takeScreenshot = async(
driver,
filepath = "./screenshot.png",
) => {
try { try {
const data = await driver.takeScreenshot(); const data = await driver.takeScreenshot();
return await Fs.outputFile(filepath, return await Fs.outputFile(filepath, data, "base64");
data, "base64");
} catch (screenshotError) { } catch (screenshotError) {
throw screenshotError; throw screenshotError;
} }
@ -275,7 +295,9 @@ module.exports.searchTelemetry = (conditionArray, telemetryArray) => {
const resultingPings = []; const resultingPings = [];
for (const condition of conditionArray) { for (const condition of conditionArray) {
const index = telemetryArray.findIndex(ping => condition(ping)); const index = telemetryArray.findIndex(ping => condition(ping));
if (index === -1) { throw new SearchError(condition); } if (index === -1) {
throw new SearchError(condition);
}
resultingPings.push(telemetryArray[index]); resultingPings.push(telemetryArray[index]);
} }
return resultingPings; return resultingPings;
@ -328,7 +350,6 @@ module.exports.searchTelemetry = (conditionArray, telemetryArray) => {
// } // }
// }; // };
// module.exports.testPanel = async(driver, panelId) => { // module.exports.testPanel = async(driver, panelId) => {
// driver.setContext(Context.CHROME); // driver.setContext(Context.CHROME);
// try { // if we can't find the panel, return false // try { // if we can't find the panel, return false
@ -351,7 +372,6 @@ module.exports.searchTelemetry = (conditionArray, telemetryArray) => {
// } // }
// }; // };
// module.exports.closePanel = async(driver, target = null) => { // module.exports.closePanel = async(driver, target = null) => {
// if (target !== null) { // if (target !== null) {
// target.sendKeys(webdriver.Key.ESCAPE); // target.sendKeys(webdriver.Key.ESCAPE);