mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1169179 - Run mozscreenshots as a mochitest-browser-chrome test. r=felipe,glandium
This commit is contained in:
parent
97b9c00329
commit
244a926991
@ -26,5 +26,9 @@ DIRS += [
|
|||||||
if CONFIG['MAKENSISU']:
|
if CONFIG['MAKENSISU']:
|
||||||
DIRS += ['installer/windows']
|
DIRS += ['installer/windows']
|
||||||
|
|
||||||
|
TEST_DIRS += [
|
||||||
|
'tools/mozscreenshots',
|
||||||
|
]
|
||||||
|
|
||||||
DIST_SUBDIR = 'browser'
|
DIST_SUBDIR = 'browser'
|
||||||
export('DIST_SUBDIR')
|
export('DIST_SUBDIR')
|
||||||
|
7
browser/tools/mozscreenshots/browser.ini
Normal file
7
browser/tools/mozscreenshots/browser.ini
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[DEFAULT]
|
||||||
|
subsuite = screenshots
|
||||||
|
support-files =
|
||||||
|
head.js
|
||||||
|
|
||||||
|
[browser_screenshots.js]
|
||||||
|
tags = screenshots
|
18
browser/tools/mozscreenshots/browser_screenshots.js
Normal file
18
browser/tools/mozscreenshots/browser_screenshots.js
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||||
|
|
||||||
|
add_task(function* test() {
|
||||||
|
let { TestRunner } = Cu.import("chrome://mozscreenshots/content/TestRunner.jsm", {});
|
||||||
|
let sets = ["TabsInTitlebar", "Tabs", "WindowSize", "Toolbars", "LightweightThemes"];
|
||||||
|
let setsEnv = env.get("MOZSCREENSHOTS_SETS");
|
||||||
|
if (setsEnv) {
|
||||||
|
sets = setsEnv.trim().split(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
yield TestRunner.start(sets);
|
||||||
|
});
|
20
browser/tools/mozscreenshots/head.js
Normal file
20
browser/tools/mozscreenshots/head.js
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {AddonWatcher} = Cu.import("resource://gre/modules/AddonWatcher.jsm", {});
|
||||||
|
let TestRunner;
|
||||||
|
|
||||||
|
function setup() {
|
||||||
|
requestLongerTimeout(10);
|
||||||
|
|
||||||
|
info("Checking for mozscreenshots extension");
|
||||||
|
AddonManager.getAddonByID("mozscreenshots@mozilla.org", function(aAddon) {
|
||||||
|
isnot(aAddon, null, "The mozscreenshots extension should be installed");
|
||||||
|
AddonWatcher.ignoreAddonPermanently(aAddon.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
add_task(setup);
|
11
browser/tools/mozscreenshots/moz.build
Normal file
11
browser/tools/mozscreenshots/moz.build
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||||
|
# vim: set filetype=python:
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
BROWSER_CHROME_MANIFESTS += ['browser.ini']
|
||||||
|
|
||||||
|
TEST_DIRS += [
|
||||||
|
'mozscreenshots/extension',
|
||||||
|
]
|
@ -0,0 +1,12 @@
|
|||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
TEST_EXTENSIONS_DIR = $(DEPTH)/_tests/testing/mochitest/extensions
|
||||||
|
GENERATED_DIRS = $(TEST_EXTENSIONS_DIR)
|
||||||
|
XPI_PKGNAME = mozscreenshots@mozilla.org
|
||||||
|
|
||||||
|
include $(topsrcdir)/config/rules.mk
|
||||||
|
|
||||||
|
libs::
|
||||||
|
(cd $(DIST)/xpi-stage && tar $(TAR_CREATE_FLAGS) - $(XPI_NAME)) | (cd $(TEST_EXTENSIONS_DIR) && tar -xf -)
|
@ -0,0 +1,175 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Screenshot"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Timer.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/osfile.jsm");
|
||||||
|
|
||||||
|
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
|
||||||
|
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
|
||||||
|
const PREF_LOG_LEVEL = "extensions.mozscreenshots@mozilla.org.loglevel";
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||||
|
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
|
||||||
|
let consoleOptions = {
|
||||||
|
maxLogLevel: "info",
|
||||||
|
maxLogLevelPref: PREF_LOG_LEVEL,
|
||||||
|
prefix: "mozscreenshots",
|
||||||
|
};
|
||||||
|
return new ConsoleAPI(consoleOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
let Screenshot = {
|
||||||
|
_extensionPath: null,
|
||||||
|
_path: null,
|
||||||
|
_imagePrefix: "",
|
||||||
|
_imageExtension: ".png",
|
||||||
|
_screenshotFunction: null,
|
||||||
|
|
||||||
|
init(path, extensionPath, imagePrefix = "") {
|
||||||
|
this._path = path;
|
||||||
|
|
||||||
|
let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||||
|
dir.initWithPath(this._path);
|
||||||
|
if (!dir.exists()) {
|
||||||
|
dir.create(Ci.nsIFile.DIRECTORY_TYPE, parseInt("0755", 8));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._extensionPath = extensionPath;
|
||||||
|
this._imagePrefix = imagePrefix;
|
||||||
|
switch (Services.appinfo.OS) {
|
||||||
|
case "WINNT":
|
||||||
|
this._screenshotFunction = this._screenshotWindows;
|
||||||
|
break;
|
||||||
|
case "Darwin":
|
||||||
|
this._screenshotFunction = this._screenshotOSX;
|
||||||
|
break;
|
||||||
|
case "Linux":
|
||||||
|
this._screenshotFunction = this._screenshotLinux;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unsupported operating system");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
_buildImagePath(baseName) {
|
||||||
|
return OS.Path.join(this._path, this._imagePrefix + baseName + this._imageExtension);
|
||||||
|
},
|
||||||
|
|
||||||
|
// Capture the whole screen using an external application.
|
||||||
|
captureExternal(filename) {
|
||||||
|
let imagePath = this._buildImagePath(filename);
|
||||||
|
return this._screenshotFunction(imagePath).then(() => {
|
||||||
|
log.debug("saved screenshot: " + filename);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
///// helpers /////
|
||||||
|
|
||||||
|
_screenshotWindows(filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
|
||||||
|
exe.append("screenshot.exe");
|
||||||
|
if (!exe.exists()) {
|
||||||
|
exe = this._extensionPath.QueryInterface(Ci.nsIFileURL).file;
|
||||||
|
exe.append("lib");
|
||||||
|
exe.append("screenshot.exe");
|
||||||
|
}
|
||||||
|
let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
|
||||||
|
process.init(exe);
|
||||||
|
|
||||||
|
let args = [filename];
|
||||||
|
process.runAsync(args, args.length, this._processObserver(resolve, reject));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_screenshotOSX: Task.async(function*(filename) {
|
||||||
|
let screencapture = (windowID = null) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Get the screencapture executable
|
||||||
|
let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||||
|
file.initWithPath("/usr/sbin/screencapture");
|
||||||
|
|
||||||
|
let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
|
||||||
|
process.init(file);
|
||||||
|
|
||||||
|
// Run the process.
|
||||||
|
let args = ["-x", "-t", "png"];
|
||||||
|
// Darwin version number for OS X 10.6 is 10.x
|
||||||
|
if (windowID && Services.sysinfo.getProperty("version").indexOf("10.") !== 0) {
|
||||||
|
// Capture only that window on 10.7+
|
||||||
|
args.push("-l");
|
||||||
|
args.push(windowID);
|
||||||
|
}
|
||||||
|
args.push(filename);
|
||||||
|
process.runAsync(args, args.length, this._processObserver(resolve, reject));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function readWindowID() {
|
||||||
|
let decoder = new TextDecoder();
|
||||||
|
let promise = OS.File.read("/tmp/mozscreenshots-windowid");
|
||||||
|
return promise.then(function onSuccess(array) {
|
||||||
|
return decoder.decode(array);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let promiseWindowID = () => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// Get the window ID of the application (assuming its front-most)
|
||||||
|
let osascript = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
|
||||||
|
osascript.initWithPath("/bin/bash");
|
||||||
|
|
||||||
|
let osascriptP = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
|
||||||
|
osascriptP.init(osascript);
|
||||||
|
let osaArgs = ["-c", "/usr/bin/osascript -e 'tell application (path to frontmost application as text) to set winID to id of window 1' > /tmp/mozscreenshots-windowid"];
|
||||||
|
osascriptP.runAsync(osaArgs, osaArgs.length, this._processObserver(resolve, reject));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
yield promiseWindowID();
|
||||||
|
let windowID = yield readWindowID();
|
||||||
|
yield screencapture(windowID);
|
||||||
|
}),
|
||||||
|
|
||||||
|
_screenshotLinux(filename) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let file = Services.dirsvc.get("GreBinD", Ci.nsIFile);
|
||||||
|
file.append("screentopng");
|
||||||
|
let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
|
||||||
|
process.init(file);
|
||||||
|
|
||||||
|
let args = [filename];
|
||||||
|
process.runAsync(args, args.length, this._processObserver(resolve, reject));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
_processObserver(resolve, reject) {
|
||||||
|
return {
|
||||||
|
observe(subject, topic, data) {
|
||||||
|
switch (topic) {
|
||||||
|
case "process-finished":
|
||||||
|
try {
|
||||||
|
// Wait 1s after process to resolve
|
||||||
|
setTimeout(resolve, 1000);
|
||||||
|
} catch (ex) {
|
||||||
|
reject(ex);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
reject(topic);
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,256 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["TestRunner"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
const defaultSetNames = ["TabsInTitlebar", "Tabs", "WindowSize", "Toolbars", "LightweightThemes"];
|
||||||
|
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/osfile.jsm");
|
||||||
|
Cu.import("chrome://mozscreenshots/content/Screenshot.jsm");
|
||||||
|
|
||||||
|
// Create a new instance of the ConsoleAPI so we can control the maxLogLevel with a pref.
|
||||||
|
// See LOG_LEVELS in Console.jsm. Common examples: "All", "Info", "Warn", & "Error".
|
||||||
|
const PREF_LOG_LEVEL = "extensions.mozscreenshots@mozilla.org.loglevel";
|
||||||
|
XPCOMUtils.defineLazyGetter(this, "log", () => {
|
||||||
|
let ConsoleAPI = Cu.import("resource://gre/modules/Console.jsm", {}).ConsoleAPI;
|
||||||
|
let consoleOptions = {
|
||||||
|
maxLogLevel: "info",
|
||||||
|
maxLogLevelPref: PREF_LOG_LEVEL,
|
||||||
|
prefix: "mozscreenshots",
|
||||||
|
};
|
||||||
|
return new ConsoleAPI(consoleOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.TestRunner = {
|
||||||
|
combos: null,
|
||||||
|
completedCombos: 0,
|
||||||
|
currentComboIndex: 0,
|
||||||
|
_lastCombo: null,
|
||||||
|
_libDir: null,
|
||||||
|
|
||||||
|
init(extensionPath) {
|
||||||
|
let subDirs = ["mozscreenshots",
|
||||||
|
(new Date()).toISOString().replace(/:/g, "-") + "_" + Services.appinfo.OS];
|
||||||
|
let screenshotPath = FileUtils.getFile("TmpD", subDirs).path;
|
||||||
|
|
||||||
|
const MOZ_UPLOAD_DIR = env.get("MOZ_UPLOAD_DIR");
|
||||||
|
if (MOZ_UPLOAD_DIR) {
|
||||||
|
screenshotPath = MOZ_UPLOAD_DIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Saving screenshots to:", screenshotPath);
|
||||||
|
log.debug("TestRunner.init");
|
||||||
|
|
||||||
|
let screenshotPrefix = Services.appinfo.appBuildID + "_";
|
||||||
|
Screenshot.init(screenshotPath, extensionPath, screenshotPrefix);
|
||||||
|
this._libDir = extensionPath.QueryInterface(Ci.nsIFileURL).file.clone();
|
||||||
|
this._libDir.append("chrome");
|
||||||
|
this._libDir.append("mozscreenshots");
|
||||||
|
this._libDir.append("lib");
|
||||||
|
|
||||||
|
// Setup some prefs
|
||||||
|
Services.prefs.setCharPref("browser.aboutHomeSnippets.updateUrl", "data:");
|
||||||
|
Services.prefs.setCharPref("extensions.ui.lastCategory", "addons://list/extension");
|
||||||
|
// Don't let the caret blink since it causes false positives for image diffs
|
||||||
|
Services.prefs.setIntPref("ui.caretBlinkTime", -1);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load specified sets, execute all combinations of them, and capture screenshots.
|
||||||
|
*/
|
||||||
|
start(setNames = null) {
|
||||||
|
setNames = setNames || defaultSetNames;
|
||||||
|
let sets = this.loadSets(setNames);
|
||||||
|
|
||||||
|
log.info(sets.length + " sets:", setNames);
|
||||||
|
this.combos = new LazyProduct(sets);
|
||||||
|
log.info(this.combos.length + " combinations");
|
||||||
|
|
||||||
|
this.currentComboIndex = this.completedCombos = 0;
|
||||||
|
this._lastCombo = null;
|
||||||
|
|
||||||
|
return Task.spawn(function* doStart() {
|
||||||
|
for (let i = 0; i < this.combos.length;
|
||||||
|
i++){
|
||||||
|
this.currentComboIndex = i;
|
||||||
|
yield* this._performCombo(this.combos.item(this.currentComboIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("Done: Completed " + this.completedCombos + " out of " +
|
||||||
|
this.combos.length + " configurations.");
|
||||||
|
this.cleanup();
|
||||||
|
}.bind(this));
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load sets of configurations from JSMs.
|
||||||
|
* @param {String[]} setNames - array of set names (e.g. ["Tabs", "WindowSize"].
|
||||||
|
* @return {Object[]} Array of sets containing `name` and `configurations` properties.
|
||||||
|
*/
|
||||||
|
loadSets(setNames) {
|
||||||
|
let sets = [];
|
||||||
|
for (let setName of setNames) {
|
||||||
|
try {
|
||||||
|
let imported = {};
|
||||||
|
Cu.import("chrome://mozscreenshots/content/configurations/" + setName + ".jsm",
|
||||||
|
imported);
|
||||||
|
imported[setName].init(this._libDir);
|
||||||
|
let configurationNames = Object.keys(imported[setName].configurations);
|
||||||
|
if (!configurationNames.length) {
|
||||||
|
throw new Error(setName + " has no configurations for this environment");
|
||||||
|
}
|
||||||
|
for (let config of configurationNames) {
|
||||||
|
// Automatically set the name property of the configuration object to
|
||||||
|
// its name from the configuration object.
|
||||||
|
imported[setName].configurations[config].name = config;
|
||||||
|
}
|
||||||
|
sets.push(imported[setName].configurations);
|
||||||
|
} catch (ex) {
|
||||||
|
log.error("Error loading set: " + setName);
|
||||||
|
log.error(ex);
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sets;
|
||||||
|
},
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let gBrowser = browserWindow.gBrowser;
|
||||||
|
while (gBrowser.tabs.length > 1) {
|
||||||
|
gBrowser.removeTab(gBrowser.selectedTab, {animate: false});
|
||||||
|
}
|
||||||
|
gBrowser.unpinTab(gBrowser.selectedTab);
|
||||||
|
gBrowser.selectedBrowser.loadURI("data:text/html;charset=utf-8,<h1>Done!");
|
||||||
|
browserWindow.restore();
|
||||||
|
},
|
||||||
|
|
||||||
|
///// helpers /////
|
||||||
|
|
||||||
|
_performCombo: function*(combo) {
|
||||||
|
let paddedComboIndex = padLeft(this.currentComboIndex + 1, String(this.combos.length).length);
|
||||||
|
log.info("Combination " + paddedComboIndex + "/" + this.combos.length + ": " +
|
||||||
|
this._comboName(combo).substring(1));
|
||||||
|
|
||||||
|
function changeConfig(config) {
|
||||||
|
log.debug("calling " + config.name);
|
||||||
|
let promise = config.applyConfig();
|
||||||
|
log.debug("called " + config.name);
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First go through and actually apply all of the configs
|
||||||
|
for (let i = 0; i < combo.length; i++) {
|
||||||
|
let config = combo[i];
|
||||||
|
if (!this._lastCombo || config !== this._lastCombo[i]) {
|
||||||
|
log.debug("promising", config.name);
|
||||||
|
yield changeConfig(config);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the lastCombo since it's now been applied regardless of whether it's accepted below.
|
||||||
|
log.debug("fulfilled all applyConfig so setting lastCombo.");
|
||||||
|
this._lastCombo = combo;
|
||||||
|
|
||||||
|
// Then ask configs if the current setup is valid. We can't can do this in
|
||||||
|
// the applyConfig methods of the config since it doesn't know what configs
|
||||||
|
// later in the loop will do that may invalidate the combo.
|
||||||
|
for (let i = 0; i < combo.length; i++) {
|
||||||
|
let config = combo[i];
|
||||||
|
// A configuration can specify an optional verifyConfig method to indicate
|
||||||
|
// if the current config is valid for a screenshot. This gets called even
|
||||||
|
// if the this config was used in the lastCombo since another config may
|
||||||
|
// have invalidated it.
|
||||||
|
if (config.verifyConfig) {
|
||||||
|
log.debug("checking if the combo is valid with", config.name);
|
||||||
|
yield config.verifyConfig();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex) {
|
||||||
|
log.warn("\tskipped configuration: " + ex);
|
||||||
|
// Don't set lastCombo here so that we properly know which configurations
|
||||||
|
// need to be applied since the last screenshot
|
||||||
|
|
||||||
|
// Return so we don't take a screenshot.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
yield this._onConfigurationReady(combo);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onConfigurationReady(combo) {
|
||||||
|
let delayedScreenshot = () => {
|
||||||
|
let filename = padLeft(this.currentComboIndex + 1,
|
||||||
|
String(this.combos.length).length) + this._comboName(combo);
|
||||||
|
return Screenshot.captureExternal(filename)
|
||||||
|
.then(() => {
|
||||||
|
this.completedCombos++;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
log.debug("_onConfigurationReady");
|
||||||
|
return Task.spawn(delayedScreenshot);
|
||||||
|
},
|
||||||
|
|
||||||
|
_comboName(combo) {
|
||||||
|
return combo.reduce(function(a, b) {
|
||||||
|
return a + "_" + b.name;
|
||||||
|
}, "");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to lazily compute the Cartesian product of all of the sets of configurations.
|
||||||
|
**/
|
||||||
|
function LazyProduct(sets) {
|
||||||
|
/**
|
||||||
|
* An entry for each set with the value being:
|
||||||
|
* [the number of permutations of the sets with lower index,
|
||||||
|
* the number of items in the set at the index]
|
||||||
|
*/
|
||||||
|
this.sets = sets;
|
||||||
|
this.lookupTable = [];
|
||||||
|
let combinations = 1;
|
||||||
|
for (let i = this.sets.length - 1; i >= 0; i--) {
|
||||||
|
let set = this.sets[i];
|
||||||
|
let setLength = Object.keys(set).length;
|
||||||
|
this.lookupTable[i] = [combinations, setLength];
|
||||||
|
combinations *= setLength;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LazyProduct.prototype = {
|
||||||
|
get length() {
|
||||||
|
let last = this.lookupTable[0];
|
||||||
|
if (!last)
|
||||||
|
return 0;
|
||||||
|
return last[0] * last[1];
|
||||||
|
},
|
||||||
|
|
||||||
|
item(n) {
|
||||||
|
// For set i, get the item from the set with the floored value of
|
||||||
|
// (n / the number of permutations of the sets already chosen from) modulo the length of set i
|
||||||
|
let result = [];
|
||||||
|
for (let i = this.sets.length - 1; i >= 0; i--) {
|
||||||
|
let priorCombinations = this.lookupTable[i][0];
|
||||||
|
let setLength = this.lookupTable[i][1];
|
||||||
|
let keyIndex = Math.floor(n / priorCombinations) % setLength;
|
||||||
|
let keys = Object.keys(this.sets[i]);
|
||||||
|
result[i] = this.sets[i][keys[keyIndex]];
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function padLeft(number, width, padding = "0") {
|
||||||
|
return padding.repeat(Math.max(0, width - String(number).length)) + number;
|
||||||
|
}
|
72
browser/tools/mozscreenshots/mozscreenshots/extension/bootstrap.js
vendored
Normal file
72
browser/tools/mozscreenshots/mozscreenshots/extension/bootstrap.js
vendored
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
/*
|
||||||
|
#if 0
|
||||||
|
Workaround a build system bug where this file doesn't get packaged if not pre-processed.
|
||||||
|
#endif
|
||||||
|
*/
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
const env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||||
|
Cu.import("resource://gre/modules/AddonManager.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Timer.jsm");
|
||||||
|
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "TestRunner",
|
||||||
|
"chrome://mozscreenshots/content/TestRunner.jsm");
|
||||||
|
|
||||||
|
function install(data, reason) {
|
||||||
|
if (!isAppSupported()) {
|
||||||
|
uninstallExtension(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonManager.getAddonByID(data.id, function(addon) {
|
||||||
|
// Enable on install in case the user disabled a prior version
|
||||||
|
if (addon) {
|
||||||
|
addon.userDisabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startup(data, reason) {
|
||||||
|
if (!isAppSupported()) {
|
||||||
|
uninstallExtension(data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddonManager.getAddonByID(data.id, function(addon) {
|
||||||
|
let extensionPath = addon.getResourceURI();
|
||||||
|
TestRunner.init(extensionPath);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function shutdown(data, reason) { }
|
||||||
|
|
||||||
|
function uninstall(data, reason) { }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return boolean whether the test suite applies to the application.
|
||||||
|
*/
|
||||||
|
function isAppSupported() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function uninstallExtension(data) {
|
||||||
|
AddonManager.getAddonByID(data.id, function(addon) {
|
||||||
|
addon.uninstall();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function startRun() {
|
||||||
|
let env = Cc["@mozilla.org/process/environment;1"]
|
||||||
|
.getService(Ci.nsIEnvironment);
|
||||||
|
let setsEnv = env.get("MOZSCREENSHOTS_SETS");
|
||||||
|
let sets = setsEnv ? setsEnv.split(",") : null;
|
||||||
|
TestRunner.start(sets);
|
||||||
|
}
|
@ -0,0 +1,84 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["AppMenu"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
this.AppMenu = {
|
||||||
|
|
||||||
|
init(libDir) {},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
appMenuClosed: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
browserWindow.PanelUI.hide();
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
appMenuMainView: {
|
||||||
|
applyConfig() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let promise = browserWindow.PanelUI.show();
|
||||||
|
browserWindow.PanelUI.showMainView();
|
||||||
|
return promise;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
appMenuHistorySubview: {
|
||||||
|
applyConfig() {
|
||||||
|
// History has a footer
|
||||||
|
if (isCustomizing()) {
|
||||||
|
return Promise.reject("Can't show subviews while customizing");
|
||||||
|
}
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let promise = browserWindow.PanelUI.show();
|
||||||
|
return promise.then(() => {
|
||||||
|
browserWindow.PanelUI.showMainView();
|
||||||
|
browserWindow.document.getElementById("history-panelmenu").click();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyConfig: verifyConfigHelper,
|
||||||
|
},
|
||||||
|
|
||||||
|
appMenuHelpSubview: {
|
||||||
|
applyConfig() {
|
||||||
|
if (isCustomizing()) {
|
||||||
|
return Promise.reject("Can't show subviews while customizing");
|
||||||
|
}
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let promise = browserWindow.PanelUI.show();
|
||||||
|
return promise.then(() => {
|
||||||
|
browserWindow.PanelUI.showMainView();
|
||||||
|
browserWindow.document.getElementById("PanelUI-help").click();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyConfig: verifyConfigHelper,
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function verifyConfigHelper() {
|
||||||
|
if (isCustomizing()) {
|
||||||
|
return Promise.reject("AppMenu verifyConfigHelper");
|
||||||
|
}
|
||||||
|
return Promise.resolve("AppMenu verifyConfigHelper");
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCustomizing() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (browserWindow.document.documentElement.hasAttribute("customizing")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
@ -0,0 +1,87 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Buttons"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource:///modules/CustomizableUI.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
this.Buttons = {
|
||||||
|
|
||||||
|
init(libDir) {
|
||||||
|
createWidget();
|
||||||
|
},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
navBarButtons: {
|
||||||
|
applyConfig: Task.async(() =>{
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
CustomizableUI.addWidgetToArea("screenshot-widget", CustomizableUI.AREA_NAVBAR);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
tabsToolbarButtons: {
|
||||||
|
applyConfig: Task.async(() => {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
CustomizableUI.addWidgetToArea("screenshot-widget", CustomizableUI.AREA_TABSTRIP);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
menuPanelButtons: {
|
||||||
|
applyConfig: Task.async(() => {
|
||||||
|
CustomizableUI.addWidgetToArea("screenshot-widget", CustomizableUI.AREA_PANEL);
|
||||||
|
}),
|
||||||
|
|
||||||
|
verifyConfig() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (browserWindow.PanelUI.panel.state == "closed") {
|
||||||
|
return Promise.reject("The button isn't shown when the panel isn't open.");
|
||||||
|
}
|
||||||
|
return Promise.resolve("menuPanelButtons.verifyConfig");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
custPaletteButtons: {
|
||||||
|
applyConfig: Task.async(() => {
|
||||||
|
CustomizableUI.removeWidgetFromArea("screenshot-widget");
|
||||||
|
}),
|
||||||
|
|
||||||
|
verifyConfig() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (browserWindow.document.documentElement.getAttribute("customizing") != "true") {
|
||||||
|
return Promise.reject("The button isn't shown when we're not in customize mode.");
|
||||||
|
}
|
||||||
|
return Promise.resolve("custPaletteButtons.verifyConfig");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function createWidget() {
|
||||||
|
let id = "screenshot-widget";
|
||||||
|
let spec = {
|
||||||
|
id: id,
|
||||||
|
label: "My Button",
|
||||||
|
removable: true,
|
||||||
|
tooltiptext: "",
|
||||||
|
type: "button",
|
||||||
|
};
|
||||||
|
CustomizableUI.createWidget(spec);
|
||||||
|
|
||||||
|
// Append a <style> for the image
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let st = browserWindow.document.createElementNS("http://www.w3.org/1999/xhtml", "style");
|
||||||
|
let styles = "" +
|
||||||
|
"#screenshot-widget > .toolbarbutton-icon {" +
|
||||||
|
" list-style-image: url(chrome://browser/skin/Toolbar.png);" +
|
||||||
|
" -moz-image-region: rect(0px, 18px, 18px, 0px);" +
|
||||||
|
"}";
|
||||||
|
st.appendChild(browserWindow.document.createTextNode(styles));
|
||||||
|
browserWindow.document.documentElement.appendChild(st);
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["CustomizeMode"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Timer.jsm");
|
||||||
|
|
||||||
|
this.CustomizeMode = {
|
||||||
|
|
||||||
|
init(libDir) {},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
notCustomizing: {
|
||||||
|
applyConfig() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (!browserWindow.document.documentElement.hasAttribute("customizing")) {
|
||||||
|
resolve("notCustomizing: already not customizing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onCustomizationEnds() {
|
||||||
|
browserWindow.gNavToolbox.removeEventListener("aftercustomization",
|
||||||
|
onCustomizationEnds);
|
||||||
|
// Wait for final changes
|
||||||
|
setTimeout(() => resolve("notCustomizing: onCustomizationEnds"), 500);
|
||||||
|
}
|
||||||
|
browserWindow.gNavToolbox.addEventListener("aftercustomization",
|
||||||
|
onCustomizationEnds);
|
||||||
|
browserWindow.gCustomizeMode.exit();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
customizing: {
|
||||||
|
applyConfig() {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (browserWindow.document.documentElement.hasAttribute("customizing")) {
|
||||||
|
resolve("customizing: already customizing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
function onCustomizing() {
|
||||||
|
browserWindow.gNavToolbox.removeEventListener("customizationready",
|
||||||
|
onCustomizing);
|
||||||
|
// Wait for final changes
|
||||||
|
setTimeout(() => resolve("customizing: onCustomizing"), 500);
|
||||||
|
}
|
||||||
|
browserWindow.gNavToolbox.addEventListener("customizationready",
|
||||||
|
onCustomizing);
|
||||||
|
browserWindow.gCustomizeMode.enter();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,42 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["DevEdition"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
const THEME_ID = "firefox-devedition@mozilla.org";
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
this.DevEdition = {
|
||||||
|
init(libDir) {},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
devEditionLight: {
|
||||||
|
applyConfig: Task.async(() => {
|
||||||
|
Services.prefs.setCharPref("devtools.theme", "light");
|
||||||
|
LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme(THEME_ID);
|
||||||
|
Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", true);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
devEditionDark: {
|
||||||
|
applyConfig: Task.async(() => {
|
||||||
|
Services.prefs.setCharPref("devtools.theme", "dark");
|
||||||
|
LightweightThemeManager.currentTheme = LightweightThemeManager.getUsedTheme(THEME_ID);
|
||||||
|
Services.prefs.setBoolPref("browser.devedition.theme.showCustomizeButton", true);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
devEditionOff: {
|
||||||
|
applyConfig: Task.async(() => {
|
||||||
|
Services.prefs.clearUserPref("devtools.theme");
|
||||||
|
LightweightThemeManager.currentTheme = null;
|
||||||
|
Services.prefs.clearUserPref("browser.devedition.theme.showCustomizeButton");
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,50 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["DevTools"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://devtools/client/framework/gDevTools.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
|
||||||
|
let TargetFactory = devtools.TargetFactory;
|
||||||
|
|
||||||
|
function getTargetForSelectedTab() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let target = TargetFactory.forTab(browserWindow.gBrowser.selectedTab);
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.DevTools = {
|
||||||
|
init(libDir) {
|
||||||
|
let panels = ["options", "webconsole", "inspector", "jsdebugger", "netmonitor"];
|
||||||
|
panels.forEach(panel => {
|
||||||
|
this.configurations[panel] = {};
|
||||||
|
this.configurations[panel].applyConfig = () => {
|
||||||
|
return gDevTools.showToolbox(getTargetForSelectedTab(), panel, "bottom");
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
bottomToolbox: {
|
||||||
|
applyConfig() {
|
||||||
|
return gDevTools.showToolbox(getTargetForSelectedTab(), "inspector", "bottom");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
sideToolbox: {
|
||||||
|
applyConfig() {
|
||||||
|
return gDevTools.showToolbox(getTargetForSelectedTab(), "inspector", "side");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
undockedToolbox: {
|
||||||
|
applyConfig() {
|
||||||
|
return gDevTools.showToolbox(getTargetForSelectedTab(), "inspector", "window");
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,92 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["LightweightThemes"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Timer.jsm");
|
||||||
|
|
||||||
|
this.LightweightThemes = {
|
||||||
|
init(libDir) {
|
||||||
|
// convert -size 3000x200 canvas:black black_theme.png
|
||||||
|
let blackImage = libDir.clone();
|
||||||
|
blackImage.append("black_theme.png");
|
||||||
|
this._blackImageURL = Services.io.newFileURI(blackImage).spec;
|
||||||
|
|
||||||
|
// convert -size 3000x200 canvas:white white_theme.png
|
||||||
|
let whiteImage = libDir.clone();
|
||||||
|
whiteImage.append("white_theme.png");
|
||||||
|
this._whiteImageURL = Services.io.newFileURI(whiteImage).spec;
|
||||||
|
},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
noLWT: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
LightweightThemeManager.currentTheme = null;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
darkLWT: {
|
||||||
|
applyConfig() {
|
||||||
|
LightweightThemeManager.setLocalTheme({
|
||||||
|
id: "black",
|
||||||
|
name: "black",
|
||||||
|
headerURL: LightweightThemes._blackImageURL,
|
||||||
|
footerURL: LightweightThemes._blackImageURL,
|
||||||
|
textcolor: "#ffffff",
|
||||||
|
accentcolor: "#111111",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for LWT listener
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve("darkLWT");
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyConfig: verifyConfigHelper,
|
||||||
|
},
|
||||||
|
|
||||||
|
lightLWT: {
|
||||||
|
applyConfig() {
|
||||||
|
LightweightThemeManager.setLocalTheme({
|
||||||
|
id: "white",
|
||||||
|
name: "white",
|
||||||
|
headerURL: LightweightThemes._whiteImageURL,
|
||||||
|
footerURL: LightweightThemes._whiteImageURL,
|
||||||
|
textcolor: "#000000",
|
||||||
|
accentcolor: "#eeeeee",
|
||||||
|
});
|
||||||
|
// Wait for LWT listener
|
||||||
|
return new Promise(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve("lightLWT");
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyConfig: verifyConfigHelper,
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
function verifyConfigHelper() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (browserWindow.document.documentElement.hasAttribute("lwtheme")) {
|
||||||
|
resolve("verifyConfigHelper");
|
||||||
|
} else {
|
||||||
|
reject("The @lwtheme attribute wasn't present so themes may not be available");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Preferences"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Timer.jsm");
|
||||||
|
|
||||||
|
this.Preferences = {
|
||||||
|
|
||||||
|
init(libDir) {
|
||||||
|
Services.prefs.setBoolPref("browser.preferences.inContent", true);
|
||||||
|
|
||||||
|
let panes = [
|
||||||
|
["paneGeneral", null],
|
||||||
|
["paneSearch", null],
|
||||||
|
["paneContent", null],
|
||||||
|
["paneApplications", null],
|
||||||
|
["panePrivacy", null],
|
||||||
|
["paneSecurity", null],
|
||||||
|
["paneSync", null],
|
||||||
|
["paneAdvanced", "generalTab"],
|
||||||
|
["paneAdvanced", "dataChoicesTab"],
|
||||||
|
["paneAdvanced", "networkTab"],
|
||||||
|
["paneAdvanced", "updateTab"],
|
||||||
|
["paneAdvanced", "encryptionTab"],
|
||||||
|
];
|
||||||
|
for (let [primary, advanced] of panes) {
|
||||||
|
let configName = primary + ("-" + advanced || "");
|
||||||
|
this.configurations[configName] = {};
|
||||||
|
this.configurations[configName].applyConfig = prefHelper.bind(null, primary, advanced);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
configurations: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function prefHelper(primary, advanced) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (primary == "paneAdvanced") {
|
||||||
|
browserWindow.openAdvancedPreferences(advanced);
|
||||||
|
} else {
|
||||||
|
browserWindow.openPreferences(primary);
|
||||||
|
}
|
||||||
|
setTimeout(resolve, 50);
|
||||||
|
});
|
||||||
|
}
|
@ -0,0 +1,143 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Tabs"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
const CUST_TAB = "chrome://browser/skin/customizableui/customizeFavicon.ico";
|
||||||
|
const PREFS_TAB = "chrome://browser/skin/preferences/in-content/favicon.ico";
|
||||||
|
const DEFAULT_FAVICON_TAB = `data:text/html,<meta charset="utf-8">
|
||||||
|
<title>No favicon</title>`;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
this.Tabs = {
|
||||||
|
init(libDir) {},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
fiveTabs: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
fiveTabsHelper();
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
hoverTab(browserWindow.gBrowser.tabs[3]);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
fourPinned: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
fiveTabsHelper();
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let tab = browserWindow.gBrowser.addTab(PREFS_TAB);
|
||||||
|
browserWindow.gBrowser.pinTab(tab);
|
||||||
|
tab = browserWindow.gBrowser.addTab(CUST_TAB);
|
||||||
|
browserWindow.gBrowser.pinTab(tab);
|
||||||
|
tab = browserWindow.gBrowser.addTab("about:privatebrowsing");
|
||||||
|
browserWindow.gBrowser.pinTab(tab);
|
||||||
|
tab = browserWindow.gBrowser.addTab("about:home");
|
||||||
|
browserWindow.gBrowser.pinTab(tab);
|
||||||
|
browserWindow.gBrowser.selectTabAtIndex(5);
|
||||||
|
hoverTab(browserWindow.gBrowser.tabs[2]);
|
||||||
|
// also hover the new tab button
|
||||||
|
let newTabButton = browserWindow.document.getAnonymousElementByAttribute(browserWindow.
|
||||||
|
gBrowser.tabContainer, "class", "tabs-newtab-button");
|
||||||
|
hoverTab(newTabButton);
|
||||||
|
browserWindow.gBrowser.tabs[browserWindow.gBrowser.tabs.length - 1].
|
||||||
|
setAttribute("beforehovered", true);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
twoPinnedWithOverflow: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
fiveTabsHelper();
|
||||||
|
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
browserWindow.gBrowser.loadTabs([
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
], true, true);
|
||||||
|
let tab = browserWindow.gBrowser.addTab(PREFS_TAB);
|
||||||
|
browserWindow.gBrowser.pinTab(tab);
|
||||||
|
tab = browserWindow.gBrowser.addTab(CUST_TAB);
|
||||||
|
browserWindow.gBrowser.pinTab(tab);
|
||||||
|
browserWindow.gBrowser.selectTabAtIndex(4);
|
||||||
|
hoverTab(browserWindow.gBrowser.tabs[6]);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/* helpers */
|
||||||
|
|
||||||
|
function fiveTabsHelper() {
|
||||||
|
// some with no favicon and some with. Selected tab in middle.
|
||||||
|
closeAllButOneTab("about:addons");
|
||||||
|
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
browserWindow.gBrowser.loadTabs([
|
||||||
|
"about:addons",
|
||||||
|
"about:home",
|
||||||
|
DEFAULT_FAVICON_TAB,
|
||||||
|
"about:newtab",
|
||||||
|
CUST_TAB,
|
||||||
|
], true, true);
|
||||||
|
browserWindow.gBrowser.selectTabAtIndex(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeAllButOneTab(url = "about:blank") {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let gBrowser = browserWindow.gBrowser;
|
||||||
|
// Close all tabs except the last so we don't quit the browser.
|
||||||
|
while (gBrowser.tabs.length > 1)
|
||||||
|
gBrowser.removeTab(gBrowser.selectedTab, {animate: false});
|
||||||
|
gBrowser.selectedBrowser.loadURI(url);
|
||||||
|
if (gBrowser.selectedTab.pinned)
|
||||||
|
gBrowser.unpinTab(gBrowser.selectedTab);
|
||||||
|
let newTabButton = browserWindow.document.getAnonymousElementByAttribute(browserWindow.gBrowser.tabContainer, "class", "tabs-newtab-button");
|
||||||
|
hoverTab(newTabButton, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hoverTab(tab, hover = true) {
|
||||||
|
const inIDOMUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
|
||||||
|
if (hover) {
|
||||||
|
inIDOMUtils.addPseudoClassLock(tab, ":hover");
|
||||||
|
} else {
|
||||||
|
inIDOMUtils.clearPseudoClassLocks(tab);
|
||||||
|
}
|
||||||
|
// XXX TODO: this isn't necessarily testing what we ship
|
||||||
|
if (tab.nextElementSibling)
|
||||||
|
tab.nextElementSibling.setAttribute("afterhovered", hover || null);
|
||||||
|
if (tab.previousElementSibling)
|
||||||
|
tab.previousElementSibling.setAttribute("beforehovered", hover || null);
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["TabsInTitlebar"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
const PREF_TABS_IN_TITLEBAR = "browser.tabs.drawInTitlebar";
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
this.TabsInTitlebar = {
|
||||||
|
|
||||||
|
init(libDir) {},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
tabsInTitlebar: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
if (Services.appinfo.OS == "Linux") {
|
||||||
|
return Promise.reject("TabsInTitlebar isn't supported on Linux");
|
||||||
|
}
|
||||||
|
Services.prefs.setBoolPref(PREF_TABS_IN_TITLEBAR, true);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
tabsOutsideTitlebar: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
Services.prefs.setBoolPref(PREF_TABS_IN_TITLEBAR, false);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,56 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["Toolbars"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
|
this.Toolbars = {
|
||||||
|
init(libDir) {},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
onlyNavBar: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let personalToolbar = browserWindow.document.getElementById("PersonalToolbar");
|
||||||
|
browserWindow.setToolbarVisibility(personalToolbar, false);
|
||||||
|
toggleMenubarIfNecessary(false);
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
allToolbars: {
|
||||||
|
applyConfig: Task.async(function*() { // Boookmarks and menubar
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
let personalToolbar = browserWindow.document.getElementById("PersonalToolbar");
|
||||||
|
browserWindow.setToolbarVisibility(personalToolbar, true);
|
||||||
|
toggleMenubarIfNecessary(true);
|
||||||
|
}),
|
||||||
|
|
||||||
|
verifyConfig: Task.async(function*() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
if (browserWindow.fullScreen) {
|
||||||
|
return Promise.reject("The bookmark toolbar and menubar are not shown in fullscreen.");
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
///// helpers /////
|
||||||
|
|
||||||
|
function toggleMenubarIfNecessary(visible) {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
// The menubar is not shown on OS X or while in fullScreen
|
||||||
|
if (Services.appinfo.OS != "Darwin" /*&& !browserWindow.fullScreen*/) {
|
||||||
|
let menubar = browserWindow.document.getElementById("toolbar-menubar");
|
||||||
|
browserWindow.setToolbarVisibility(menubar, visible);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
this.EXPORTED_SYMBOLS = ["WindowSize"];
|
||||||
|
|
||||||
|
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||||
|
|
||||||
|
Cu.import("resource://gre/modules/Services.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Task.jsm");
|
||||||
|
Cu.import("resource://gre/modules/Timer.jsm");
|
||||||
|
|
||||||
|
this.WindowSize = {
|
||||||
|
|
||||||
|
init(libDir) {
|
||||||
|
Services.prefs.setBoolPref("browser.fullscreen.autohide", false);
|
||||||
|
},
|
||||||
|
|
||||||
|
configurations: {
|
||||||
|
maximized: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
browserWindow.fullScreen = false;
|
||||||
|
|
||||||
|
// Wait for the Lion fullscreen transition to end as there doesn't seem to be an event
|
||||||
|
// and trying to maximize while still leaving fullscreen doesn't work.
|
||||||
|
yield new Promise((resolve, reject) => {
|
||||||
|
setTimeout(function waitToLeaveFS() {
|
||||||
|
browserWindow.maximize();
|
||||||
|
resolve();
|
||||||
|
}, Services.appinfo.OS == "Darwin" ? 1500 : 0);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
normal: {
|
||||||
|
applyConfig: Task.async(() => {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
browserWindow.fullScreen = false;
|
||||||
|
browserWindow.restore();
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
fullScreen: {
|
||||||
|
applyConfig: Task.async(function*() {
|
||||||
|
let browserWindow = Services.wm.getMostRecentWindow("navigator:browser");
|
||||||
|
browserWindow.fullScreen = true;
|
||||||
|
// OS X Lion fullscreen transition takes a while
|
||||||
|
yield new Promise((resolve, reject) => {
|
||||||
|
setTimeout(function waitAfterEnteringFS() {
|
||||||
|
resolve();
|
||||||
|
}, Services.appinfo.OS == "Darwin" ? 1500 : 0);
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
|
||||||
|
},
|
||||||
|
};
|
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
|
||||||
|
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||||
|
|
||||||
|
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
|
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||||
|
|
||||||
|
<Description about="urn:mozilla:install-manifest">
|
||||||
|
<em:id>mozscreenshots@mozilla.org</em:id>
|
||||||
|
#expand <em:version>__MOZILLA_VERSION_U__</em:version>
|
||||||
|
<em:bootstrap>true</em:bootstrap>
|
||||||
|
|
||||||
|
<!-- for running custom screenshot binaries -->
|
||||||
|
<em:unpack>true</em:unpack>
|
||||||
|
|
||||||
|
<!-- Front End MetaData -->
|
||||||
|
<em:name>mozscreenshots</em:name>
|
||||||
|
<em:description>Take screenshots of Mozilla applications in various UI configurations.</em:description>
|
||||||
|
<em:creator>Mozilla</em:creator>
|
||||||
|
|
||||||
|
<em:targetApplication>
|
||||||
|
<Description>
|
||||||
|
<!-- Firefox -->
|
||||||
|
<em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
|
||||||
|
#expand <em:minVersion>__MOZILLA_VERSION_U__</em:minVersion>
|
||||||
|
<em:maxVersion>*</em:maxVersion>
|
||||||
|
</Description>
|
||||||
|
</em:targetApplication>
|
||||||
|
|
||||||
|
</Description>
|
||||||
|
</RDF>
|
@ -0,0 +1,6 @@
|
|||||||
|
mozscreenshots.jar:
|
||||||
|
% content mozscreenshots chrome/mozscreenshots/
|
||||||
|
Screenshot.jsm
|
||||||
|
TestRunner.jsm
|
||||||
|
configurations/ (configurations/*.jsm)
|
||||||
|
lib/ (lib/*.png)
|
Binary file not shown.
After Width: | Height: | Size: 343 B |
Binary file not shown.
After Width: | Height: | Size: 522 B |
@ -0,0 +1,17 @@
|
|||||||
|
# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
|
||||||
|
# vim: set filetype=python:
|
||||||
|
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
XPI_NAME = 'mozscreenshots'
|
||||||
|
|
||||||
|
JAR_MANIFESTS += ['jar.mn']
|
||||||
|
|
||||||
|
USE_EXTENSION_MANIFEST = True
|
||||||
|
NO_JS_MANIFEST = True
|
||||||
|
|
||||||
|
FINAL_TARGET_PP_FILES += [
|
||||||
|
'bootstrap.js',
|
||||||
|
'install.rdf',
|
||||||
|
]
|
Loading…
Reference in New Issue
Block a user