зеркало из https://github.com/mozilla/gecko-dev.git
Merge m-c to graphics
MozReview-Commit-ID: 8Oqr9Nbqsn0
This commit is contained in:
Коммит
1be864c6ad
|
@ -141,3 +141,6 @@ GPATH
|
|||
\.vscode/(?!extensions.json$)
|
||||
|
||||
subinclude:servo/.hgignore
|
||||
|
||||
# Ignore Infer output
|
||||
^infer-out/
|
|
@ -65,6 +65,8 @@ tasks:
|
|||
- "index.gecko.v2.{{project}}.latest.firefox.decision"
|
||||
- "tc-treeherder.v2.{{project}}.{{revision}}.{{pushlog_id}}"
|
||||
- "tc-treeherder-stage.v2.{{project}}.{{revision}}.{{pushlog_id}}"
|
||||
- "notify.email.{{owner}}.on-failed"
|
||||
- "notify.email.{{owner}}.on-exception"
|
||||
|
||||
payload:
|
||||
env:
|
||||
|
|
2
CLOBBER
2
CLOBBER
|
@ -22,4 +22,4 @@
|
|||
# changes to stick? As of bug 928195, this shouldn't be necessary! Please
|
||||
# don't change CLOBBER for WebIDL changes any more.
|
||||
|
||||
Bug 1356151 - Clobber needed after bug 1353295 was backed out
|
||||
Bug 1356927 - Mac builds in automation require a clobber for a change in which ranlib they use
|
||||
|
|
|
@ -23,7 +23,7 @@ interface IGeckoBackChannel;
|
|||
// channels are present. Note that we do not do this for all UUIDs in this IDL,
|
||||
// just the ones that are written to the registry (coclass and interfaces that
|
||||
// have the [object] annotation)
|
||||
#if !defined(MOZ_OFFICIAL_BRANDING)
|
||||
#if defined(USE_LOCAL_UUID)
|
||||
|
||||
# if defined(DEBUG)
|
||||
|
||||
|
@ -45,15 +45,23 @@ interface IGeckoBackChannel;
|
|||
|
||||
#elif defined(NIGHTLY_BUILD)
|
||||
|
||||
// Official Nightly
|
||||
// Nightly Builds
|
||||
# define IHANDLERCONTROL_IID c57343fc-e011-40c2-b748-da82eabf0f1f
|
||||
# define ASYNCIHANDLERCONTROL_IID 648c92a1-ea35-46da-a806-6b55c6247373
|
||||
# define HANDLER_CLSID 4629216b-8753-41bf-9527-5bff51401671
|
||||
# define IGECKOBACKCHANNEL_IID e61e038d-40dd-464a-9aba-66b206b6911b
|
||||
|
||||
#elif defined(USE_BETA_UUID)
|
||||
|
||||
// Beta Builds
|
||||
# define IHANDLERCONTROL_IID 119149fa-d212-4f22-9517-082eecc1a084
|
||||
# define ASYNCIHANDLERCONTROL_IID 4e253d9b-59cf-4b32-a973-38bc85495d61
|
||||
# define HANDLER_CLSID 21e9f98d-a6c9-4cb5-b288-ae2fd2a96c58
|
||||
# define IGECKOBACKCHANNEL_IID 77b75c7d-d1c2-4469-864d-31aaebb67cc6
|
||||
|
||||
#elif defined(RELEASE_OR_BETA)
|
||||
|
||||
// Official Beta and Official Release
|
||||
// Release Builds
|
||||
# define IHANDLERCONTROL_IID ce30f77e-8847-44f0-a648-a9656bd89c0d
|
||||
# define ASYNCIHANDLERCONTROL_IID dca8d857-1a63-4045-8f36-8809eb093d04
|
||||
# define HANDLER_CLSID 1baa303d-b4b9-45e5-9ccb-e3fca3e274b6
|
||||
|
@ -61,7 +69,7 @@ interface IGeckoBackChannel;
|
|||
|
||||
#else
|
||||
|
||||
// Official Aurora
|
||||
// Catch-all
|
||||
# define IHANDLERCONTROL_IID 3316ce35-f892-4832-97c5-06c52c03cdba
|
||||
# define ASYNCIHANDLERCONTROL_IID 15b48b76-ad38-4ad3-bd1a-d3c48a5a9947
|
||||
# define HANDLER_CLSID 4a195748-dca2-45fb-9295-0a139e76a9e7
|
||||
|
|
|
@ -20,7 +20,7 @@ export:: $(MIDL_GENERATED_FILES)
|
|||
$(MIDL_GENERATED_FILES): midl_done
|
||||
|
||||
midl_done: HandlerData.acf HandlerData.idl
|
||||
$(MIDL) $(MIDL_FLAGS) -I $(IA2DIR) -I $(MSAADIR) -Oicf -acf $(srcdir)/HandlerData.acf $(srcdir)/HandlerData.idl
|
||||
$(MIDL) $(MIDL_FLAGS) $(DEFINES) -I $(IA2DIR) -I $(MSAADIR) -Oicf -acf $(srcdir)/HandlerData.acf $(srcdir)/HandlerData.idl
|
||||
touch $@
|
||||
|
||||
INSTALL_TARGETS += midl
|
||||
|
|
|
@ -50,6 +50,15 @@ RCINCLUDE = 'AccessibleHandler.rc'
|
|||
DEFINES['ENTRY_PREFIX'] = 'Proxy'
|
||||
DEFINES['REGISTER_PROXY_DLL'] = True
|
||||
|
||||
# We want to generate distinct UUIDs on a per-channel basis, so we need
|
||||
# finer granularity than the standard preprocessor definitions offer.
|
||||
# These defines allow us to separate local builds from automated builds,
|
||||
# as well as separate beta from release.
|
||||
if CONFIG['MOZ_UPDATE_CHANNEL'] == 'default':
|
||||
DEFINES['USE_LOCAL_UUID'] = True
|
||||
elif CONFIG['MOZ_UPDATE_CHANNEL'] == 'beta':
|
||||
DEFINES['USE_BETA_UUID'] = True
|
||||
|
||||
# This DLL may be loaded into other processes, so we need static libs for
|
||||
# Windows 7 and Windows 8.
|
||||
USE_STATIC_LIBS = True
|
||||
|
|
|
@ -260,27 +260,10 @@ Target.prototype = {
|
|||
this._logHistogram(data.metric);
|
||||
},
|
||||
|
||||
_getAddonHistogram(item) {
|
||||
let appName = this._getAddonHistogramName(item, APPNAME_IDX);
|
||||
let histName = this._getAddonHistogramName(item, HISTNAME_IDX);
|
||||
|
||||
return Services.telemetry.getAddonHistogram(appName, CUSTOM_HISTOGRAM_PREFIX
|
||||
+ histName);
|
||||
},
|
||||
|
||||
_getAddonHistogramName(item, index) {
|
||||
let array = item.split('_');
|
||||
return array[index].toUpperCase();
|
||||
},
|
||||
|
||||
_clearTelemetryData() {
|
||||
developerHUD._histograms.forEach(function(item) {
|
||||
Services.telemetry.getKeyedHistogramById(item).clear();
|
||||
});
|
||||
|
||||
developerHUD._customHistograms.forEach(item => {
|
||||
this._getAddonHistogram(item).clear();
|
||||
});
|
||||
},
|
||||
|
||||
_sendTelemetryData() {
|
||||
|
@ -291,7 +274,6 @@ Target.prototype = {
|
|||
let frame = this.frame;
|
||||
let payload = {
|
||||
keyedHistograms: {},
|
||||
addonHistograms: {}
|
||||
};
|
||||
// Package the hud histograms.
|
||||
developerHUD._histograms.forEach(function(item) {
|
||||
|
@ -299,20 +281,6 @@ Target.prototype = {
|
|||
Services.telemetry.getKeyedHistogramById(item).snapshot();
|
||||
});
|
||||
|
||||
// Package the registered hud custom histograms
|
||||
developerHUD._customHistograms.forEach(item => {
|
||||
let appName = this._getAddonHistogramName(item, APPNAME_IDX);
|
||||
let histName = CUSTOM_HISTOGRAM_PREFIX +
|
||||
this._getAddonHistogramName(item, HISTNAME_IDX);
|
||||
let addonHist = Services.telemetry.getAddonHistogram(appName, histName).snapshot();
|
||||
if (!(appName in payload.addonHistograms)) {
|
||||
payload.addonHistograms[appName] = {};
|
||||
}
|
||||
// Do not include histograms with sum of 0.
|
||||
if (addonHist.sum > 0) {
|
||||
payload.addonHistograms[appName][histName] = addonHist;
|
||||
}
|
||||
});
|
||||
shell.sendEvent(frame, 'advanced-telemetry-update', Cu.cloneInto(payload, frame));
|
||||
},
|
||||
|
||||
|
|
Разница между файлами не показана из-за своего большого размера
Загрузить разницу
|
@ -65,10 +65,7 @@ var gFxAccounts = {
|
|||
get loginFailed() {
|
||||
// Referencing Weave.Service will implicitly initialize sync, and we don't
|
||||
// want to force that - so first check if it is ready.
|
||||
let service = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
if (!service.ready) {
|
||||
if (!this.weaveService.ready) {
|
||||
return false;
|
||||
}
|
||||
// LOGIN_FAILED_LOGIN_REJECTED explicitly means "you must log back in".
|
||||
|
@ -376,7 +373,8 @@ var gFxAccounts = {
|
|||
},
|
||||
|
||||
updateTabContextMenu(aPopupMenu, aTargetTab) {
|
||||
if (!this.sendTabToDeviceEnabled) {
|
||||
if (!this.sendTabToDeviceEnabled ||
|
||||
!this.weaveService.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -388,7 +386,8 @@ var gFxAccounts = {
|
|||
},
|
||||
|
||||
initPageContextMenu(contextMenu) {
|
||||
if (!this.sendTabToDeviceEnabled) {
|
||||
if (!this.sendTabToDeviceEnabled ||
|
||||
!this.weaveService.ready) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -422,3 +421,10 @@ XPCOMUtils.defineLazyGetter(gFxAccounts, "FxAccountsCommon", function() {
|
|||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "EnsureFxAccountsWebChannel",
|
||||
"resource://gre/modules/FxAccountsWebChannel.jsm");
|
||||
|
||||
|
||||
XPCOMUtils.defineLazyGetter(gFxAccounts, "weaveService", function() {
|
||||
return Components.classes["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
});
|
||||
|
|
|
@ -346,6 +346,7 @@ let gDecoderDoctorHandler = {
|
|||
|
||||
let params = new URLSearchParams;
|
||||
params.append("url", docURL);
|
||||
params.append("label", "type-media");
|
||||
params.append("problem_type", "video_bug");
|
||||
params.append("src", "media-decode-error");
|
||||
params.append("details",
|
||||
|
|
|
@ -39,6 +39,8 @@ var gSyncUI = {
|
|||
// once syncing completes (bug 1239042).
|
||||
_syncStartTime: 0,
|
||||
_syncAnimationTimer: 0,
|
||||
_withinLastWeekFormat: null,
|
||||
_oneWeekOrOlderFormat: null,
|
||||
|
||||
init() {
|
||||
// Proceed to set up the UI if Sync has already started up.
|
||||
|
@ -59,18 +61,17 @@ var gSyncUI = {
|
|||
|
||||
// Remove the observer if the window is closed before the observer
|
||||
// was triggered.
|
||||
window.addEventListener("unload", function onUnload() {
|
||||
gSyncUI._unloaded = true;
|
||||
window.removeEventListener("unload", onUnload);
|
||||
Services.obs.removeObserver(gSyncUI, "weave:service:ready");
|
||||
Services.obs.removeObserver(gSyncUI, "quit-application");
|
||||
window.addEventListener("unload", () => {
|
||||
this._unloaded = true;
|
||||
Services.obs.removeObserver(this, "weave:service:ready");
|
||||
Services.obs.removeObserver(this, "quit-application");
|
||||
|
||||
if (Weave.Status.ready) {
|
||||
gSyncUI._obs.forEach(function(topic) {
|
||||
Services.obs.removeObserver(gSyncUI, topic);
|
||||
if (this.weaveService.ready) {
|
||||
this._obs.forEach(topic => {
|
||||
Services.obs.removeObserver(this, topic);
|
||||
});
|
||||
}
|
||||
});
|
||||
}, { once: true });
|
||||
},
|
||||
|
||||
initUI: function SUI_initUI() {
|
||||
|
@ -367,21 +368,31 @@ var gSyncUI = {
|
|||
}
|
||||
}),
|
||||
|
||||
getWithinLastWeekFormat() {
|
||||
return this._withinLastWeekFormat ||
|
||||
(this._withinLastWeekFormat =
|
||||
new Intl.DateTimeFormat(undefined, {weekday: "long", hour: "numeric", minute: "numeric"}));
|
||||
},
|
||||
|
||||
getOneWeekOrOlderFormat() {
|
||||
return this._oneWeekOrOlderFormat ||
|
||||
(this._oneWeekOrOlderFormat =
|
||||
new Intl.DateTimeFormat(undefined, {month: "long", day: "numeric"}));
|
||||
},
|
||||
|
||||
formatLastSyncDate(date) {
|
||||
let dateFormat;
|
||||
let sixDaysAgo = (() => {
|
||||
let tempDate = new Date();
|
||||
tempDate.setDate(tempDate.getDate() - 6);
|
||||
tempDate.setHours(0, 0, 0, 0);
|
||||
return tempDate;
|
||||
})();
|
||||
// It may be confusing for the user to see "Last Sync: Monday" when the last sync was a indeed a Monday but 3 weeks ago
|
||||
if (date < sixDaysAgo) {
|
||||
dateFormat = {month: "long", day: "numeric"};
|
||||
} else {
|
||||
dateFormat = {weekday: "long", hour: "numeric", minute: "numeric"};
|
||||
}
|
||||
let lastSyncDateString = date.toLocaleDateString(undefined, dateFormat);
|
||||
|
||||
// It may be confusing for the user to see "Last Sync: Monday" when the last
|
||||
// sync was indeed a Monday, but 3 weeks ago.
|
||||
let dateFormat = date < sixDaysAgo ? this.getOneWeekOrOlderFormat() : this.getWithinLastWeekFormat();
|
||||
|
||||
let lastSyncDateString = dateFormat.format(date);
|
||||
return this._stringBundle.formatStringFromName("lastSync2.label", [lastSyncDateString], 1);
|
||||
},
|
||||
|
||||
|
|
|
@ -4898,7 +4898,7 @@
|
|||
tab.linkedBrowser &&
|
||||
tab.linkedBrowser.isRemoteBrowser) {
|
||||
label += " - e10s";
|
||||
if (Services.prefs.getIntPref("dom.ipc.processCount") > 1) {
|
||||
if (Services.appinfo.maxWebProcessCount > 1) {
|
||||
label += " (" + tab.linkedBrowser.frameLoader.tabParent.osPid + ")";
|
||||
}
|
||||
}
|
||||
|
@ -5909,6 +5909,12 @@
|
|||
|
||||
<method name="adjustTabstrip">
|
||||
<body><![CDATA[
|
||||
// If we're overflowing, tab widths don't change anymore, so we can
|
||||
// return early to avoid flushing layout.
|
||||
if (this.getAttribute("overflow") == "true") {
|
||||
return;
|
||||
}
|
||||
|
||||
let numTabs = this.childNodes.length -
|
||||
this.tabbrowser._removingTabs.length;
|
||||
if (numTabs > 2) {
|
||||
|
@ -6242,7 +6248,7 @@
|
|||
if (width != this.mTabstripWidth) {
|
||||
this.adjustTabstrip();
|
||||
this._fillTrailingGap();
|
||||
this._handleTabSelect();
|
||||
this._handleTabSelect(false);
|
||||
this.mTabstripWidth = width;
|
||||
}
|
||||
break;
|
||||
|
@ -7689,7 +7695,7 @@
|
|||
if (val) {
|
||||
this.setAttribute("label", val);
|
||||
this.removeAttribute("inactive");
|
||||
this._calcMouseTargetRect();
|
||||
this._mouseTargetRect = null;
|
||||
MousePosTracker.addListener(this);
|
||||
} else {
|
||||
this.setAttribute("inactive", "true");
|
||||
|
@ -7705,6 +7711,9 @@
|
|||
|
||||
<method name="getMouseTargetRect">
|
||||
<body><![CDATA[
|
||||
if (!this._mouseTargetRect) {
|
||||
this._calcMouseTargetRect();
|
||||
}
|
||||
return this._mouseTargetRect;
|
||||
]]></body>
|
||||
</method>
|
||||
|
@ -7729,7 +7738,7 @@
|
|||
|
||||
switch (event.type) {
|
||||
case "resize":
|
||||
this._calcMouseTargetRect();
|
||||
this._mouseTargetRect = null;
|
||||
break;
|
||||
}
|
||||
]]></body>
|
||||
|
@ -7760,7 +7769,7 @@
|
|||
|
||||
if (!this.hasAttribute("sizelimit")) {
|
||||
this.setAttribute("sizelimit", "true");
|
||||
this._calcMouseTargetRect();
|
||||
this._mouseTargetRect = null;
|
||||
}
|
||||
</body>
|
||||
</method>
|
||||
|
|
|
@ -896,6 +896,7 @@ add_task(function* test_plaintext_sendpagetodevice() {
|
|||
if (!gFxAccounts.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
yield ensureSyncReady();
|
||||
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
|
||||
|
||||
let plainTextItemsWithSendPage =
|
||||
|
@ -933,6 +934,7 @@ add_task(function* test_link_sendlinktodevice() {
|
|||
if (!gFxAccounts.sendTabToDeviceEnabled) {
|
||||
return;
|
||||
}
|
||||
yield ensureSyncReady();
|
||||
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
|
||||
|
||||
yield test_contextmenu("#test-link",
|
||||
|
@ -990,3 +992,10 @@ function* selectText(selector) {
|
|||
win.getSelection().addRange(div);
|
||||
});
|
||||
}
|
||||
|
||||
function ensureSyncReady() {
|
||||
let service = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
return service.whenLoaded();
|
||||
}
|
||||
|
|
|
@ -158,7 +158,7 @@ add_task(function* test_decode_error() {
|
|||
gNavigatorBundle.getString("decoder.decodeError.button"),
|
||||
gNavigatorBundle.getString("decoder.decodeError.accesskey"),
|
||||
tab_checker_for_webcompat(
|
||||
{url: "DocURL", problem_type: "video_bug",
|
||||
{url: "DocURL", label: "type-media", problem_type: "video_bug",
|
||||
details: "Technical Information:\nDecodeIssue\nResource: ResURL"}));
|
||||
});
|
||||
|
||||
|
@ -174,6 +174,6 @@ add_task(function* test_decode_warning() {
|
|||
gNavigatorBundle.getString("decoder.decodeError.button"),
|
||||
gNavigatorBundle.getString("decoder.decodeError.accesskey"),
|
||||
tab_checker_for_webcompat(
|
||||
{url: "DocURL", problem_type: "video_bug",
|
||||
{url: "DocURL", label: "type-media", problem_type: "video_bug",
|
||||
details: "Technical Information:\nDecodeIssue\nResource: ResURL"}));
|
||||
});
|
||||
|
|
|
@ -21,6 +21,7 @@ add_task(function* test() {
|
|||
const origIsSendableURI = gFxAccounts.isSendableURI;
|
||||
gFxAccounts.isSendableURI = () => true;
|
||||
// Check the send tab to device menu item
|
||||
yield ensureSyncReady();
|
||||
const oldGetter = setupRemoteClientsFixture(remoteClientsFixture);
|
||||
yield updateTabContextMenu(origTab, function* () {
|
||||
yield openMenuItemSubmenu("context_sendTabToDevice");
|
||||
|
@ -76,3 +77,10 @@ add_task(function* test() {
|
|||
gBrowser.removeTab(pinned);
|
||||
});
|
||||
|
||||
function ensureSyncReady() {
|
||||
let service = Cc["@mozilla.org/weave/service;1"]
|
||||
.getService(Components.interfaces.nsISupports)
|
||||
.wrappedJSObject;
|
||||
return service.whenLoaded();
|
||||
}
|
||||
|
||||
|
|
|
@ -11,4 +11,4 @@ BROWSER_CHROME_MANIFESTS += [
|
|||
JAR_MANIFESTS += ['jar.mn']
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Contextual Identity')
|
||||
BUG_COMPONENT = ('DOM', 'Security')
|
||||
|
|
|
@ -254,12 +254,14 @@ add_task(function* () {
|
|||
id: "guid_mobile",
|
||||
type: "client",
|
||||
name: "My Phone",
|
||||
lastModified: 1492201200,
|
||||
tabs: [],
|
||||
},
|
||||
{
|
||||
id: "guid_desktop",
|
||||
type: "client",
|
||||
name: "My Desktop",
|
||||
lastModified: 1492201200,
|
||||
tabs: [
|
||||
{
|
||||
title: "http://example.com/10",
|
||||
|
@ -278,6 +280,7 @@ add_task(function* () {
|
|||
{
|
||||
id: "guid_second_desktop",
|
||||
name: "My Other Desktop",
|
||||
lastModified: 1492201200,
|
||||
tabs: [
|
||||
{
|
||||
title: "http://example.com/6",
|
||||
|
@ -351,6 +354,7 @@ add_task(function* () {
|
|||
id: "guid_desktop",
|
||||
type: "client",
|
||||
name: "My Desktop",
|
||||
lastModified: 1492201200,
|
||||
tabs: function() {
|
||||
let allTabsDesktop = [];
|
||||
// We choose 77 tabs, because TABS_PER_PAGE is 25, which means
|
||||
|
|
|
@ -67,6 +67,7 @@ support-files =
|
|||
[browser_ext_incognito_popup.js]
|
||||
[browser_ext_lastError.js]
|
||||
[browser_ext_omnibox.js]
|
||||
skip-if = debug || asan # Bug 1354681
|
||||
[browser_ext_optionsPage_browser_style.js]
|
||||
[browser_ext_optionsPage_privileges.js]
|
||||
[browser_ext_pageAction_context.js]
|
||||
|
|
|
@ -34,11 +34,11 @@ function run_test() {
|
|||
// url prefix for test history population
|
||||
const TEST_URL = "https://mozilla.com/";
|
||||
// time when the test starts execution
|
||||
const TIME_NOW = (new Date()).getTime();
|
||||
const TIME_NOW = new Date();
|
||||
|
||||
// utility function to compute past timestap
|
||||
function timeDaysAgo(numDays) {
|
||||
return TIME_NOW - (numDays * 24 * 60 * 60 * 1000);
|
||||
return new Date(TIME_NOW - (numDays * 24 * 60 * 60 * 1000));
|
||||
}
|
||||
|
||||
// utility function to make a visit for insetion into places db
|
||||
|
|
|
@ -85,6 +85,7 @@
|
|||
<spacer flex="1"/>
|
||||
<vbox>
|
||||
<button id="showUpdateHistory"
|
||||
class="accessory-button"
|
||||
label="&updateHistory2.label;"
|
||||
accesskey="&updateHistory2.accesskey;"
|
||||
preference="app.update.disable_button.showUpdateHistory"/>
|
||||
|
|
|
@ -241,6 +241,7 @@
|
|||
<hbox align="center" class="indent">
|
||||
<label id="isNotDefaultLabel" flex="1">&isNotDefault.label;</label>
|
||||
<button id="setDefaultButton"
|
||||
class="accessory-button"
|
||||
label="&setAsMyDefaultBrowser2.label;" accesskey="&setAsMyDefaultBrowser2.accesskey;"
|
||||
preference="pref.general.disable_button.default_browser"/>
|
||||
</hbox>
|
||||
|
@ -357,15 +358,13 @@
|
|||
<hbox>
|
||||
<button id="restoreDefaultSearchEngines"
|
||||
label="&restoreDefaultSearchEngines.label;"
|
||||
accesskey="&restoreDefaultSearchEngines.accesskey;"
|
||||
/>
|
||||
accesskey="&restoreDefaultSearchEngines.accesskey;"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="removeEngineButton"
|
||||
class="searchEngineAction"
|
||||
class="searchEngineAction accessory-button"
|
||||
label="&removeEngine.label;"
|
||||
accesskey="&removeEngine.accesskey;"
|
||||
disabled="true"
|
||||
/>
|
||||
disabled="true"/>
|
||||
</hbox>
|
||||
|
||||
<separator class="thin"/>
|
||||
|
@ -441,6 +440,7 @@
|
|||
<hbox id="languagesBox" align="center">
|
||||
<description flex="1" control="chooseLanguage">&chooseLanguage.label;</description>
|
||||
<button id="chooseLanguage"
|
||||
class="accessory-button"
|
||||
label="&chooseButton.label;"
|
||||
accesskey="&chooseButton.accesskey;"/>
|
||||
</hbox>
|
||||
|
@ -460,7 +460,9 @@
|
|||
<label>&translation.options.attribution.afterLogo;</label>
|
||||
</hbox>
|
||||
</hbox>
|
||||
<button id="translateButton" label="&translateExceptions.label;"
|
||||
<button id="translateButton"
|
||||
class="accessory-button"
|
||||
label="&translateExceptions.label;"
|
||||
accesskey="&translateExceptions.accesskey;"/>
|
||||
</hbox>
|
||||
<hbox>
|
||||
|
@ -519,13 +521,17 @@
|
|||
</menupopup>
|
||||
</menulist>
|
||||
</hbox>
|
||||
<button id="advancedFonts" icon="select-font"
|
||||
<button id="advancedFonts"
|
||||
class="accessory-button"
|
||||
icon="select-font"
|
||||
label="&advancedFonts.label;"
|
||||
accesskey="&advancedFonts.accesskey;"/>
|
||||
</row>
|
||||
<row id="colorsRow">
|
||||
<hbox/>
|
||||
<button id="colors" icon="select-color"
|
||||
<button id="colors"
|
||||
class="accessory-button"
|
||||
icon="select-color"
|
||||
label="&colors.label;"
|
||||
accesskey="&colors.accesskey;"/>
|
||||
</row>
|
||||
|
|
|
@ -251,6 +251,7 @@
|
|||
onsynctopreference="return gPrivacyPane.writeAcceptCookies();"/>
|
||||
<spacer flex="1" />
|
||||
<button id="cookieExceptions"
|
||||
class="accessory-button"
|
||||
label="&cookieExceptions.label;" accesskey="&cookieExceptions.accesskey;"
|
||||
preference="pref.privacy.disable_button.cookie_exceptions"/>
|
||||
</hbox>
|
||||
|
@ -284,6 +285,7 @@
|
|||
</menulist>
|
||||
<spacer flex="1"/>
|
||||
<button id="showCookiesButton"
|
||||
class="accessory-button"
|
||||
label="&showCookies.label;" accesskey="&showCookies.accesskey;"
|
||||
preference="pref.privacy.disable_button.view_cookies"/>
|
||||
</hbox>
|
||||
|
@ -294,7 +296,9 @@
|
|||
label="&clearOnClose.label;"
|
||||
accesskey="&clearOnClose.accesskey;"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="clearDataSettings" label="&clearOnCloseSettings.label;"
|
||||
<button id="clearDataSettings"
|
||||
class="accessory-button"
|
||||
label="&clearOnCloseSettings.label;"
|
||||
accesskey="&clearOnCloseSettings.accesskey;"/>
|
||||
</hbox>
|
||||
</vbox>
|
||||
|
@ -314,6 +318,7 @@
|
|||
onsyncfrompreference="return gPrivacyPane.readSavePasswords();"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="passwordExceptions"
|
||||
class="accessory-button"
|
||||
label="&passwordExceptions.label;"
|
||||
accesskey="&passwordExceptions.accesskey;"
|
||||
preference="pref.privacy.disable_button.view_passwords_exceptions"/>
|
||||
|
@ -332,12 +337,14 @@
|
|||
<spacer flex="1"/>
|
||||
</hbox>
|
||||
<button id="changeMasterPassword"
|
||||
class="accessory-button"
|
||||
label="&changeMasterPassword.label;"
|
||||
accesskey="&changeMasterPassword.accesskey;"/>
|
||||
</row>
|
||||
<row id="showPasswordRow">
|
||||
<hbox id="showPasswordsBox"/>
|
||||
<button id="showPasswords"
|
||||
class="accessory-button"
|
||||
label="&savedLogins.label;" accesskey="&savedLogins.accesskey;"
|
||||
preference="pref.privacy.disable_button.view_passwords"/>
|
||||
</row>
|
||||
|
@ -375,10 +382,12 @@
|
|||
<spacer flex="1"/>
|
||||
<vbox id="trackingProtectionAdvancedSettings">
|
||||
<button id="trackingProtectionExceptions"
|
||||
class="accessory-button"
|
||||
label="&trackingProtectionExceptions.label;"
|
||||
accesskey="&trackingProtectionExceptions.accesskey;"
|
||||
preference="pref.privacy.disable_button.tracking_protection_exceptions"/>
|
||||
<button id="changeBlockList"
|
||||
class="accessory-button"
|
||||
label="&changeBlockList.label;"
|
||||
accesskey="&changeBlockList.accesskey;"
|
||||
preference="pref.privacy.disable_button.change_blocklist"/>
|
||||
|
@ -425,7 +434,9 @@
|
|||
'dom.disable_open_during_load');"/>
|
||||
</vbox>
|
||||
<hbox pack="end">
|
||||
<button id="popupPolicyButton" label="&popupExceptions.label;"
|
||||
<button id="popupPolicyButton"
|
||||
class="accessory-button"
|
||||
label="&popupExceptions.label;"
|
||||
accesskey="&popupExceptions.accesskey;"/>
|
||||
</hbox>
|
||||
</row>
|
||||
|
@ -450,7 +461,9 @@
|
|||
value="¬ificationsPolicyLearnMore.label;"/>
|
||||
</hbox>
|
||||
<hbox pack="end">
|
||||
<button id="notificationsPolicyButton" label="¬ificationsPolicyButton.label;"
|
||||
<button id="notificationsPolicyButton"
|
||||
class="accessory-button"
|
||||
label="¬ificationsPolicyButton.label;"
|
||||
accesskey="¬ificationsPolicyButton.accesskey;"/>
|
||||
</hbox>
|
||||
</row>
|
||||
|
@ -499,6 +512,7 @@
|
|||
onsyncfrompreference="return gPrivacyPane.readWarnAddonInstall();"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="addonExceptions"
|
||||
class="accessory-button"
|
||||
label="&addonExceptions.label;"
|
||||
accesskey="&addonExceptions.accesskey;"/>
|
||||
</hbox>
|
||||
|
@ -551,12 +565,12 @@
|
|||
<spacer flex="1"/>
|
||||
<vbox>
|
||||
<button id="viewCertificatesButton"
|
||||
flex="1"
|
||||
class="accessory-button"
|
||||
label="&viewCerts.label;"
|
||||
accesskey="&viewCerts.accesskey;"
|
||||
preference="security.disable_button.openCertManager"/>
|
||||
<button id="viewSecurityDevicesButton"
|
||||
flex="1"
|
||||
class="accessory-button"
|
||||
label="&viewSecurityDevices.label;"
|
||||
accesskey="&viewSecurityDevices.accesskey;"
|
||||
preference="security.disable_button.openDeviceManager"/>
|
||||
|
@ -611,7 +625,10 @@
|
|||
|
||||
<hbox align="center">
|
||||
<description flex="1" control="connectionSettings">&connectionDesc.label;</description>
|
||||
<button id="connectionSettings" icon="network" label="&connectionSettings.label;"
|
||||
<button id="connectionSettings"
|
||||
class="accessory-button"
|
||||
icon="network"
|
||||
label="&connectionSettings.label;"
|
||||
accesskey="&connectionSettings.accesskey;"/>
|
||||
</hbox>
|
||||
</groupbox>
|
||||
|
@ -622,7 +639,9 @@
|
|||
|
||||
<hbox align="center">
|
||||
<label id="actualDiskCacheSize" flex="1"/>
|
||||
<button id="clearCacheButton" icon="clear"
|
||||
<button id="clearCacheButton"
|
||||
class="accessory-button"
|
||||
icon="clear"
|
||||
label="&clearCacheNow.label;" accesskey="&clearCacheNow.accesskey;"/>
|
||||
</hbox>
|
||||
<hbox>
|
||||
|
@ -649,7 +668,9 @@
|
|||
|
||||
<hbox align="center">
|
||||
<label id="actualAppCacheSize" flex="1"/>
|
||||
<button id="clearOfflineAppCacheButton" icon="clear"
|
||||
<button id="clearOfflineAppCacheButton"
|
||||
class="accessory-button"
|
||||
icon="clear"
|
||||
label="&clearOfflineAppCacheNow.label;" accesskey="&clearOfflineAppCacheNow.accesskey;"/>
|
||||
</hbox>
|
||||
<hbox align="center">
|
||||
|
@ -659,6 +680,7 @@
|
|||
onsyncfrompreference="return gPrivacyPane.readOfflineNotify();"/>
|
||||
<spacer flex="1"/>
|
||||
<button id="offlineNotifyExceptions"
|
||||
class="accessory-button"
|
||||
label="&offlineStorageNotifyExceptions.label;"
|
||||
accesskey="&offlineStorageNotifyExceptions.accesskey;"/>
|
||||
</hbox>
|
||||
|
@ -672,6 +694,7 @@
|
|||
</vbox>
|
||||
<vbox pack="end">
|
||||
<button id="offlineAppsListRemove"
|
||||
class="accessory-button"
|
||||
disabled="true"
|
||||
label="&offlineAppsListRemove.label;"
|
||||
accesskey="&offlineAppsListRemove.accesskey;"/>
|
||||
|
@ -687,11 +710,14 @@
|
|||
<label id="totalSiteDataSize"></label>
|
||||
<label id="siteDataLearnMoreLink" class="learnMore text-link" value="&siteDataLearnMoreLink.label;"></label>
|
||||
<spacer flex="1" />
|
||||
<button id="clearSiteDataButton" icon="clear"
|
||||
<button id="clearSiteDataButton"
|
||||
class="accessory-button"
|
||||
icon="clear"
|
||||
label="&clearSiteData.label;" accesskey="&clearSiteData.accesskey;"/>
|
||||
</hbox>
|
||||
<vbox align="end">
|
||||
<button id="siteDataSettings"
|
||||
class="accessory-button"
|
||||
label="&siteDataSettings.label;"
|
||||
accesskey="&siteDataSettings.accesskey;"/>
|
||||
</vbox>
|
||||
|
|
|
@ -1209,7 +1209,7 @@
|
|||
tooltiptext="&changeSearchSettings.tooltip;"
|
||||
xbl:inherits="compact"/>
|
||||
</xul:description>
|
||||
<xul:vbox anonid="add-engines"/>
|
||||
<xul:vbox anonid="add-engines" class="search-add-engines"/>
|
||||
<xul:button anonid="search-settings"
|
||||
oncommand="showSettings();"
|
||||
class="search-setting-button search-panel-header"
|
||||
|
@ -1225,7 +1225,7 @@
|
|||
</xul:menupopup>
|
||||
</content>
|
||||
|
||||
<implementation implements="nsIDOMEventListener">
|
||||
<implementation implements="nsIDOMEventListener,nsIObserver,nsIWeakReference">
|
||||
|
||||
<!-- Width in pixels of the one-off buttons. 49px is the min-width of
|
||||
each search engine button, adapt this const when changing the css.
|
||||
|
@ -1273,6 +1273,7 @@
|
|||
</property>
|
||||
|
||||
<field name="_textbox">null</field>
|
||||
<field name="_textboxWidth">0</field>
|
||||
|
||||
<!-- The textbox associated with the one-offs. Set this to a textbox to
|
||||
automatically keep the related one-offs UI up to date. Otherwise you
|
||||
|
@ -1415,6 +1416,10 @@
|
|||
this._ignoreMouseEvents = false;
|
||||
aEvent.stopPropagation();
|
||||
});
|
||||
|
||||
// Add weak referenced observers to invalidate our cached list of engines.
|
||||
Services.prefs.addObserver("browser.search.hiddenOneOffs", this, true);
|
||||
Services.obs.addObserver(this, "browser-search-engine-modified", true);
|
||||
]]></constructor>
|
||||
|
||||
<!-- This handles events outside the one-off buttons, like on the popup
|
||||
|
@ -1443,6 +1448,16 @@
|
|||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="observe">
|
||||
<parameter name="aEngine"/>
|
||||
<parameter name="aTopic"/>
|
||||
<parameter name="aData"/>
|
||||
<body><![CDATA[
|
||||
// Make sure the engine list is refetched next time it's needed.
|
||||
this._engines = null;
|
||||
]]></body>
|
||||
</method>
|
||||
|
||||
<method name="showSettings">
|
||||
<body><![CDATA[
|
||||
BrowserUITelemetry.countSearchSettingsEvent(this.telemetryOrigin);
|
||||
|
@ -1491,20 +1506,60 @@
|
|||
]]></body>
|
||||
</method>
|
||||
|
||||
<field name="_engines">null</field>
|
||||
<property name="engines" readonly="true">
|
||||
<getter><![CDATA[
|
||||
if (this._engines)
|
||||
return this._engines;
|
||||
let currentEngineNameToIgnore;
|
||||
if (!this.getAttribute("includecurrentengine"))
|
||||
currentEngineNameToIgnore = Services.search.currentEngine.name;
|
||||
|
||||
let pref = Services.prefs.getStringPref("browser.search.hiddenOneOffs");
|
||||
let hiddenList = pref ? pref.split(",") : [];
|
||||
|
||||
this._engines = Services.search.getVisibleEngines().filter(e => {
|
||||
let name = e.name;
|
||||
return (!currentEngineNameToIgnore ||
|
||||
name != currentEngineNameToIgnore) &&
|
||||
!hiddenList.includes(name);
|
||||
});
|
||||
|
||||
return this._engines;
|
||||
]]></getter>
|
||||
</property>
|
||||
|
||||
<!-- Builds all the UI. -->
|
||||
<method name="_rebuild">
|
||||
<body><![CDATA[
|
||||
// Update the 'Search for <keywords> with:" header.
|
||||
this._updateAfterQueryChanged();
|
||||
|
||||
let list = document.getAnonymousElementByAttribute(this, "anonid",
|
||||
"search-panel-one-offs");
|
||||
|
||||
// Handle opensearch items. This needs to be done before building the
|
||||
// list of one off providers, as that code will return early if all the
|
||||
// alternative engines are hidden.
|
||||
this._rebuildAddEngineList();
|
||||
// Skip this in compact mode, ie. for the urlbar.
|
||||
if (!this.compact)
|
||||
this._rebuildAddEngineList();
|
||||
|
||||
// Check if the one-off buttons really need to be rebuilt.
|
||||
if (this._textbox) {
|
||||
// We can't get a reliable value for the popup width without flushing,
|
||||
// but the popup width won't change if the textbox width doesn't.
|
||||
let DOMUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIDOMWindowUtils);
|
||||
let textboxWidth =
|
||||
DOMUtils.getBoundsWithoutFlushing(this._textbox).width;
|
||||
// We can return early if neither the list of engines nor the panel
|
||||
// width has changed.
|
||||
if (this._engines && this._textboxWidth == textboxWidth) {
|
||||
return;
|
||||
}
|
||||
this._textboxWidth = textboxWidth;
|
||||
}
|
||||
|
||||
let list = document.getAnonymousElementByAttribute(this, "anonid",
|
||||
"search-panel-one-offs");
|
||||
let settingsButton =
|
||||
document.getAnonymousElementByAttribute(this, "anonid",
|
||||
"search-settings-compact");
|
||||
|
@ -1516,24 +1571,15 @@
|
|||
if (settingsButton.nextSibling)
|
||||
settingsButton.nextSibling.remove();
|
||||
|
||||
let Preferences =
|
||||
Cu.import("resource://gre/modules/Preferences.jsm", {}).Preferences;
|
||||
let pref = Preferences.get("browser.search.hiddenOneOffs");
|
||||
let hiddenList = pref ? pref.split(",") : [];
|
||||
|
||||
let currentEngineName = Services.search.currentEngine.name;
|
||||
let includeCurrentEngine = this.getAttribute("includecurrentengine");
|
||||
let engines = Services.search.getVisibleEngines().filter(e => {
|
||||
return (includeCurrentEngine || e.name != currentEngineName) &&
|
||||
!hiddenList.includes(e.name);
|
||||
});
|
||||
let engines = this.engines;
|
||||
let oneOffCount = engines.length;
|
||||
|
||||
let header = document.getAnonymousElementByAttribute(this, "anonid",
|
||||
"search-panel-one-offs-header")
|
||||
// header is a xul:deck so collapsed doesn't work on it, see bug 589569.
|
||||
header.hidden = list.collapsed = !engines.length;
|
||||
header.hidden = list.collapsed = !oneOffCount;
|
||||
|
||||
if (!engines.length)
|
||||
if (!oneOffCount)
|
||||
return;
|
||||
|
||||
let panelWidth = parseInt(this.popup.clientWidth);
|
||||
|
@ -1547,7 +1593,6 @@
|
|||
// If the <description> tag with the list of search engines doesn't have
|
||||
// a fixed height, the panel will be sized incorrectly, causing the bottom
|
||||
// of the suggestion <tree> to be hidden.
|
||||
let oneOffCount = engines.length;
|
||||
if (this.compact)
|
||||
++oneOffCount;
|
||||
let rowCount = Math.ceil(oneOffCount / enginesPerRow);
|
||||
|
@ -1647,20 +1692,12 @@
|
|||
}
|
||||
|
||||
// Add a button for each engine that the page in the selected browser
|
||||
// offers, but with the following exceptions:
|
||||
//
|
||||
// (1) Not when the one-offs are compact. Compact one-offs are shown in
|
||||
// the urlbar, and the add-engine buttons span the width of the popup,
|
||||
// so if we added all the engines that a page offers, it could break the
|
||||
// urlbar popup by offering a ton of engines. We should probably make a
|
||||
// smaller version of the buttons for compact one-offs.
|
||||
//
|
||||
// (2) Not when there are too many offered engines. The popup isn't
|
||||
// designed to handle too many (by scrolling for example), so a page
|
||||
// could break the popup by offering too many. Instead, add a single
|
||||
// menu button with a submenu of all the engines.
|
||||
// offers, except when there are too many offered engines.
|
||||
// The popup isn't designed to handle too many (by scrolling for
|
||||
// example), so a page could break the popup by offering too many.
|
||||
// Instead, add a single menu button with a submenu of all the engines.
|
||||
|
||||
if (this.compact || !gBrowser.selectedBrowser.engines) {
|
||||
if (!gBrowser.selectedBrowser.engines) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ const FIXTURE = [
|
|||
{
|
||||
"id": "7cqCr77ptzX3",
|
||||
"type": "client",
|
||||
"lastModified": 1492201200,
|
||||
"name": "zcarter's Nightly on MacBook-Pro-25",
|
||||
"isMobile": false,
|
||||
"tabs": [
|
||||
|
@ -20,6 +21,7 @@ const FIXTURE = [
|
|||
{
|
||||
"id": "2xU5h-4bkWqA",
|
||||
"type": "client",
|
||||
"lastModified": 1492201200,
|
||||
"name": "laptop",
|
||||
"isMobile": false,
|
||||
"tabs": [
|
||||
|
@ -53,6 +55,7 @@ const FIXTURE = [
|
|||
{
|
||||
"id": "OL3EJCsdb2JD",
|
||||
"type": "client",
|
||||
"lastModified": 1492201200,
|
||||
"name": "desktop",
|
||||
"isMobile": false,
|
||||
"tabs": []
|
||||
|
|
|
@ -7,6 +7,7 @@ const FIXTURE = [
|
|||
{
|
||||
"id": "2xU5h-4bkWqA",
|
||||
"type": "client",
|
||||
"lastModified": 1492201200,
|
||||
"name": "laptop",
|
||||
"isMobile": false,
|
||||
"tabs": [
|
||||
|
@ -31,6 +32,7 @@ const FIXTURE = [
|
|||
{
|
||||
"id": "OL3EJCsdb2JD",
|
||||
"type": "client",
|
||||
"lastModified": 1492201200,
|
||||
"name": "desktop",
|
||||
"isMobile": false,
|
||||
"tabs": []
|
||||
|
|
|
@ -34,8 +34,8 @@
|
|||
},
|
||||
{
|
||||
"version": "clang + llvm 3.9.0, built from SVN r290136",
|
||||
"size": 168062128,
|
||||
"digest": "2a5458a25792fcade86a56ff0f4acdfa284d2b62966991a7c34a92c2e8c0b4a162ce00512d4467754e7f74598d64c56e91517e1606ed3fba011f7c10e8ad3288",
|
||||
"size": 166261192,
|
||||
"digest": "52f3fc23f0f5c98050f8b0ac7c92a6752d067582a16f712a5a58074be98975d594f9e36249fc2be7f1cc2ca6d509c663faaf2bea66f949243cc1f41651638ba6",
|
||||
"algorithm": "sha512",
|
||||
"filename": "clang.tar.xz",
|
||||
"unpack": true
|
||||
|
|
|
@ -1,10 +0,0 @@
|
|||
[
|
||||
{
|
||||
"version": "clang 3.8.0",
|
||||
"size": 133060926,
|
||||
"digest": "aff5ad3ac2d41db19d1ba0df5f97b189a7d7e1b6af8c56e22c2b0cced84d75fa98394ded6a4ba5713652e6684a0a46f47aeccf87991f9e849bf8d7d82e564f6f",
|
||||
"algorithm": "sha512",
|
||||
"filename": "clang.tar.bz2",
|
||||
"unpack": true
|
||||
}
|
||||
]
|
|
@ -25,9 +25,9 @@
|
|||
"size": 1603430
|
||||
},
|
||||
{
|
||||
"version": "cctools port from commit hash 84ce22dbb22a26ce7f392e9de0ee39c2efe6fd68",
|
||||
"size": 2174783,
|
||||
"digest": "8678348faff8f344b377075007975ae77a55a2a73488e36950a43c8ec27a79970cd8e34003e33e756a57d9cbf5c3e2e4461184102c6c03f793377a4d250a7f24",
|
||||
"version": "cctools port from commit hash 8e9c3f2506b51",
|
||||
"size": 2233376,
|
||||
"digest": "d632ef587f0253f016aa5323999a3d9576284c04e66b5243a5780af9a55f474ac91ad8dee5bd86a6ee4e2593e2b345e2fd0aa4e8838b3686f84c5c5ac5c9c418",
|
||||
"algorithm": "sha512",
|
||||
"filename": "cctools.tar.bz2",
|
||||
"unpack": true
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[
|
||||
{
|
||||
"version": "clang 3.9.0",
|
||||
"size": 168062128,
|
||||
"digest": "2a5458a25792fcade86a56ff0f4acdfa284d2b62966991a7c34a92c2e8c0b4a162ce00512d4467754e7f74598d64c56e91517e1606ed3fba011f7c10e8ad3288",
|
||||
"version": "clang + llvm 3.9.0, built from SVN r290136",
|
||||
"size": 166261192,
|
||||
"digest": "52f3fc23f0f5c98050f8b0ac7c92a6752d067582a16f712a5a58074be98975d594f9e36249fc2be7f1cc2ca6d509c663faaf2bea66f949243cc1f41651638ba6",
|
||||
"algorithm": "sha512",
|
||||
"filename": "clang.tar.xz",
|
||||
"unpack": true
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[
|
||||
{
|
||||
"version": "clang 3.8.0",
|
||||
"size": 133060926,
|
||||
"digest": "aff5ad3ac2d41db19d1ba0df5f97b189a7d7e1b6af8c56e22c2b0cced84d75fa98394ded6a4ba5713652e6684a0a46f47aeccf87991f9e849bf8d7d82e564f6f",
|
||||
"version": "clang 3.9.0",
|
||||
"size": 184678304,
|
||||
"digest": "cfde9a0f7f59823200f94422b4adb9a2fb5d4d07f240bbd1142c792434f6a1cbb4096d25c9853d77008fc40db0d827daa7003e78016f51241f621d6040ccc635",
|
||||
"algorithm": "sha512",
|
||||
"filename": "clang.tar.bz2",
|
||||
"unpack": true
|
||||
|
@ -16,9 +16,9 @@
|
|||
"unpack": true
|
||||
},
|
||||
{
|
||||
"version": "cctools port from commit hash db1f8d906cb28, ld only",
|
||||
"size": 634496,
|
||||
"digest": "037f31fcf29e7bb7fada0d2bdd5e95c7d4cb2692f2a5c98ed6f6a7561b9d81622d015f0d12b291d3667719655f1369e8ce8a0a4a4773aa0ee4753e04a8821173",
|
||||
"version": "cctools port from commit hash 8e9c3f2506b51",
|
||||
"size": 2233376,
|
||||
"digest": "d632ef587f0253f016aa5323999a3d9576284c04e66b5243a5780af9a55f474ac91ad8dee5bd86a6ee4e2593e2b345e2fd0aa4e8838b3686f84c5c5ac5c9c418",
|
||||
"algorithm": "sha512",
|
||||
"filename": "cctools.tar.bz2",
|
||||
"unpack": true
|
||||
|
|
|
@ -3,9 +3,15 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
|
||||
this.MAIN_MESSAGE_TYPE = "ActivityStream:Main";
|
||||
this.CONTENT_MESSAGE_TYPE = "ActivityStream:Content";
|
||||
|
||||
this.actionTypes = [
|
||||
"INIT",
|
||||
"UNINIT",
|
||||
"NEW_TAB_INITIAL_STATE",
|
||||
"NEW_TAB_LOAD",
|
||||
"NEW_TAB_UNLOAD"
|
||||
// The line below creates an object like this:
|
||||
// {
|
||||
// INIT: "INIT",
|
||||
|
@ -14,6 +20,111 @@ this.actionTypes = [
|
|||
// It prevents accidentally adding a different key/value name.
|
||||
].reduce((obj, type) => { obj[type] = type; return obj; }, {});
|
||||
|
||||
// Helper function for creating routed actions between content and main
|
||||
// Not intended to be used by consumers
|
||||
function _RouteMessage(action, options) {
|
||||
const meta = action.meta ? Object.assign({}, action.meta) : {};
|
||||
if (!options || !options.from || !options.to) {
|
||||
throw new Error("Routed Messages must have options as the second parameter, and must at least include a .from and .to property.");
|
||||
}
|
||||
// For each of these fields, if they are passed as an option,
|
||||
// add them to the action. If they are not defined, remove them.
|
||||
["from", "to", "toTarget", "fromTarget", "skipOrigin"].forEach(o => {
|
||||
if (typeof options[o] !== "undefined") {
|
||||
meta[o] = options[o];
|
||||
} else if (meta[o]) {
|
||||
delete meta[o];
|
||||
}
|
||||
});
|
||||
return Object.assign({}, action, {meta});
|
||||
}
|
||||
|
||||
/**
|
||||
* SendToMain - Creates a message that will be sent to the Main process.
|
||||
*
|
||||
* @param {object} action Any redux action (required)
|
||||
* @param {object} options
|
||||
* @param {string} options.fromTarget The id of the content port from which the action originated. (optional)
|
||||
* @return {object} An action with added .meta properties
|
||||
*/
|
||||
function SendToMain(action, options = {}) {
|
||||
return _RouteMessage(action, {
|
||||
from: CONTENT_MESSAGE_TYPE,
|
||||
to: MAIN_MESSAGE_TYPE,
|
||||
fromTarget: options.fromTarget
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* BroadcastToContent - Creates a message that will be sent to ALL content processes.
|
||||
*
|
||||
* @param {object} action Any redux action (required)
|
||||
* @return {object} An action with added .meta properties
|
||||
*/
|
||||
function BroadcastToContent(action) {
|
||||
return _RouteMessage(action, {
|
||||
from: MAIN_MESSAGE_TYPE,
|
||||
to: CONTENT_MESSAGE_TYPE
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* SendToContent - Creates a message that will be sent to a particular Content process.
|
||||
*
|
||||
* @param {object} action Any redux action (required)
|
||||
* @param {string} target The id of a content port
|
||||
* @return {object} An action with added .meta properties
|
||||
*/
|
||||
function SendToContent(action, target) {
|
||||
if (!target) {
|
||||
throw new Error("You must provide a target ID as the second parameter of SendToContent. If you want to send to all content processes, use BroadcastToContent");
|
||||
}
|
||||
return _RouteMessage(action, {
|
||||
from: MAIN_MESSAGE_TYPE,
|
||||
to: CONTENT_MESSAGE_TYPE,
|
||||
toTarget: target
|
||||
});
|
||||
}
|
||||
|
||||
this.actionCreators = {
|
||||
SendToMain,
|
||||
SendToContent,
|
||||
BroadcastToContent
|
||||
};
|
||||
|
||||
// These are helpers to test for certain kinds of actions
|
||||
this.actionUtils = {
|
||||
isSendToMain(action) {
|
||||
if (!action.meta) {
|
||||
return false;
|
||||
}
|
||||
return action.meta.to === MAIN_MESSAGE_TYPE && action.meta.from === CONTENT_MESSAGE_TYPE;
|
||||
},
|
||||
isBroadcastToContent(action) {
|
||||
if (!action.meta) {
|
||||
return false;
|
||||
}
|
||||
if (action.meta.to === CONTENT_MESSAGE_TYPE && !action.meta.toTarget) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
isSendToContent(action) {
|
||||
if (!action.meta) {
|
||||
return false;
|
||||
}
|
||||
if (action.meta.to === CONTENT_MESSAGE_TYPE && action.meta.toTarget) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
_RouteMessage
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"actionTypes"
|
||||
"actionTypes",
|
||||
"actionCreators",
|
||||
"actionUtils",
|
||||
"MAIN_MESSAGE_TYPE",
|
||||
"CONTENT_MESSAGE_TYPE"
|
||||
];
|
||||
|
|
|
@ -7,6 +7,25 @@
|
|||
<body>
|
||||
<div id="root">
|
||||
<h1>New Tab</h1>
|
||||
<ul id="top-sites"></ul>
|
||||
</div>
|
||||
<script>
|
||||
const topSitesEl = document.getElementById("top-sites");
|
||||
window.addMessageListener("ActivityStream:MainToContent", msg => {
|
||||
if (msg.data.type === "NEW_TAB_INITIAL_STATE") {
|
||||
const fragment = document.createDocumentFragment()
|
||||
for (const row of msg.data.data.TopSites.rows) {
|
||||
const li = document.createElement("li");
|
||||
const a = document.createElement("a");
|
||||
a.href = row.url;
|
||||
a.textContent = row.title;
|
||||
li.appendChild(a);
|
||||
fragment.appendChild(li);
|
||||
}
|
||||
topSitesEl.appendChild(fragment);
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
const {utils: Cu} = Components;
|
||||
const {Store} = Cu.import("resource://activity-stream/lib/Store.jsm", {});
|
||||
|
||||
// Feeds
|
||||
const {NewTabInit} = Cu.import("resource://activity-stream/lib/NewTabInit.jsm", {});
|
||||
|
||||
this.ActivityStream = class ActivityStream {
|
||||
|
||||
/**
|
||||
|
@ -23,7 +26,9 @@ this.ActivityStream = class ActivityStream {
|
|||
}
|
||||
init() {
|
||||
this.initialized = true;
|
||||
this.store.init();
|
||||
this.store.init([
|
||||
new NewTabInit()
|
||||
]);
|
||||
}
|
||||
uninit() {
|
||||
this.store.uninit();
|
||||
|
|
|
@ -0,0 +1,197 @@
|
|||
/* 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/. */
|
||||
/* globals AboutNewTab, RemotePages, XPCOMUtils */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
const {
|
||||
actionUtils: au,
|
||||
actionCreators: ac,
|
||||
actionTypes: at
|
||||
} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "AboutNewTab",
|
||||
"resource:///modules/AboutNewTab.jsm");
|
||||
|
||||
XPCOMUtils.defineLazyModuleGetter(this, "RemotePages",
|
||||
"resource://gre/modules/RemotePageManager.jsm");
|
||||
|
||||
const ABOUT_NEW_TAB_URL = "about:newtab";
|
||||
|
||||
const DEFAULT_OPTIONS = {
|
||||
dispatch(action) {
|
||||
throw new Error(`\nMessageChannel: Received action ${action.type}, but no dispatcher was defined.\n`);
|
||||
},
|
||||
pageURL: ABOUT_NEW_TAB_URL,
|
||||
outgoingMessageName: "ActivityStream:MainToContent",
|
||||
incomingMessageName: "ActivityStream:ContentToMain"
|
||||
};
|
||||
|
||||
this.ActivityStreamMessageChannel = class ActivityStreamMessageChannel {
|
||||
/**
|
||||
* ActivityStreamMessageChannel - This module connects a Redux store to a RemotePageManager in Firefox.
|
||||
* Call .createChannel to start the connection, and .destroyChannel to destroy it.
|
||||
* You should use the BroadcastToContent, SendToContent, and SendToMain action creators
|
||||
* in common/Actions.jsm to help you create actions that will be automatically routed
|
||||
* to the correct location.
|
||||
*
|
||||
* @param {object} options
|
||||
* @param {function} options.dispatch The dispatch method from a Redux store
|
||||
* @param {string} options.pageURL The URL to which a RemotePageManager should be attached.
|
||||
* Note that if it is about:newtab, the existing RemotePageManager
|
||||
* for about:newtab will also be disabled
|
||||
* @param {string} options.outgoingMessageName The name of the message sent to child processes
|
||||
* @param {string} options.incomingMessageName The name of the message received from child processes
|
||||
* @return {ActivityStreamMessageChannel}
|
||||
*/
|
||||
constructor(options = {}) {
|
||||
Object.assign(this, DEFAULT_OPTIONS, options);
|
||||
this.channel = null;
|
||||
|
||||
this.middleware = this.middleware.bind(this);
|
||||
this.onMessage = this.onMessage.bind(this);
|
||||
this.onNewTabLoad = this.onNewTabLoad.bind(this);
|
||||
this.onNewTabUnload = this.onNewTabUnload.bind(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* middleware - Redux middleware that looks for SendToContent and BroadcastToContent type
|
||||
* actions, and sends them out.
|
||||
*
|
||||
* @param {object} store A redux store
|
||||
* @return {function} Redux middleware
|
||||
*/
|
||||
middleware(store) {
|
||||
return next => action => {
|
||||
if (!this.channel) {
|
||||
next(action);
|
||||
return;
|
||||
}
|
||||
if (au.isSendToContent(action)) {
|
||||
this.send(action);
|
||||
} else if (au.isBroadcastToContent(action)) {
|
||||
this.broadcast(action);
|
||||
}
|
||||
next(action);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* onActionFromContent - Handler for actions from a content processes
|
||||
*
|
||||
* @param {object} action A Redux action
|
||||
* @param {string} targetId The portID of the port that sent the message
|
||||
*/
|
||||
onActionFromContent(action, targetId) {
|
||||
this.dispatch(ac.SendToMain(action, {fromTarget: targetId}));
|
||||
}
|
||||
|
||||
/**
|
||||
* broadcast - Sends an action to all ports
|
||||
*
|
||||
* @param {object} action A Redux action
|
||||
*/
|
||||
broadcast(action) {
|
||||
this.channel.sendAsyncMessage(this.outgoingMessageName, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* send - Sends an action to a specific port
|
||||
*
|
||||
* @param {obj} action A redux action; it should contain a portID in the meta.toTarget property
|
||||
*/
|
||||
send(action) {
|
||||
const targetId = action.meta && action.meta.toTarget;
|
||||
const target = this.getTargetById(targetId);
|
||||
if (!target) {
|
||||
// The target is no longer around - maybe the user closed the page
|
||||
return;
|
||||
}
|
||||
target.sendAsyncMessage(this.outgoingMessageName, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* getIdByTarget - Retrieve the id of a message target, if it exists in this.targets
|
||||
*
|
||||
* @param {obj} targetObj A message target
|
||||
* @return {string|null} The unique id of the target, if it exists.
|
||||
*/
|
||||
getTargetById(id) {
|
||||
for (let port of this.channel.messagePorts) {
|
||||
if (port.portID === id) {
|
||||
return port;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* createChannel - Create RemotePages channel to establishing message passing
|
||||
* between the main process and child pages
|
||||
*/
|
||||
createChannel() {
|
||||
// RemotePageManager must be disabled for about:newtab, since only one can exist at once
|
||||
if (this.pageURL === ABOUT_NEW_TAB_URL) {
|
||||
AboutNewTab.override();
|
||||
}
|
||||
this.channel = new RemotePages(this.pageURL);
|
||||
this.channel.addMessageListener("RemotePage:Load", this.onNewTabLoad);
|
||||
this.channel.addMessageListener("RemotePage:Unload", this.onNewTabUnload);
|
||||
this.channel.addMessageListener(this.incomingMessageName, this.onMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
* destroyChannel - Destroys the RemotePages channel
|
||||
*/
|
||||
destroyChannel() {
|
||||
this.channel.destroy();
|
||||
this.channel = null;
|
||||
if (this.pageURL === ABOUT_NEW_TAB_URL) {
|
||||
AboutNewTab.reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* onNewTabLoad - Handler for special RemotePage:Load message fired by RemotePages
|
||||
*
|
||||
* @param {obj} msg The messsage from a page that was just loaded
|
||||
*/
|
||||
onNewTabLoad(msg) {
|
||||
this.onActionFromContent({type: at.NEW_TAB_LOAD}, msg.target.portID);
|
||||
}
|
||||
|
||||
/**
|
||||
* onNewTabUnloadLoad - Handler for special RemotePage:Unload message fired by RemotePages
|
||||
*
|
||||
* @param {obj} msg The messsage from a page that was just unloaded
|
||||
*/
|
||||
onNewTabUnload(msg) {
|
||||
this.onActionFromContent({type: at.NEW_TAB_UNLOAD}, msg.target.portID);
|
||||
}
|
||||
|
||||
/**
|
||||
* onMessage - Handles custom messages from content. It expects all messages to
|
||||
* be formatted as Redux actions, and dispatches them to this.store
|
||||
*
|
||||
* @param {obj} msg A custom message from content
|
||||
* @param {obj} msg.action A Redux action (e.g. {type: "HELLO_WORLD"})
|
||||
* @param {obj} msg.target A message target
|
||||
*/
|
||||
onMessage(msg) {
|
||||
const action = msg.data;
|
||||
const {portID} = msg.target;
|
||||
if (!action || !action.type) {
|
||||
Cu.reportError(new Error(`Received an improperly formatted message from ${portID}`));
|
||||
return;
|
||||
}
|
||||
this.onActionFromContent(action, msg.target.portID);
|
||||
}
|
||||
}
|
||||
|
||||
this.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
||||
this.EXPORTED_SYMBOLS = ["ActivityStreamMessageChannel", "DEFAULT_OPTIONS"];
|
|
@ -0,0 +1,25 @@
|
|||
/* 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 {utils: Cu} = Components;
|
||||
const {actionTypes: at, actionCreators: ac} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
|
||||
/**
|
||||
* NewTabInit - A placeholder for now. This will send a copy of the state to all
|
||||
* newly opened tabs.
|
||||
*/
|
||||
this.NewTabInit = class NewTabInit {
|
||||
onAction(action) {
|
||||
let newAction;
|
||||
switch (action.type) {
|
||||
case at.NEW_TAB_LOAD:
|
||||
newAction = {type: at.NEW_TAB_INITIAL_STATE, data: this.store.getState()};
|
||||
this.store.dispatch(ac.SendToContent(newAction, action.meta.fromTarget));
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["NewTabInit"];
|
|
@ -8,16 +8,19 @@ const {utils: Cu} = Components;
|
|||
const {redux} = Cu.import("resource://activity-stream/vendor/Redux.jsm", {});
|
||||
const {actionTypes: at} = Cu.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
const {reducers} = Cu.import("resource://activity-stream/common/Reducers.jsm", {});
|
||||
const {ActivityStreamMessageChannel} = Cu.import("resource://activity-stream/lib/ActivityStreamMessageChannel.jsm", {});
|
||||
|
||||
/**
|
||||
* Store - This has a similar structure to a redux store, but includes some extra
|
||||
* functionality. It accepts an array of "Feeds" on inititalization, which
|
||||
* functionality to allow for routing of actions between the Main processes
|
||||
* and child processes via a ActivityStreamMessageChannel.
|
||||
* It also accepts an array of "Feeds" on inititalization, which
|
||||
* can listen for any action that is dispatched through the store.
|
||||
*/
|
||||
this.Store = class Store {
|
||||
|
||||
/**
|
||||
* constructor - The redux store is created here,
|
||||
* constructor - The redux store and message manager are created here,
|
||||
* but no listeners are added until "init" is called.
|
||||
*/
|
||||
constructor() {
|
||||
|
@ -30,9 +33,10 @@ this.Store = class Store {
|
|||
}.bind(this);
|
||||
});
|
||||
this.feeds = new Set();
|
||||
this._messageChannel = new ActivityStreamMessageChannel({dispatch: this.dispatch});
|
||||
this._store = redux.createStore(
|
||||
redux.combineReducers(reducers),
|
||||
redux.applyMiddleware(this._middleware)
|
||||
redux.applyMiddleware(this._middleware, this._messageChannel.middleware)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -49,7 +53,7 @@ this.Store = class Store {
|
|||
}
|
||||
|
||||
/**
|
||||
* init - Initializes the MessageManager channel, and adds feeds.
|
||||
* init - Initializes the ActivityStreamMessageChannel channel, and adds feeds.
|
||||
* After initialization has finished, an INIT action is dispatched.
|
||||
*
|
||||
* @param {array} feeds An array of objects with an optional .onAction method
|
||||
|
@ -61,17 +65,20 @@ this.Store = class Store {
|
|||
this.feeds.add(subscriber);
|
||||
});
|
||||
}
|
||||
this._messageChannel.createChannel();
|
||||
this.dispatch({type: at.INIT});
|
||||
}
|
||||
|
||||
/**
|
||||
* uninit - Clears all feeds, dispatches an UNINIT action
|
||||
* uninit - Clears all feeds, dispatches an UNINIT action, and
|
||||
* destroys the message manager channel.
|
||||
*
|
||||
* @return {type} description
|
||||
*/
|
||||
uninit() {
|
||||
this.feeds.clear();
|
||||
this.dispatch({type: at.UNINIT});
|
||||
this._messageChannel.destroyChannel();
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ Cu.import("resource://gre/modules/AppConstants.jsm");
|
|||
|
||||
// The amount of people to be part of e10s
|
||||
const TEST_THRESHOLD = {
|
||||
"beta": 0.5, // 50%
|
||||
"beta": 0.9, // 90%
|
||||
"release": 1.0, // 100%
|
||||
"esr": 1.0, // 100%
|
||||
};
|
||||
|
@ -51,6 +51,10 @@ const PREF_TOGGLE_E10S = "browser.tabs.remote.autostart.2";
|
|||
const PREF_E10S_ADDON_POLICY = "extensions.e10s.rollout.policy";
|
||||
const PREF_E10S_ADDON_BLOCKLIST = "extensions.e10s.rollout.blocklist";
|
||||
const PREF_E10S_HAS_NONEXEMPT_ADDON = "extensions.e10s.rollout.hasAddon";
|
||||
const PREF_E10S_MULTI_OPTOUT = "dom.ipc.multiOptOut";
|
||||
const PREF_E10S_PROCESSCOUNT = "dom.ipc.processCount";
|
||||
const PREF_E10S_MULTI_ADDON_BLOCKS = "extensions.e10sMultiBlocksEnabling";
|
||||
const PREF_E10S_MULTI_BLOCKED_BY_ADDONS = "extensions.e10sMultiBlockedByAddons";
|
||||
|
||||
function startup() {
|
||||
// In theory we only need to run this once (on install()), but
|
||||
|
@ -104,7 +108,7 @@ function defineCohort() {
|
|||
let userOptedIn = optedIn();
|
||||
let disqualified = (Services.appinfo.multiprocessBlockPolicy != 0);
|
||||
let testThreshold = TEST_THRESHOLD[updateChannel];
|
||||
let testGroup = (getUserSample() < testThreshold);
|
||||
let testGroup = (getUserSample(false) < testThreshold);
|
||||
let hasNonExemptAddon = Preferences.get(PREF_E10S_HAS_NONEXEMPT_ADDON, false);
|
||||
let temporaryDisqualification = getTemporaryDisqualification();
|
||||
let temporaryQualification = getTemporaryQualification();
|
||||
|
@ -116,9 +120,12 @@ function defineCohort() {
|
|||
cohortPrefix = `addons-set${addonPolicy}-`;
|
||||
}
|
||||
|
||||
if (userOptedOut) {
|
||||
let inMultiExperiment = false;
|
||||
if (userOptedOut.e10s || userOptedOut.multi) {
|
||||
// If we detected that the user opted out either for multi or e10s, then
|
||||
// the proper prefs must already be set.
|
||||
setCohort("optedOut");
|
||||
} else if (userOptedIn) {
|
||||
} else if (userOptedIn.e10s) {
|
||||
setCohort("optedIn");
|
||||
} else if (temporaryDisqualification != "") {
|
||||
// Users who are disqualified by the backend (from multiprocessBlockPolicy)
|
||||
|
@ -131,6 +138,7 @@ function defineCohort() {
|
|||
// here will be accumulated as "2 - Disabled", which is fine too.
|
||||
setCohort(`temp-disqualified-${temporaryDisqualification}`);
|
||||
Preferences.reset(PREF_TOGGLE_E10S);
|
||||
Preferences.reset(PREF_E10S_PROCESSCOUNT + ".web");
|
||||
} else if (!disqualified && testThreshold < 1.0 &&
|
||||
temporaryQualification != "") {
|
||||
// Users who are qualified for e10s and on channels where some population
|
||||
|
@ -138,12 +146,49 @@ function defineCohort() {
|
|||
// qualification which overrides the user sample value when non-empty.
|
||||
setCohort(`temp-qualified-${temporaryQualification}`);
|
||||
Preferences.set(PREF_TOGGLE_E10S, true);
|
||||
inMultiExperiment = true;
|
||||
} else if (testGroup) {
|
||||
setCohort(`${cohortPrefix}test`);
|
||||
Preferences.set(PREF_TOGGLE_E10S, true);
|
||||
inMultiExperiment = true;
|
||||
} else {
|
||||
setCohort(`${cohortPrefix}control`);
|
||||
Preferences.reset(PREF_TOGGLE_E10S);
|
||||
Preferences.reset(PREF_E10S_PROCESSCOUNT + ".web");
|
||||
}
|
||||
|
||||
// Now determine if this user should be in the e10s-multi experiment.
|
||||
// - We only run the experiment on the beta channel.
|
||||
// - We decided above whether this user qualifies for the experiment.
|
||||
// - If the user already opted into multi, then their prefs are already set
|
||||
// correctly, we're done.
|
||||
// - If the user has addons that disqualify them for multi, leave them with
|
||||
// the default number of content processes (1 on beta) but still in the
|
||||
// test cohort.
|
||||
if (updateChannel !== "beta" ||
|
||||
!inMultiExperiment ||
|
||||
userOptedIn.multi ||
|
||||
getAddonsDisqualifyForMulti()) {
|
||||
Preferences.reset(PREF_E10S_PROCESSCOUNT + ".web");
|
||||
return;
|
||||
}
|
||||
|
||||
// The user is in the multi experiment!
|
||||
// Decide how many content processes to use for this user.
|
||||
let BUCKETS = {
|
||||
1: .25,
|
||||
2: .5,
|
||||
4: .75,
|
||||
8: 1
|
||||
};
|
||||
|
||||
let multiUserSample = getUserSample(true);
|
||||
for (let sampleName of Object.getOwnPropertyNames(BUCKETS)) {
|
||||
if (multiUserSample < BUCKETS[sampleName]) {
|
||||
setCohort(`${cohortPrefix}multiBucket${sampleName}`);
|
||||
Preferences.set(PREF_E10S_PROCESSCOUNT + ".web", sampleName);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,8 +198,9 @@ function shutdown(data, reason) {
|
|||
function uninstall() {
|
||||
}
|
||||
|
||||
function getUserSample() {
|
||||
let prefValue = Preferences.get(PREF_COHORT_SAMPLE, undefined);
|
||||
function getUserSample(multi) {
|
||||
let pref = multi ? (PREF_COHORT_SAMPLE + ".multi") : PREF_COHORT_SAMPLE;
|
||||
let prefValue = Preferences.get(pref, undefined);
|
||||
let value = 0.0;
|
||||
|
||||
if (typeof(prefValue) == "string") {
|
||||
|
@ -169,7 +215,7 @@ function getUserSample() {
|
|||
value = Math.random();
|
||||
}
|
||||
|
||||
Preferences.set(PREF_COHORT_SAMPLE, value.toString().substr(0, 8));
|
||||
Preferences.set(pref, value.toString().substr(0, 8));
|
||||
return value;
|
||||
}
|
||||
|
||||
|
@ -183,17 +229,22 @@ function setCohort(cohortName) {
|
|||
}
|
||||
|
||||
function optedIn() {
|
||||
return Preferences.get(PREF_E10S_OPTED_IN, false) ||
|
||||
Preferences.get(PREF_E10S_FORCE_ENABLED, false);
|
||||
let e10s = Preferences.get(PREF_E10S_OPTED_IN, false) ||
|
||||
Preferences.get(PREF_E10S_FORCE_ENABLED, false);
|
||||
let multi = Preferences.isSet(PREF_E10S_PROCESSCOUNT);
|
||||
return { e10s, multi };
|
||||
}
|
||||
|
||||
function optedOut() {
|
||||
// Users can also opt-out by toggling back the pref to false.
|
||||
// If they reset the pref instead they might be re-enabled if
|
||||
// they are still part of the threshold.
|
||||
return Preferences.get(PREF_E10S_FORCE_DISABLED, false) ||
|
||||
(Preferences.isSet(PREF_TOGGLE_E10S) &&
|
||||
Preferences.get(PREF_TOGGLE_E10S) == false);
|
||||
let e10s = Preferences.get(PREF_E10S_FORCE_DISABLED, false) ||
|
||||
(Preferences.isSet(PREF_TOGGLE_E10S) &&
|
||||
Preferences.get(PREF_TOGGLE_E10S) == false);
|
||||
let multi = Preferences.get(PREF_E10S_MULTI_OPTOUT, 0) >=
|
||||
Services.appinfo.E10S_MULTI_EXPERIMENT;
|
||||
return { e10s, multi };
|
||||
}
|
||||
|
||||
/* If this function returns a non-empty string, it
|
||||
|
@ -225,3 +276,8 @@ function getTemporaryQualification() {
|
|||
|
||||
return "";
|
||||
}
|
||||
|
||||
function getAddonsDisqualifyForMulti() {
|
||||
return Preferences.get("extensions.e10sMultiBlocksEnabling", false) &&
|
||||
Preferences.get("extensions.e10sMultiBlockedByAddons", false);
|
||||
}
|
||||
|
|
|
@ -330,6 +330,7 @@
|
|||
@RESPATH@/components/urlformatter.xpt
|
||||
@RESPATH@/components/webBrowser_core.xpt
|
||||
@RESPATH@/components/webbrowserpersist.xpt
|
||||
@RESPATH@/components/webextensions.xpt
|
||||
@RESPATH@/components/widget.xpt
|
||||
#ifdef XP_MACOSX
|
||||
@RESPATH@/components/widget_cocoa.xpt
|
||||
|
|
|
@ -59,19 +59,21 @@
|
|||
# These defines must match HANDLER_CLSID defined in
|
||||
# accessible/ipc/win/handler/HandlerData.idl
|
||||
|
||||
#ifndef MOZ_OFFICIAL_BRANDING
|
||||
!if "@MOZ_UPDATE_CHANNEL@" == "default"
|
||||
#ifdef DEBUG
|
||||
!define AccessibleHandlerCLSID "{398FFD8D-5382-48F7-9E3B-19012762D39A}"
|
||||
#else
|
||||
!define AccessibleHandlerCLSID "{CE573FAF-7815-4FC2-A031-B092268ACE9E}"
|
||||
#endif
|
||||
#elifdef NIGHTLY_BUILD
|
||||
!else if "@MOZ_UPDATE_CHANNEL@" == "nightly"
|
||||
!define AccessibleHandlerCLSID "{4629216B-8753-41BF-9527-5BFF51401671}"
|
||||
#elifdef RELEASE_OR_BETA
|
||||
!else if "@MOZ_UPDATE_CHANNEL@" == "beta"
|
||||
!define AccessibleHandlerCLSID "{21E9F98D-A6C9-4CB5-B288-AE2FD2A96C58}"
|
||||
!else if "@MOZ_UPDATE_CHANNEL@" == "release"
|
||||
!define AccessibleHandlerCLSID "{1BAA303D-B4B9-45E5-9CCB-E3FCA3E274B6}"
|
||||
#else
|
||||
!else
|
||||
!define AccessibleHandlerCLSID "{4A195748-DCA2-45FB-9295-0A139E76A9E7}"
|
||||
#endif
|
||||
!endif
|
||||
|
||||
# Due to official and beta using the same branding this is needed to
|
||||
# differentiante between the url used by the stub for downloading.
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
; Adds a pinned Task Bar shortcut (see MigrateTaskBarShortcut for details).
|
||||
${MigrateTaskBarShortcut}
|
||||
|
||||
${UpdateShortcutNames}
|
||||
${UpdateShortcutBranding}
|
||||
|
||||
${RemoveDeprecatedKeys}
|
||||
|
||||
|
@ -315,7 +315,7 @@
|
|||
; in case the branding has changed between updates.
|
||||
; This should only be called sometime after both MigrateStartMenuShortcut
|
||||
; and MigrateTaskBarShurtcut
|
||||
!macro UpdateShortcutNames
|
||||
!macro UpdateShortcutBranding
|
||||
${GetLongPath} "$INSTDIR\uninstall\${SHORTCUTS_LOG}" $R9
|
||||
${If} ${FileExists} "$R9"
|
||||
ClearErrors
|
||||
|
@ -324,13 +324,35 @@
|
|||
ReadINIStr $R8 "$R9" "STARTMENU" "Shortcut0"
|
||||
${IfNot} ${Errors}
|
||||
${If} ${FileExists} "$SMPROGRAMS\$R8"
|
||||
${AndIf} $R8 != "${BrandFullName}.lnk"
|
||||
ShellLink::GetShortCutTarget "$SMPROGRAMS\$R8"
|
||||
Pop $R7
|
||||
${GetLongPath} "$R7" $R7
|
||||
${If} "$INSTDIR\${FileMainEXE}" == "$R7"
|
||||
Rename "$SMPROGRAMS\$R8" "$SMPROGRAMS\${BrandFullName}.lnk"
|
||||
WriteINIStr "$R9" "STARTMENU" "Shortcut0" "${BrandFullName}.lnk"
|
||||
${If} $R7 == "$INSTDIR\${FileMainEXE}"
|
||||
ShellLink::GetShortCutIconLocation "$SMPROGRAMS\$R8"
|
||||
Pop $R6
|
||||
${GetLongPath} "$R6" $R6
|
||||
${If} $R6 != "$INSTDIR\firefox.ico"
|
||||
${AndIf} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
StrCpy $R5 "1"
|
||||
${ElseIf} $R6 == "$INSTDIR\firefox.ico"
|
||||
${AndIfNot} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
StrCpy $R5 "1"
|
||||
${Else}
|
||||
StrCpy $R5 "0"
|
||||
${EndIf}
|
||||
|
||||
${If} $R5 == "1"
|
||||
${OrIf} $R8 != "${BrandFullName}.lnk"
|
||||
Delete "$SMPROGRAMS\$R8"
|
||||
${If} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
CreateShortcut "$SMPROGRAMS\${BrandFullName}.lnk" \
|
||||
"$INSTDIR\${FileMainEXE}" "" "$INSTDIR\firefox.ico"
|
||||
${Else}
|
||||
CreateShortcut "$SMPROGRAMS\${BrandFullName}.lnk" \
|
||||
"$INSTDIR\${FileMainEXE}"
|
||||
${EndIf}
|
||||
WriteINIStr "$R9" "STARTMENU" "Shortcut0" "${BrandFullName}.lnk"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
@ -339,13 +361,35 @@
|
|||
ReadINIStr $R8 "$R9" "DESKTOP" "Shortcut0"
|
||||
${IfNot} ${Errors}
|
||||
${If} ${FileExists} "$DESKTOP\$R8"
|
||||
${AndIf} $R8 != "${BrandFullName}.lnk"
|
||||
ShellLink::GetShortCutTarget "$DESKTOP\$R8"
|
||||
Pop $R7
|
||||
${GetLongPath} "$R7" $R7
|
||||
${If} "$INSTDIR\${FileMainEXE}" == "$R7"
|
||||
Rename "$DESKTOP\$R8" "$DESKTOP\${BrandFullName}.lnk"
|
||||
WriteINIStr "$R9" "DESKTOP" "Shortcut0" "${BrandFullName}.lnk"
|
||||
${If} $R7 == "$INSTDIR\${FileMainEXE}"
|
||||
ShellLink::GetShortCutIconLocation "$DESKTOP\$R8"
|
||||
Pop $R6
|
||||
${GetLongPath} "$R6" $R6
|
||||
${If} $R6 != "$INSTDIR\firefox.ico"
|
||||
${AndIf} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
StrCpy $R5 "1"
|
||||
${ElseIf} $R6 == "$INSTDIR\firefox.ico"
|
||||
${AndIfNot} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
StrCpy $R5 "1"
|
||||
${Else}
|
||||
StrCpy $R5 "0"
|
||||
${EndIf}
|
||||
|
||||
${If} $R5 == "1"
|
||||
${OrIf} $R8 != "${BrandFullName}.lnk"
|
||||
Delete "$DESKTOP\$R8"
|
||||
${If} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
CreateShortcut "$DESKTOP\${BrandFullName}.lnk" \
|
||||
"$INSTDIR\${FileMainEXE}" "" "$INSTDIR\firefox.ico"
|
||||
${Else}
|
||||
CreateShortcut "$DESKTOP\${BrandFullName}.lnk" \
|
||||
"$INSTDIR\${FileMainEXE}"
|
||||
${EndIf}
|
||||
WriteINIStr "$R9" "DESKTOP" "Shortcut0" "${BrandFullName}.lnk"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
@ -354,21 +398,41 @@
|
|||
ReadINIStr $R8 "$R9" "QUICKLAUNCH" "Shortcut0"
|
||||
${IfNot} ${Errors}
|
||||
; "QUICKLAUNCH" actually means a taskbar pin.
|
||||
; We can't simultaneously rename and change the icon for a taskbar pin
|
||||
; without the icon breaking, and the icon is more important than the name,
|
||||
; so we'll forget about changing the name and just overwrite the icon.
|
||||
${If} ${FileExists} "$QUICKLAUNCH\User Pinned\TaskBar\$R8"
|
||||
${AndIf} $R8 != "${BrandFullName}.lnk"
|
||||
ShellLink::GetShortCutTarget "$QUICKLAUNCH\User Pinned\TaskBar\$R8"
|
||||
Pop $R7
|
||||
${GetLongPath} "$R7" $R7
|
||||
${If} "$INSTDIR\${FileMainEXE}" == "$R7"
|
||||
Rename "$QUICKLAUNCH\User Pinned\TaskBar\$R8" \
|
||||
"$QUICKLAUNCH\User Pinned\TaskBar\${BrandFullName}.lnk"
|
||||
WriteINIStr "$R9" "QUICKLAUNCH" "Shortcut0" "${BrandFullName}.lnk"
|
||||
ShellLink::GetShortCutIconLocation "$QUICKLAUNCH\User Pinned\TaskBar\$R8"
|
||||
Pop $R6
|
||||
${GetLongPath} "$R6" $R6
|
||||
${If} $R6 != "$INSTDIR\firefox.ico"
|
||||
${AndIf} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
StrCpy $R5 "1"
|
||||
${ElseIf} $R6 == "$INSTDIR\firefox.ico"
|
||||
${AndIfNot} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
StrCpy $R5 "1"
|
||||
${Else}
|
||||
StrCpy $R5 "0"
|
||||
${EndIf}
|
||||
|
||||
${If} $R5 == "1"
|
||||
${If} ${FileExists} "$INSTDIR\firefox.ico"
|
||||
CreateShortcut "$QUICKLAUNCH\User Pinned\TaskBar\$R8" "$R7" "" \
|
||||
"$INSTDIR\firefox.ico"
|
||||
${Else}
|
||||
CreateShortcut "$QUICKLAUNCH\User Pinned\TaskBar\$R8" "$R7"
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
!macroend
|
||||
!define UpdateShortcutNames "!insertmacro UpdateShortcutNames"
|
||||
!define UpdateShortcutBranding "!insertmacro UpdateShortcutBranding"
|
||||
|
||||
!macro AddAssociationIfNoneExist FILE_TYPE KEY
|
||||
ClearErrors
|
||||
|
|
|
@ -14,6 +14,7 @@ function mockSyncedTabs() {
|
|||
id: "guid_desktop",
|
||||
type: "client",
|
||||
name: "My Desktop",
|
||||
lastModified: 1492201200,
|
||||
tabs: [
|
||||
{
|
||||
title: "http://example.com/10",
|
||||
|
|
|
@ -183,6 +183,7 @@ menuitem[cmd="cmd_clearhistory"][disabled] {
|
|||
.search-panel-one-offs {
|
||||
margin: 0 !important;
|
||||
border-top: 1px solid var(--panel-separator-color);
|
||||
background-color: var(--arrowpanel-dimmed);
|
||||
}
|
||||
|
||||
.searchbar-engine-one-off-item {
|
||||
|
@ -222,7 +223,6 @@ menuitem[cmd="cmd_clearhistory"][disabled] {
|
|||
}
|
||||
|
||||
.searchbar-engine-one-off-item:not([selected]):not(.dummy):hover,
|
||||
.search-setting-button:hover,
|
||||
.addengine-item:hover {
|
||||
background-color: hsla(0, 0%, 0%, 0.06);
|
||||
color: inherit;
|
||||
|
@ -249,6 +249,10 @@ menuitem[cmd="cmd_clearhistory"][disabled] {
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
.search-add-engines {
|
||||
background-color: var(--arrowpanel-dimmed);
|
||||
}
|
||||
|
||||
.addengine-item {
|
||||
-moz-appearance: none;
|
||||
background-color: transparent;
|
||||
|
@ -343,6 +347,7 @@ menuitem[cmd="cmd_clearhistory"][disabled] {
|
|||
min-height: 32px;
|
||||
}
|
||||
|
||||
.search-setting-button:hover,
|
||||
.search-setting-button[selected] {
|
||||
background-color: var(--arrowpanel-dimmed-further);
|
||||
}
|
||||
|
|
|
@ -173,6 +173,7 @@
|
|||
.search-panel-one-offs {
|
||||
margin: 0 !important;
|
||||
border-top: 1px solid var(--panel-separator-color);
|
||||
background-color: var(--arrowpanel-dimmed);
|
||||
}
|
||||
|
||||
.searchbar-engine-one-off-item {
|
||||
|
@ -211,7 +212,6 @@
|
|||
}
|
||||
|
||||
.searchbar-engine-one-off-item:not([selected]):not(.dummy):hover,
|
||||
.search-setting-button:hover,
|
||||
.addengine-item:hover {
|
||||
background-color: hsla(0, 0%, 0%, 0.06);
|
||||
color: inherit;
|
||||
|
@ -233,6 +233,10 @@
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
.search-add-engines {
|
||||
background-color: var(--arrowpanel-dimmed);
|
||||
}
|
||||
|
||||
.addengine-item {
|
||||
-moz-appearance: none;
|
||||
font-size: 10px;
|
||||
|
@ -326,6 +330,7 @@
|
|||
min-height: 32px;
|
||||
}
|
||||
|
||||
.search-setting-button:hover,
|
||||
.search-setting-button[selected] {
|
||||
background-color: var(--arrowpanel-dimmed-further);
|
||||
}
|
||||
|
|
|
@ -49,6 +49,10 @@ treecol {
|
|||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.accessory-button {
|
||||
min-width: 145px;
|
||||
}
|
||||
|
||||
/* Category List */
|
||||
|
||||
#categories {
|
||||
|
|
|
@ -180,6 +180,7 @@
|
|||
margin: 0 !important;
|
||||
border-top: 1px solid var(--panel-separator-color);
|
||||
line-height: 0;
|
||||
background-color: var(--arrowpanel-dimmed);
|
||||
}
|
||||
|
||||
.searchbar-engine-one-off-item {
|
||||
|
@ -223,7 +224,6 @@
|
|||
}
|
||||
|
||||
.searchbar-engine-one-off-item:not([selected]):not(.dummy):hover,
|
||||
.search-setting-button:hover,
|
||||
.addengine-item:hover {
|
||||
background-color: hsla(0, 0%, 0%, 0.06);
|
||||
color: inherit;
|
||||
|
@ -248,6 +248,10 @@
|
|||
height: 16px;
|
||||
}
|
||||
|
||||
.search-add-engines {
|
||||
background-color: var(--arrowpanel-dimmed);
|
||||
}
|
||||
|
||||
.addengine-item {
|
||||
-moz-appearance: none;
|
||||
border: none;
|
||||
|
@ -339,6 +343,7 @@
|
|||
min-height: 32px;
|
||||
}
|
||||
|
||||
.search-setting-button:hover,
|
||||
.search-setting-button[selected] {
|
||||
background-color: var(--arrowpanel-dimmed-further);
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ if [ -d "$topsrcdir/clang" ]; then
|
|||
export DSYMUTIL=$topsrcdir/clang/bin/llvm-dsymutil
|
||||
# Use an updated linker.
|
||||
ldflags="-B$topsrcdir/cctools/bin"
|
||||
export AR=$topsrcdir/cctools/bin/ar
|
||||
export RANLIB=$topsrcdir/cctools/bin/ranlib
|
||||
elif [ -d "$topsrcdir/../clang" ]; then
|
||||
# comm-central based build
|
||||
export CC=$topsrcdir/../clang/bin/clang
|
||||
|
@ -25,6 +27,8 @@ elif [ -d "$topsrcdir/../clang" ]; then
|
|||
export DSYMUTIL=$topsrcdir/../clang/bin/llvm-dsymutil
|
||||
# Use an updated linker.
|
||||
ldflags="-B$topsrcdir/../cctools/bin"
|
||||
export AR=$topsrcdir/../cctools/bin/ar
|
||||
export RANLIB=$topsrcdir/../cctools/bin/ranlib
|
||||
fi
|
||||
|
||||
# Ensure the updated linker doesn't generate things our older build tools
|
||||
|
|
|
@ -292,13 +292,11 @@ HOST_CFLAGS += $(_DEPEND_CFLAGS)
|
|||
HOST_CXXFLAGS += $(_DEPEND_CFLAGS)
|
||||
ifdef CROSS_COMPILE
|
||||
HOST_CFLAGS += $(HOST_OPTIMIZE_FLAGS)
|
||||
HOST_CXXFLAGS += $(HOST_OPTIMIZE_FLAGS)
|
||||
else
|
||||
ifdef MOZ_OPTIMIZE
|
||||
ifeq (1,$(MOZ_OPTIMIZE))
|
||||
HOST_CFLAGS += $(MOZ_OPTIMIZE_FLAGS)
|
||||
else
|
||||
HOST_CFLAGS += $(MOZ_OPTIMIZE_FLAGS)
|
||||
endif # MOZ_OPTIMIZE == 1
|
||||
HOST_CXXFLAGS += $(MOZ_OPTIMIZE_FLAGS)
|
||||
endif # MOZ_OPTIMIZE
|
||||
endif # CROSS_COMPILE
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ loader.lazyRequireGetter(this, "DebuggerClient",
|
|||
"devtools/shared/client/main", true);
|
||||
|
||||
const Strings = Services.strings.createBundle("chrome://devtools/locale/aboutdebugging.properties");
|
||||
const PROCESS_COUNT_PREF = "dom.ipc.processCount";
|
||||
const MULTI_OPT_OUT_PREF = "dom.ipc.multiOptOut";
|
||||
|
||||
module.exports = createClass({
|
||||
displayName: "multiE10SWarning",
|
||||
|
@ -28,7 +28,9 @@ module.exports = createClass({
|
|||
onUpdatePreferenceClick() {
|
||||
let message = Strings.GetStringFromName("multiProcessWarningConfirmUpdate");
|
||||
if (window.confirm(message)) {
|
||||
Services.prefs.setIntPref(PROCESS_COUNT_PREF, 1);
|
||||
// Disable multi until at least the next experiment.
|
||||
Services.prefs.setIntPref(MULTI_OPT_OUT_PREF,
|
||||
Services.appinfo.E10S_MULTI_EXPERIMENT);
|
||||
// Restart the browser.
|
||||
Services.startup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
|
||||
}
|
||||
|
|
|
@ -75,7 +75,9 @@ module.exports = createClass({
|
|||
},
|
||||
|
||||
updateMultiE10S() {
|
||||
let processCount = Services.prefs.getIntPref(PROCESS_COUNT_PREF);
|
||||
// We watch the pref but set the state based on
|
||||
// nsIXULRuntime::maxWebProcessCount.
|
||||
let processCount = Services.appinfo.maxWebProcessCount;
|
||||
this.setState({ processCount });
|
||||
},
|
||||
|
||||
|
|
|
@ -100,6 +100,8 @@ var getServerTraits = Task.async(function* (target) {
|
|||
method: "getProperties" },
|
||||
{ name: "hasSetWalkerActor", actor: "animations",
|
||||
method: "setWalkerActor" },
|
||||
{ name: "hasGetAnimationTypes", actor: "animationplayer",
|
||||
method: "getAnimationTypes" },
|
||||
];
|
||||
|
||||
let traits = {};
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
|
||||
<link rel="stylesheet" href="chrome://devtools/skin/animationinspector.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="resource://devtools/client/shared/components/splitter/split-box.css"/>
|
||||
<script type="application/javascript" src="chrome://devtools/content/shared/theme-switching.js"/>
|
||||
</head>
|
||||
<body class="theme-sidebar devtools-monospace" role="application" empty="true">
|
||||
|
@ -26,6 +27,16 @@
|
|||
<p id="error-hint"></p>
|
||||
<button id="element-picker" data-standalone="true" class="devtools-button"></button>
|
||||
</div>
|
||||
<script type="text/javascript">
|
||||
/* eslint-disable */
|
||||
var isInChrome = window.location.href.includes("chrome:");
|
||||
if (isInChrome) {
|
||||
var exports = {};
|
||||
var Cu = Components.utils;
|
||||
var { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
var { BrowserLoader } = Cu.import("resource://devtools/client/shared/browser-loader.js", {});
|
||||
}
|
||||
</script>
|
||||
<script type="application/javascript" src="animation-controller.js"></script>
|
||||
<script type="application/javascript" src="animation-panel.js"></script>
|
||||
</body>
|
||||
|
|
|
@ -8,9 +8,14 @@
|
|||
|
||||
const {Task} = require("devtools/shared/task");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
|
||||
const {createNode, getCssPropertyName} =
|
||||
require("devtools/client/animationinspector/utils");
|
||||
const {Keyframes} = require("devtools/client/animationinspector/components/keyframes");
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying detailed information for a given
|
||||
* animation.
|
||||
|
@ -22,8 +27,6 @@ const {Keyframes} = require("devtools/client/animationinspector/components/keyfr
|
|||
function AnimationDetails(serverTraits) {
|
||||
EventEmitter.decorate(this);
|
||||
|
||||
this.onFrameSelected = this.onFrameSelected.bind(this);
|
||||
|
||||
this.keyframeComponents = [];
|
||||
this.serverTraits = serverTraits;
|
||||
}
|
||||
|
@ -33,7 +36,8 @@ exports.AnimationDetails = AnimationDetails;
|
|||
AnimationDetails.prototype = {
|
||||
// These are part of frame objects but are not animated properties. This
|
||||
// array is used to skip them.
|
||||
NON_PROPERTIES: ["easing", "composite", "computedOffset", "offset"],
|
||||
NON_PROPERTIES: ["easing", "composite", "computedOffset",
|
||||
"offset", "simulateComputeValuesFailure"],
|
||||
|
||||
init: function (containerEl) {
|
||||
this.containerEl = containerEl;
|
||||
|
@ -43,11 +47,11 @@ AnimationDetails.prototype = {
|
|||
this.unrender();
|
||||
this.containerEl = null;
|
||||
this.serverTraits = null;
|
||||
this.progressIndicatorEl = null;
|
||||
},
|
||||
|
||||
unrender: function () {
|
||||
for (let component of this.keyframeComponents) {
|
||||
component.off("frame-selected", this.onFrameSelected);
|
||||
component.destroy();
|
||||
}
|
||||
this.keyframeComponents = [];
|
||||
|
@ -108,8 +112,9 @@ AnimationDetails.prototype = {
|
|||
tracks[name] = [];
|
||||
}
|
||||
|
||||
for (let {value, offset} of values) {
|
||||
tracks[name].push({value, offset});
|
||||
for (let {value, offset, easing, distance} of values) {
|
||||
distance = distance ? distance : 0;
|
||||
tracks[name].push({value, offset, easing, distance});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -120,13 +125,18 @@ AnimationDetails.prototype = {
|
|||
continue;
|
||||
}
|
||||
|
||||
if (!tracks[name]) {
|
||||
tracks[name] = [];
|
||||
// We have to change to CSS property name
|
||||
// since GetKeyframes returns JS property name.
|
||||
const propertyCSSName = getCssPropertyName(name);
|
||||
if (!tracks[propertyCSSName]) {
|
||||
tracks[propertyCSSName] = [];
|
||||
}
|
||||
|
||||
tracks[name].push({
|
||||
tracks[propertyCSSName].push({
|
||||
value: frame[name],
|
||||
offset: frame.computedOffset
|
||||
offset: frame.computedOffset,
|
||||
easing: frame.easing,
|
||||
distance: 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -135,6 +145,26 @@ AnimationDetails.prototype = {
|
|||
return tracks;
|
||||
}),
|
||||
|
||||
/**
|
||||
* Get animation types of given CSS property names.
|
||||
* @param {Array} CSS property names.
|
||||
* e.g. ["background-color", "opacity", ...]
|
||||
* @return {Object} Animation type mapped with CSS property name.
|
||||
* e.g. { "background-color": "color", }
|
||||
* "opacity": "float", ... }
|
||||
*/
|
||||
getAnimationTypes: Task.async(function* (propertyNames) {
|
||||
if (this.serverTraits.hasGetAnimationTypes) {
|
||||
return yield this.animation.getAnimationTypes(propertyNames);
|
||||
}
|
||||
// Set animation type 'none' since does not support getAnimationTypes.
|
||||
const animationTypes = {};
|
||||
propertyNames.forEach(propertyName => {
|
||||
animationTypes[propertyName] = "none";
|
||||
});
|
||||
return Promise.resolve(animationTypes);
|
||||
}),
|
||||
|
||||
render: Task.async(function* (animation) {
|
||||
this.unrender();
|
||||
|
||||
|
@ -152,16 +182,95 @@ AnimationDetails.prototype = {
|
|||
// Build an element for each animated property track.
|
||||
this.tracks = yield this.getTracks(animation, this.serverTraits);
|
||||
|
||||
// Useful for tests to know when the keyframes have been retrieved.
|
||||
this.emit("keyframes-retrieved");
|
||||
// Get animation type for each CSS properties.
|
||||
const animationTypes = yield this.getAnimationTypes(Object.keys(this.tracks));
|
||||
|
||||
// Render progress indicator.
|
||||
this.renderProgressIndicator();
|
||||
// Render animated properties header.
|
||||
this.renderAnimatedPropertiesHeader();
|
||||
// Render animated properties body.
|
||||
this.renderAnimatedPropertiesBody(animationTypes);
|
||||
|
||||
// Create dummy animation to indicate the animation progress.
|
||||
const timing = Object.assign({}, animation.state, {
|
||||
iterations: animation.state.iterationCount
|
||||
? animation.state.iterationCount : Infinity
|
||||
});
|
||||
this.dummyAnimation =
|
||||
new this.win.Animation(new this.win.KeyframeEffect(null, null, timing), null);
|
||||
|
||||
// Useful for tests to know when rendering of all animation detail UIs
|
||||
// have been completed.
|
||||
this.emit("animation-detail-rendering-completed");
|
||||
}),
|
||||
|
||||
renderAnimatedPropertiesHeader: function () {
|
||||
// Add animated property header.
|
||||
const headerEl = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: { "class": "animated-properties-header" }
|
||||
});
|
||||
|
||||
// Add progress tick container.
|
||||
const progressTickContainerEl = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: { "class": "progress-tick-container track-container" }
|
||||
});
|
||||
|
||||
// Add label container.
|
||||
const headerLabelContainerEl = createNode({
|
||||
parent: headerEl,
|
||||
attributes: { "class": "track-container" }
|
||||
});
|
||||
|
||||
// Add labels
|
||||
for (let label of [L10N.getFormatStr("detail.propertiesHeader.percentage", 0),
|
||||
L10N.getFormatStr("detail.propertiesHeader.percentage", 50),
|
||||
L10N.getFormatStr("detail.propertiesHeader.percentage", 100)]) {
|
||||
createNode({
|
||||
parent: progressTickContainerEl,
|
||||
nodeType: "span",
|
||||
attributes: { "class": "progress-tick" }
|
||||
});
|
||||
createNode({
|
||||
parent: headerLabelContainerEl,
|
||||
nodeType: "label",
|
||||
attributes: { "class": "header-item" },
|
||||
textContent: label
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderAnimatedPropertiesBody: function (animationTypes) {
|
||||
// Add animated property body.
|
||||
const bodyEl = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: { "class": "animated-properties-body" }
|
||||
});
|
||||
|
||||
// Move unchanged value animation to bottom in the list.
|
||||
const propertyNames = [];
|
||||
const unchangedPropertyNames = [];
|
||||
for (let propertyName in this.tracks) {
|
||||
if (!isUnchangedProperty(this.tracks[propertyName])) {
|
||||
propertyNames.push(propertyName);
|
||||
} else {
|
||||
unchangedPropertyNames.push(propertyName);
|
||||
}
|
||||
}
|
||||
Array.prototype.push.apply(propertyNames, unchangedPropertyNames);
|
||||
|
||||
for (let propertyName of propertyNames) {
|
||||
let line = createNode({
|
||||
parent: this.containerEl,
|
||||
parent: bodyEl,
|
||||
attributes: {"class": "property"}
|
||||
});
|
||||
if (unchangedPropertyNames.includes(propertyName)) {
|
||||
line.classList.add("unchanged");
|
||||
}
|
||||
let {warning, className} =
|
||||
this.getPerfDataForProperty(animation, propertyName);
|
||||
this.getPerfDataForProperty(this.animation, propertyName);
|
||||
createNode({
|
||||
// text-overflow doesn't work in flex items, so we need a second level
|
||||
// of container to actually have an ellipsis on the name.
|
||||
|
@ -186,37 +295,63 @@ AnimationDetails.prototype = {
|
|||
attributes: {"class": "frames"}
|
||||
});
|
||||
|
||||
// Scale the list of keyframes according to the current time scale.
|
||||
let {x, w} = TimeScale.getAnimationDimensions(animation);
|
||||
framesEl.style.left = `${x}%`;
|
||||
framesEl.style.width = `${w}%`;
|
||||
|
||||
let keyframesComponent = new Keyframes();
|
||||
keyframesComponent.init(framesEl);
|
||||
keyframesComponent.render({
|
||||
keyframes: this.tracks[propertyName],
|
||||
propertyName: propertyName,
|
||||
animation: animation
|
||||
animation: this.animation,
|
||||
animationType: animationTypes[propertyName]
|
||||
});
|
||||
keyframesComponent.on("frame-selected", this.onFrameSelected);
|
||||
|
||||
this.keyframeComponents.push(keyframesComponent);
|
||||
}
|
||||
}),
|
||||
},
|
||||
|
||||
onFrameSelected: function (e, args) {
|
||||
// Relay the event up, it's needed in parents too.
|
||||
this.emit(e, args);
|
||||
renderProgressIndicator: function () {
|
||||
// The wrapper represents the area which the indicator is displayable.
|
||||
const progressIndicatorWrapperEl = createNode({
|
||||
parent: this.containerEl,
|
||||
attributes: {
|
||||
"class": "track-container progress-indicator-wrapper"
|
||||
}
|
||||
});
|
||||
this.progressIndicatorEl = createNode({
|
||||
parent: progressIndicatorWrapperEl,
|
||||
attributes: {
|
||||
"class": "progress-indicator"
|
||||
}
|
||||
});
|
||||
createNode({
|
||||
parent: this.progressIndicatorEl,
|
||||
attributes: {
|
||||
"class": "progress-indicator-shape"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
indicateProgress: function (time) {
|
||||
if (!this.progressIndicatorEl) {
|
||||
// Not displayed yet.
|
||||
return;
|
||||
}
|
||||
const startTime = this.animation.state.previousStartTime || 0;
|
||||
this.dummyAnimation.currentTime =
|
||||
(time - startTime) * this.animation.state.playbackRate;
|
||||
this.progressIndicatorEl.style.left =
|
||||
`${ this.dummyAnimation.effect.getComputedTiming().progress * 100 }%`;
|
||||
},
|
||||
|
||||
get win() {
|
||||
return this.containerEl.ownerDocument.defaultView;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Turn propertyName into property-name.
|
||||
* @param {String} jsPropertyName A camelcased CSS property name. Typically
|
||||
* something that comes out of computed styles. E.g. borderBottomColor
|
||||
* @return {String} The corresponding CSS property name: border-bottom-color
|
||||
*/
|
||||
function getCssPropertyName(jsPropertyName) {
|
||||
return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
|
||||
function isUnchangedProperty(values) {
|
||||
const firstValue = values[0].value;
|
||||
for (let i = 1; i < values.length; i++) {
|
||||
if (values[i].value !== firstValue) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
exports.getCssPropertyName = getCssPropertyName;
|
||||
|
|
|
@ -7,31 +7,18 @@
|
|||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {createNode, TimeScale} = require("devtools/client/animationinspector/utils");
|
||||
const {createNode, createSVGNode, TimeScale, getFormattedAnimationTitle} =
|
||||
require("devtools/client/animationinspector/utils");
|
||||
const {createPathSegments, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
|
||||
require("devtools/client/animationinspector/graph-helper");
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// In the createPathSegments function, an animation duration is divided by
|
||||
// DURATION_RESOLUTION in order to draw the way the animation progresses.
|
||||
// But depending on the timing-function, we may be not able to make the graph
|
||||
// smoothly progress if this resolution is not high enough.
|
||||
// So, if the difference of animation progress between 2 divisions is more than
|
||||
// MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
|
||||
// by DURATION_RESOLUTION.
|
||||
// DURATION_RESOLUTION shoud be integer and more than 2.
|
||||
const DURATION_RESOLUTION = 4;
|
||||
// MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
|
||||
const MIN_PROGRESS_THRESHOLD = 0.1;
|
||||
// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
|
||||
// and end bounds when dividing duration in createPathSegments.
|
||||
const BOUND_EXCLUDING_TIME = 0.001;
|
||||
// Show max 10 iterations for infinite animations
|
||||
// to give users a clue that the animation does repeat.
|
||||
const MAX_INFINITE_ANIMATIONS_ITERATIONS = 10;
|
||||
// SVG namespace
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a single animation timeline, which
|
||||
|
@ -76,9 +63,8 @@ AnimationTimeBlock.prototype = {
|
|||
TimeScale.getAnimationDimensions(animation);
|
||||
|
||||
// Animation summary graph element.
|
||||
const summaryEl = createNode({
|
||||
const summaryEl = createSVGNode({
|
||||
parent: this.containerEl,
|
||||
namespace: "http://www.w3.org/2000/svg",
|
||||
nodeType: "svg",
|
||||
attributes: {
|
||||
"class": "summary",
|
||||
|
@ -107,7 +93,7 @@ AnimationTimeBlock.prototype = {
|
|||
const minSegmentDuration =
|
||||
totalDisplayedDuration / this.containerEl.clientWidth;
|
||||
// Minimum progress threshold.
|
||||
let minProgressThreshold = MIN_PROGRESS_THRESHOLD;
|
||||
let minProgressThreshold = DEFAULT_MIN_PROGRESS_THRESHOLD;
|
||||
// If the easing is step function,
|
||||
// minProgressThreshold should be changed by the steps.
|
||||
const stepFunction = state.easing.match(/steps\((\d+)/);
|
||||
|
@ -354,30 +340,6 @@ AnimationTimeBlock.prototype = {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a formatted title for this animation. This will be either:
|
||||
* "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
|
||||
* "some-name : Script Animation", or "Script Animation", depending
|
||||
* if the server provides the type, what type it is and if the animation
|
||||
* has a name
|
||||
* @param {AnimationPlayerFront} animation
|
||||
*/
|
||||
function getFormattedAnimationTitle({state}) {
|
||||
// Older servers don't send a type, and only know about
|
||||
// CSSAnimations and CSSTransitions, so it's safe to use
|
||||
// just the name.
|
||||
if (!state.type) {
|
||||
return state.name;
|
||||
}
|
||||
|
||||
// Script-generated animations may not have a name.
|
||||
if (state.type === "scriptanimation" && !state.name) {
|
||||
return L10N.getStr("timeline.scriptanimation.unnamedLabel");
|
||||
}
|
||||
|
||||
return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render delay section.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
|
@ -638,86 +600,3 @@ function getSegmentHelper(state, win) {
|
|||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the path segments from given parameters.
|
||||
* @param {Number} startTime - Starting time of animation.
|
||||
* @param {Number} endTime - Ending time of animation.
|
||||
* @param {Number} minSegmentDuration - Minimum segment duration.
|
||||
* @param {Number} minProgressThreshold - Minimum progress threshold.
|
||||
* @param {Object} segmentHelper - The object of getSegmentHelper.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} progress}, ...]
|
||||
*/
|
||||
function createPathSegments(startTime, endTime, minSegmentDuration,
|
||||
minProgressThreshold, segmentHelper) {
|
||||
// If the duration is too short, early return.
|
||||
if (endTime - startTime < minSegmentDuration) {
|
||||
return [segmentHelper.getSegment(startTime),
|
||||
segmentHelper.getSegment(endTime)];
|
||||
}
|
||||
|
||||
// Otherwise, start creating segments.
|
||||
let pathSegments = [];
|
||||
|
||||
// Append the segment for the startTime position.
|
||||
const startTimeSegment = segmentHelper.getSegment(startTime);
|
||||
pathSegments.push(startTimeSegment);
|
||||
let previousSegment = startTimeSegment;
|
||||
|
||||
// Split the duration in equal intervals, and iterate over them.
|
||||
// See the definition of DURATION_RESOLUTION for more information about this.
|
||||
const interval = (endTime - startTime) / DURATION_RESOLUTION;
|
||||
for (let index = 1; index <= DURATION_RESOLUTION; index++) {
|
||||
// Create a segment for this interval.
|
||||
const currentSegment =
|
||||
segmentHelper.getSegment(startTime + index * interval);
|
||||
|
||||
// If the distance between the Y coordinate (the animation's progress) of
|
||||
// the previous segment and the Y coordinate of the current segment is too
|
||||
// large, then recurse with a smaller duration to get more details
|
||||
// in the graph.
|
||||
if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
|
||||
// Divide the current interval (excluding start and end bounds
|
||||
// by adding/subtracting BOUND_EXCLUDING_TIME).
|
||||
pathSegments = pathSegments.concat(
|
||||
createPathSegments(previousSegment.x + BOUND_EXCLUDING_TIME,
|
||||
currentSegment.x - BOUND_EXCLUDING_TIME,
|
||||
minSegmentDuration, minProgressThreshold,
|
||||
segmentHelper));
|
||||
}
|
||||
|
||||
pathSegments.push(currentSegment);
|
||||
previousSegment = currentSegment;
|
||||
}
|
||||
|
||||
return pathSegments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Append path element.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
|
||||
* @param {String} cls - Class name.
|
||||
* @return {Element} path element.
|
||||
*/
|
||||
function appendPathElement(parentEl, pathSegments, cls) {
|
||||
// Create path string.
|
||||
let path = `M${ pathSegments[0].x },0`;
|
||||
pathSegments.forEach(pathSegment => {
|
||||
path += ` L${ pathSegment.x },${ pathSegment.y }`;
|
||||
});
|
||||
path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
|
||||
// Append and return the path element.
|
||||
return createNode({
|
||||
parent: parentEl,
|
||||
namespace: SVG_NS,
|
||||
nodeType: "path",
|
||||
attributes: {
|
||||
"d": path,
|
||||
"class": cls,
|
||||
"vector-effect": "non-scaling-stroke",
|
||||
"transform": "scale(1, -1)"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,16 +6,22 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const {Task} = require("devtools/shared/task");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {
|
||||
createNode,
|
||||
findOptimalTimeInterval,
|
||||
getFormattedAnimationTitle,
|
||||
TimeScale
|
||||
} = require("devtools/client/animationinspector/utils");
|
||||
const {AnimationDetails} = require("devtools/client/animationinspector/components/animation-details");
|
||||
const {AnimationTargetNode} = require("devtools/client/animationinspector/components/animation-target-node");
|
||||
const {AnimationTimeBlock} = require("devtools/client/animationinspector/components/animation-time-block");
|
||||
|
||||
const { LocalizationHelper } = require("devtools/shared/l10n");
|
||||
const L10N =
|
||||
new LocalizationHelper("devtools/client/locales/animationinspector.properties");
|
||||
|
||||
// The minimum spacing between 2 time graduation headers in the timeline (px).
|
||||
const TIME_GRADUATION_MIN_SPACING = 40;
|
||||
// When the container window is resized, the timeline background gets refreshed,
|
||||
|
@ -42,7 +48,6 @@ function AnimationsTimeline(inspector, serverTraits) {
|
|||
this.animations = [];
|
||||
this.targetNodes = [];
|
||||
this.timeBlocks = [];
|
||||
this.details = [];
|
||||
this.inspector = inspector;
|
||||
this.serverTraits = serverTraits;
|
||||
|
||||
|
@ -53,7 +58,8 @@ function AnimationsTimeline(inspector, serverTraits) {
|
|||
this.onScrubberMouseMove = this.onScrubberMouseMove.bind(this);
|
||||
this.onAnimationSelected = this.onAnimationSelected.bind(this);
|
||||
this.onWindowResize = this.onWindowResize.bind(this);
|
||||
this.onFrameSelected = this.onFrameSelected.bind(this);
|
||||
this.onTimelineDataChanged = this.onTimelineDataChanged.bind(this);
|
||||
this.onDetailCloseButtonClick = this.onDetailCloseButtonClick.bind(this);
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
@ -63,16 +69,52 @@ exports.AnimationsTimeline = AnimationsTimeline;
|
|||
AnimationsTimeline.prototype = {
|
||||
init: function (containerEl) {
|
||||
this.win = containerEl.ownerDocument.defaultView;
|
||||
this.rootWrapperEl = containerEl;
|
||||
|
||||
this.rootWrapperEl = createNode({
|
||||
parent: containerEl,
|
||||
attributes: {
|
||||
"class": "animation-timeline"
|
||||
}
|
||||
this.setupSplitBox();
|
||||
this.setupAnimationTimeline();
|
||||
this.setupAnimationDetail();
|
||||
|
||||
this.win.addEventListener("resize",
|
||||
this.onWindowResize);
|
||||
},
|
||||
|
||||
setupSplitBox: function () {
|
||||
const browserRequire = this.win.BrowserLoader({
|
||||
window: this.win,
|
||||
useOnlyShared: true
|
||||
}).require;
|
||||
|
||||
const React = browserRequire("devtools/client/shared/vendor/react");
|
||||
const ReactDOM = browserRequire("devtools/client/shared/vendor/react-dom");
|
||||
|
||||
const SplitBox = React.createFactory(
|
||||
browserRequire("devtools/client/shared/components/splitter/split-box"));
|
||||
|
||||
const splitter = SplitBox({
|
||||
className: "animation-root",
|
||||
splitterSize: 1,
|
||||
initialHeight: "50%",
|
||||
endPanelControl: true,
|
||||
startPanel: React.DOM.div({
|
||||
className: "animation-timeline"
|
||||
}),
|
||||
endPanel: React.DOM.div({
|
||||
className: "animation-detail"
|
||||
}),
|
||||
vert: false
|
||||
});
|
||||
|
||||
ReactDOM.render(splitter, this.rootWrapperEl);
|
||||
|
||||
this.animationRootEl = this.rootWrapperEl.querySelector(".animation-root");
|
||||
},
|
||||
|
||||
setupAnimationTimeline: function () {
|
||||
const animationTimelineEl = this.rootWrapperEl.querySelector(".animation-timeline");
|
||||
|
||||
let scrubberContainer = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
parent: animationTimelineEl,
|
||||
attributes: {"class": "scrubber-wrapper"}
|
||||
});
|
||||
|
||||
|
@ -89,11 +131,17 @@ AnimationsTimeline.prototype = {
|
|||
"class": "scrubber-handle"
|
||||
}
|
||||
});
|
||||
createNode({
|
||||
parent: this.scrubberHandleEl,
|
||||
attributes: {
|
||||
"class": "scrubber-line"
|
||||
}
|
||||
});
|
||||
this.scrubberHandleEl.addEventListener("mousedown",
|
||||
this.onScrubberMouseDown);
|
||||
this.onScrubberMouseDown);
|
||||
|
||||
this.headerWrapper = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
parent: animationTimelineEl,
|
||||
attributes: {
|
||||
"class": "header-wrapper"
|
||||
}
|
||||
|
@ -107,30 +155,83 @@ AnimationsTimeline.prototype = {
|
|||
});
|
||||
|
||||
this.timeHeaderEl.addEventListener("mousedown",
|
||||
this.onScrubberMouseDown);
|
||||
this.onScrubberMouseDown);
|
||||
|
||||
this.timeTickEl = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
parent: animationTimelineEl,
|
||||
attributes: {
|
||||
"class": "time-body track-container"
|
||||
}
|
||||
});
|
||||
|
||||
this.animationsEl = createNode({
|
||||
parent: this.rootWrapperEl,
|
||||
parent: animationTimelineEl,
|
||||
nodeType: "ul",
|
||||
attributes: {
|
||||
"class": "animations"
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
this.win.addEventListener("resize",
|
||||
this.onWindowResize);
|
||||
setupAnimationDetail: function () {
|
||||
const animationDetailEl = this.rootWrapperEl.querySelector(".animation-detail");
|
||||
|
||||
const animationDetailHeaderEl = createNode({
|
||||
parent: animationDetailEl,
|
||||
attributes: {
|
||||
"class": "animation-detail-header"
|
||||
}
|
||||
});
|
||||
|
||||
const headerTitleEl = createNode({
|
||||
parent: animationDetailHeaderEl,
|
||||
attributes: {
|
||||
"class": "devtools-toolbar"
|
||||
}
|
||||
});
|
||||
|
||||
createNode({
|
||||
parent: headerTitleEl,
|
||||
textContent: L10N.getStr("detail.headerTitle")
|
||||
});
|
||||
|
||||
this.animationAnimationNameEl = createNode({
|
||||
parent: headerTitleEl
|
||||
});
|
||||
|
||||
this.animationDetailCloseButton = createNode({
|
||||
parent: headerTitleEl,
|
||||
nodeType: "button",
|
||||
attributes: {
|
||||
"class": "devtools-button",
|
||||
title: L10N.getStr("detail.header.closeLabel"),
|
||||
}
|
||||
});
|
||||
this.animationDetailCloseButton.addEventListener("click",
|
||||
this.onDetailCloseButtonClick);
|
||||
|
||||
const animationDetailBodyEl = createNode({
|
||||
parent: animationDetailEl,
|
||||
attributes: {
|
||||
"class": "animation-detail-body"
|
||||
}
|
||||
});
|
||||
|
||||
this.animatedPropertiesEl = createNode({
|
||||
parent: animationDetailBodyEl,
|
||||
attributes: {
|
||||
"class": "animated-properties"
|
||||
}
|
||||
});
|
||||
|
||||
this.details = new AnimationDetails(this.serverTraits);
|
||||
this.details.init(this.animatedPropertiesEl);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.stopAnimatingScrubber();
|
||||
this.unrender();
|
||||
this.details.destroy();
|
||||
|
||||
this.win.removeEventListener("resize",
|
||||
this.onWindowResize);
|
||||
|
@ -138,18 +239,26 @@ AnimationsTimeline.prototype = {
|
|||
this.onScrubberMouseDown);
|
||||
this.scrubberHandleEl.removeEventListener("mousedown",
|
||||
this.onScrubberMouseDown);
|
||||
this.animationDetailCloseButton.removeEventListener("click",
|
||||
this.onDetailCloseButtonClick);
|
||||
|
||||
this.rootWrapperEl.remove();
|
||||
this.animations = [];
|
||||
|
||||
this.rootWrapperEl = null;
|
||||
this.timeHeaderEl = null;
|
||||
this.animationsEl = null;
|
||||
this.animatedPropertiesEl = null;
|
||||
this.scrubberEl = null;
|
||||
this.scrubberHandleEl = null;
|
||||
this.win = null;
|
||||
this.inspector = null;
|
||||
this.serverTraits = null;
|
||||
this.animationDetailEl = null;
|
||||
this.animationAnimationNameEl = null;
|
||||
this.animatedPropertiesEl = null;
|
||||
this.animationDetailCloseButton = null;
|
||||
this.animationRootEl = null;
|
||||
this.selectedAnimation = null;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -169,6 +278,11 @@ AnimationsTimeline.prototype = {
|
|||
},
|
||||
|
||||
unrender: function () {
|
||||
this.unrenderButLeaveDetailsComponent();
|
||||
this.details.unrender();
|
||||
},
|
||||
|
||||
unrenderButLeaveDetailsComponent: function () {
|
||||
for (let animation of this.animations) {
|
||||
animation.off("changed", this.onAnimationStateChanged);
|
||||
}
|
||||
|
@ -176,11 +290,8 @@ AnimationsTimeline.prototype = {
|
|||
TimeScale.reset();
|
||||
this.destroySubComponents("targetNodes");
|
||||
this.destroySubComponents("timeBlocks");
|
||||
this.destroySubComponents("details", [{
|
||||
event: "frame-selected",
|
||||
fn: this.onFrameSelected
|
||||
}]);
|
||||
this.animationsEl.innerHTML = "";
|
||||
this.off("timeline-data-changed", this.onTimelineDataChanged);
|
||||
},
|
||||
|
||||
onWindowResize: function () {
|
||||
|
@ -198,36 +309,52 @@ AnimationsTimeline.prototype = {
|
|||
}, TIMELINE_BACKGROUND_RESIZE_DEBOUNCE_TIMER);
|
||||
},
|
||||
|
||||
onAnimationSelected: function (e, animation) {
|
||||
onAnimationSelected: Task.async(function* (e, animation) {
|
||||
let index = this.animations.indexOf(animation);
|
||||
if (index === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = this.rootWrapperEl;
|
||||
let animationEl = el.querySelectorAll(".animation")[index];
|
||||
let propsEl = el.querySelectorAll(".animated-properties")[index];
|
||||
|
||||
// Toggle the selected state on this animation.
|
||||
animationEl.classList.toggle("selected");
|
||||
propsEl.classList.toggle("selected");
|
||||
|
||||
// Render the details component for this animation if it was shown.
|
||||
if (animationEl.classList.contains("selected")) {
|
||||
this.details[index].render(animation);
|
||||
this.emit("animation-selected", animation);
|
||||
} else {
|
||||
this.emit("animation-unselected", animation);
|
||||
// Unselect an animation which was selected.
|
||||
const animationEls = this.rootWrapperEl.querySelectorAll(".animation");
|
||||
for (let i = 0; i < animationEls.length; i++) {
|
||||
const animationEl = animationEls[i];
|
||||
if (!animationEl.classList.contains("selected")) {
|
||||
continue;
|
||||
}
|
||||
if (i === index) {
|
||||
// Already the animation is selected.
|
||||
this.emit("animation-already-selected", this.animations[i]);
|
||||
return;
|
||||
}
|
||||
animationEl.classList.remove("selected");
|
||||
this.emit("animation-unselected", this.animations[i]);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
// Add class of animation type to animatedPropertiesEl to display the compositor sign.
|
||||
if (!this.animatedPropertiesEl.classList.contains(animation.state.type)) {
|
||||
this.animatedPropertiesEl.className =
|
||||
`animated-properties ${ animation.state.type }`;
|
||||
}
|
||||
|
||||
// Select and render.
|
||||
const selectedAnimationEl = animationEls[index];
|
||||
selectedAnimationEl.classList.add("selected");
|
||||
this.animationRootEl.classList.add("animation-detail-visible");
|
||||
if (animation !== this.details.animation) {
|
||||
this.selectedAnimation = animation;
|
||||
// Don't render if the detail displays same animation already.
|
||||
yield this.details.render(animation);
|
||||
this.animationAnimationNameEl.textContent = getFormattedAnimationTitle(animation);
|
||||
}
|
||||
this.onTimelineDataChanged(null, { time: this.currentTime || 0 });
|
||||
this.emit("animation-selected", animation);
|
||||
}),
|
||||
|
||||
/**
|
||||
* When a frame gets selected, move the scrubber to the corresponding position
|
||||
* When move the scrubber to the corresponding position
|
||||
*/
|
||||
onFrameSelected: function (e, {x}) {
|
||||
this.moveScrubberTo(x, true);
|
||||
},
|
||||
|
||||
onScrubberMouseDown: function (e) {
|
||||
this.moveScrubberTo(e.pageX);
|
||||
this.win.addEventListener("mouseup", this.onScrubberMouseUp);
|
||||
|
@ -303,7 +430,7 @@ AnimationsTimeline.prototype = {
|
|||
},
|
||||
|
||||
render: function (animations, documentCurrentTime) {
|
||||
this.unrender();
|
||||
this.unrenderButLeaveDetailsComponent();
|
||||
|
||||
this.animations = animations;
|
||||
if (!this.animations.length) {
|
||||
|
@ -331,21 +458,6 @@ AnimationsTimeline.prototype = {
|
|||
}
|
||||
});
|
||||
|
||||
// Right below the line is a hidden-by-default line for displaying the
|
||||
// inline keyframes.
|
||||
let detailsEl = createNode({
|
||||
parent: this.animationsEl,
|
||||
nodeType: "li",
|
||||
attributes: {
|
||||
"class": "animated-properties " + animation.state.type
|
||||
}
|
||||
});
|
||||
|
||||
let details = new AnimationDetails(this.serverTraits);
|
||||
details.init(detailsEl);
|
||||
details.on("frame-selected", this.onFrameSelected);
|
||||
this.details.push(details);
|
||||
|
||||
// Left sidebar for the animated node.
|
||||
let animatedNodeEl = createNode({
|
||||
parent: animationEl,
|
||||
|
@ -389,6 +501,25 @@ AnimationsTimeline.prototype = {
|
|||
? TimeScale.minStartTime
|
||||
: documentCurrentTime);
|
||||
}
|
||||
|
||||
// To indicate the animation progress in AnimationDetails.
|
||||
this.on("timeline-data-changed", this.onTimelineDataChanged);
|
||||
|
||||
// Display animation's detail if there is only one animation,
|
||||
// or the previously displayed animation is included in timeline list.
|
||||
if (this.animations.length === 1) {
|
||||
this.onAnimationSelected(null, this.animations[0]);
|
||||
return;
|
||||
}
|
||||
if (!this.animationRootEl.classList.contains("animation-detail-visible")) {
|
||||
// Do nothing since the animation detail pane is closing now.
|
||||
return;
|
||||
}
|
||||
if (this.animations.indexOf(this.selectedAnimation) >= 0) {
|
||||
this.onAnimationSelected(null, this.selectedAnimation);
|
||||
return;
|
||||
}
|
||||
this.onDetailCloseButtonClick();
|
||||
},
|
||||
|
||||
isAtLeastOneAnimationPlaying: function () {
|
||||
|
@ -498,5 +629,17 @@ AnimationsTimeline.prototype = {
|
|||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onTimelineDataChanged: function (e, { time }) {
|
||||
this.currentTime = time;
|
||||
const indicateTime =
|
||||
TimeScale.minStartTime === Infinity ? 0 : this.currentTime + TimeScale.minStartTime;
|
||||
this.details.indicateProgress(indicateTime);
|
||||
},
|
||||
|
||||
onDetailCloseButtonClick: function (e) {
|
||||
this.animationRootEl.classList.remove("animation-detail-visible");
|
||||
this.emit("animation-detail-closed");
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,14 +7,20 @@
|
|||
"use strict";
|
||||
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
const {createNode} = require("devtools/client/animationinspector/utils");
|
||||
const {createNode, createSVGNode} =
|
||||
require("devtools/client/animationinspector/utils");
|
||||
const {ProgressGraphHelper, appendPathElement, DEFAULT_MIN_PROGRESS_THRESHOLD} =
|
||||
require("devtools/client/animationinspector/graph-helper.js");
|
||||
|
||||
// Counter for linearGradient ID.
|
||||
let LINEAR_GRADIENT_ID_COUNTER = 0;
|
||||
|
||||
/**
|
||||
* UI component responsible for displaying a list of keyframes.
|
||||
* Also, shows a graphical graph for the animation progress of one iteration.
|
||||
*/
|
||||
function Keyframes() {
|
||||
EventEmitter.decorate(this);
|
||||
this.onClick = this.onClick.bind(this);
|
||||
}
|
||||
|
||||
exports.Keyframes = Keyframes;
|
||||
|
@ -27,55 +33,116 @@ Keyframes.prototype = {
|
|||
parent: this.containerEl,
|
||||
attributes: {"class": "keyframes"}
|
||||
});
|
||||
|
||||
this.containerEl.addEventListener("click", this.onClick);
|
||||
},
|
||||
|
||||
destroy: function () {
|
||||
this.containerEl.removeEventListener("click", this.onClick);
|
||||
this.keyframesEl.remove();
|
||||
this.containerEl = this.keyframesEl = this.animation = null;
|
||||
},
|
||||
|
||||
render: function ({keyframes, propertyName, animation}) {
|
||||
render: function ({keyframes, propertyName, animation, animationType}) {
|
||||
this.keyframes = keyframes;
|
||||
this.propertyName = propertyName;
|
||||
this.animation = animation;
|
||||
|
||||
let iterationStartOffset =
|
||||
animation.state.iterationStart % 1 == 0
|
||||
? 0
|
||||
: 1 - animation.state.iterationStart % 1;
|
||||
// Create graph element.
|
||||
const graphEl = createSVGNode({
|
||||
parent: this.keyframesEl,
|
||||
nodeType: "svg",
|
||||
attributes: {
|
||||
"preserveAspectRatio": "none"
|
||||
}
|
||||
});
|
||||
|
||||
// This visual is only one iteration,
|
||||
// so we use animation.state.duration as total duration.
|
||||
const totalDuration = animation.state.duration;
|
||||
|
||||
// Calculate stroke height in viewBox to display stroke of path.
|
||||
const strokeHeightForViewBox = 0.5 / this.containerEl.clientHeight;
|
||||
// Minimum segment duration is the duration of one pixel.
|
||||
const minSegmentDuration =
|
||||
totalDuration / this.containerEl.clientWidth;
|
||||
|
||||
// Set viewBox.
|
||||
graphEl.setAttribute("viewBox",
|
||||
`0 -${ 1 + strokeHeightForViewBox }
|
||||
${ totalDuration }
|
||||
${ 1 + strokeHeightForViewBox * 2 }`);
|
||||
|
||||
// Create graph helper to render the animation property graph.
|
||||
const graphHelper =
|
||||
new ProgressGraphHelper(this.containerEl.ownerDocument.defaultView,
|
||||
propertyName, animationType, keyframes, totalDuration);
|
||||
|
||||
renderPropertyGraph(graphEl, totalDuration, minSegmentDuration,
|
||||
DEFAULT_MIN_PROGRESS_THRESHOLD, graphHelper);
|
||||
|
||||
// Destroy ProgressGraphHelper resources.
|
||||
graphHelper.destroy();
|
||||
|
||||
// Append elements to display keyframe values.
|
||||
this.keyframesEl.classList.add(animation.state.type);
|
||||
for (let frame of this.keyframes) {
|
||||
let offset = frame.offset + iterationStartOffset;
|
||||
createNode({
|
||||
parent: this.keyframesEl,
|
||||
attributes: {
|
||||
"class": "frame",
|
||||
"style": `left:${offset * 100}%;`,
|
||||
"style": `left:${frame.offset * 100}%;`,
|
||||
"data-offset": frame.offset,
|
||||
"data-property": propertyName,
|
||||
"title": frame.value
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
onClick: function (e) {
|
||||
// If the click happened on a frame, tell our parent about it.
|
||||
if (!e.target.classList.contains("frame")) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.stopPropagation();
|
||||
this.emit("frame-selected", {
|
||||
animation: this.animation,
|
||||
propertyName: this.propertyName,
|
||||
offset: parseFloat(e.target.dataset.offset),
|
||||
value: e.target.getAttribute("title"),
|
||||
x: e.target.offsetLeft + e.target.closest(".frames").offsetLeft
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Render a graph representing the progress of the animation over one iteration.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Number} duration - Duration of one iteration.
|
||||
* @param {Number} minSegmentDuration - Minimum segment duration.
|
||||
* @param {Number} minProgressThreshold - Minimum progress threshold.
|
||||
* @param {ProgressGraphHelper} graphHelper - The object of ProgressGraphHalper.
|
||||
*/
|
||||
function renderPropertyGraph(parentEl, duration, minSegmentDuration,
|
||||
minProgressThreshold, graphHelper) {
|
||||
const segments = graphHelper.createPathSegments(0, duration, minSegmentDuration,
|
||||
minProgressThreshold);
|
||||
|
||||
const graphType = graphHelper.getGraphType();
|
||||
if (graphType !== "color") {
|
||||
appendPathElement(parentEl, segments, graphType);
|
||||
return;
|
||||
}
|
||||
|
||||
// Append the color to the path.
|
||||
segments.forEach(segment => {
|
||||
segment.y = 1;
|
||||
});
|
||||
const path = appendPathElement(parentEl, segments, graphType);
|
||||
const defEl = createSVGNode({
|
||||
parent: parentEl,
|
||||
nodeType: "def"
|
||||
});
|
||||
const id = `color-property-${ LINEAR_GRADIENT_ID_COUNTER++ }`;
|
||||
const linearGradientEl = createSVGNode({
|
||||
parent: defEl,
|
||||
nodeType: "linearGradient",
|
||||
attributes: {
|
||||
"id": id
|
||||
}
|
||||
});
|
||||
segments.forEach(segment => {
|
||||
createSVGNode({
|
||||
parent: linearGradientEl,
|
||||
nodeType: "stop",
|
||||
attributes: {
|
||||
"stop-color": segment.style,
|
||||
"offset": segment.x / duration
|
||||
}
|
||||
});
|
||||
});
|
||||
path.style.fill = `url(#${ id })`;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,461 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* 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 {createSVGNode, getJsPropertyName} =
|
||||
require("devtools/client/animationinspector/utils");
|
||||
const {colorUtils} = require("devtools/shared/css/color.js");
|
||||
const {parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
|
||||
|
||||
// In the createPathSegments function, an animation duration is divided by
|
||||
// DURATION_RESOLUTION in order to draw the way the animation progresses.
|
||||
// But depending on the timing-function, we may be not able to make the graph
|
||||
// smoothly progress if this resolution is not high enough.
|
||||
// So, if the difference of animation progress between 2 divisions is more than
|
||||
// DEFAULT_MIN_PROGRESS_THRESHOLD, then createPathSegments re-divides
|
||||
// by DURATION_RESOLUTION.
|
||||
// DURATION_RESOLUTION shoud be integer and more than 2.
|
||||
const DURATION_RESOLUTION = 4;
|
||||
// DEFAULT_MIN_PROGRESS_THRESHOLD shoud be between more than 0 to 1.
|
||||
const DEFAULT_MIN_PROGRESS_THRESHOLD = 0.1;
|
||||
exports.DEFAULT_MIN_PROGRESS_THRESHOLD = DEFAULT_MIN_PROGRESS_THRESHOLD;
|
||||
// BOUND_EXCLUDING_TIME should be less than 1ms and is used to exclude start
|
||||
// and end bounds when dividing duration in createPathSegments.
|
||||
const BOUND_EXCLUDING_TIME = 0.001;
|
||||
|
||||
/**
|
||||
* This helper return the segment coordinates and style for property graph,
|
||||
* also return the graph type.
|
||||
* Parameters of constructor are below.
|
||||
* @param {Window} win - window object to animate.
|
||||
* @param {String} propertyCSSName - CSS property name (e.g. background-color).
|
||||
* @param {String} animationType - Animation type of CSS property.
|
||||
* @param {Object} keyframes - AnimationInspector's keyframes object.
|
||||
* @param {float} duration - Duration of animation.
|
||||
*/
|
||||
function ProgressGraphHelper(win, propertyCSSName, animationType, keyframes, duration) {
|
||||
this.win = win;
|
||||
const doc = this.win.document;
|
||||
this.targetEl = doc.createElement("div");
|
||||
doc.documentElement.appendChild(this.targetEl);
|
||||
|
||||
this.propertyCSSName = propertyCSSName;
|
||||
this.propertyJSName = getJsPropertyName(this.propertyCSSName);
|
||||
this.animationType = animationType;
|
||||
|
||||
// Create keyframe object to make dummy animation.
|
||||
const keyframesObject = keyframes.map(keyframe => {
|
||||
const keyframeObject = Object.assign({}, keyframe);
|
||||
keyframeObject[this.propertyJSName] = keyframe.value;
|
||||
return keyframeObject;
|
||||
});
|
||||
|
||||
// Create effect timing object to make dummy animation.
|
||||
const effectTiming = {
|
||||
duration: duration,
|
||||
fill: "forwards"
|
||||
};
|
||||
|
||||
this.keyframes = keyframesObject;
|
||||
this.devtoolsKeyframes = keyframes;
|
||||
this.animation = this.targetEl.animate(this.keyframes, effectTiming);
|
||||
this.animation.pause();
|
||||
this.valueHelperFunction = this.getValueHelperFunction();
|
||||
}
|
||||
|
||||
ProgressGraphHelper.prototype = {
|
||||
/**
|
||||
* Destory this object.
|
||||
*/
|
||||
destroy: function () {
|
||||
this.targetEl.remove();
|
||||
this.animation.cancel();
|
||||
|
||||
this.targetEl = null;
|
||||
this.animation = null;
|
||||
this.valueHelperFunction = null;
|
||||
this.propertyCSSName = null;
|
||||
this.propertyJSName = null;
|
||||
this.animationType = null;
|
||||
this.keyframes = null;
|
||||
this.win = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return graph type.
|
||||
* @return {String} if property is 'opacity' or 'transform', return that value.
|
||||
* Otherwise, return given animation type in constructor.
|
||||
*/
|
||||
getGraphType: function () {
|
||||
return (this.propertyJSName === "opacity" || this.propertyJSName === "transform")
|
||||
? this.propertyJSName : this.animationType;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return a segment in graph by given the time.
|
||||
* @return {Object} Computed result which has follwing values.
|
||||
* - x: x value of graph (float)
|
||||
* - y: y value of graph (float between 0 - 1)
|
||||
* - style: the computed style value of the property at the time
|
||||
*/
|
||||
getSegment: function (time) {
|
||||
this.animation.currentTime = time;
|
||||
const style = this.win.getComputedStyle(this.targetEl)[this.propertyJSName];
|
||||
const value = this.valueHelperFunction(style);
|
||||
return { x: time, y: value, style: style };
|
||||
},
|
||||
|
||||
/**
|
||||
* Get a value helper function which calculates the value of Y axis by animation type.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given progress and style (e.g. rgb(0, 0, 0))
|
||||
*/
|
||||
getValueHelperFunction: function () {
|
||||
switch (this.animationType) {
|
||||
case "none": {
|
||||
return () => 1;
|
||||
}
|
||||
case "float": {
|
||||
return this.getFloatValueHelperFunction();
|
||||
}
|
||||
case "coord": {
|
||||
return this.getCoordinateValueHelperFunction();
|
||||
}
|
||||
case "color": {
|
||||
return this.getColorValueHelperFunction();
|
||||
}
|
||||
case "discrete": {
|
||||
return this.getDiscreteValueHelperFunction();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'float'.
|
||||
* @param {Object} keyframes - This object shoud be same as
|
||||
* the parameter of getGraphHelper.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given float (e.g. 1.0, 0.5 and so on)
|
||||
*/
|
||||
getFloatValueHelperFunction: function () {
|
||||
let maxValue = 0;
|
||||
let minValue = Infinity;
|
||||
this.keyframes.forEach(keyframe => {
|
||||
maxValue = Math.max(maxValue, keyframe.value);
|
||||
minValue = Math.min(minValue, keyframe.value);
|
||||
});
|
||||
const distance = maxValue - minValue;
|
||||
return value => {
|
||||
return (value - minValue) / distance;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'coord'.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given style (e.g. 100px)
|
||||
*/
|
||||
getCoordinateValueHelperFunction: function () {
|
||||
let maxValue = 0;
|
||||
let minValue = Infinity;
|
||||
for (let i = 0, n = this.keyframes.length; i < n; i++) {
|
||||
if (this.keyframes[i].value.match(/calc/)) {
|
||||
return null;
|
||||
}
|
||||
const value = parseFloat(this.keyframes[i].value);
|
||||
minValue = Math.min(minValue, value);
|
||||
maxValue = Math.max(maxValue, value);
|
||||
}
|
||||
const distance = maxValue - minValue;
|
||||
return value => {
|
||||
return (parseFloat(value) - minValue) / distance;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'color'.
|
||||
* @param {Object} keyframes - This object shoud be same as
|
||||
* the parameter of getGraphHelper.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given color (e.g. rgb(0, 0, 0))
|
||||
*/
|
||||
getColorValueHelperFunction: function () {
|
||||
const maxObject = { distance: 0 };
|
||||
for (let i = 0; i < this.keyframes.length - 1; i++) {
|
||||
const value1 = getRGBA(this.keyframes[i].value);
|
||||
for (let j = i + 1; j < this.keyframes.length; j++) {
|
||||
const value2 = getRGBA(this.keyframes[j].value);
|
||||
const distance = getRGBADistance(value1, value2);
|
||||
if (maxObject.distance >= distance) {
|
||||
continue;
|
||||
}
|
||||
maxObject.distance = distance;
|
||||
maxObject.value1 = value1;
|
||||
maxObject.value2 = value2;
|
||||
}
|
||||
}
|
||||
const baseValue =
|
||||
maxObject.value1 < maxObject.value2 ? maxObject.value1 : maxObject.value2;
|
||||
|
||||
return value => {
|
||||
const colorValue = getRGBA(value);
|
||||
return getRGBADistance(baseValue, colorValue) / maxObject.distance;
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Return value helper function of animation type 'discrete'.
|
||||
* @return {function} ValueHelperFunction returns float value of Y axis
|
||||
* from given style (e.g. center)
|
||||
*/
|
||||
getDiscreteValueHelperFunction: function () {
|
||||
const discreteValues = [];
|
||||
this.keyframes.forEach(keyframe => {
|
||||
if (!discreteValues.includes(keyframe.value)) {
|
||||
discreteValues.push(keyframe.value);
|
||||
}
|
||||
});
|
||||
return value => {
|
||||
return discreteValues.indexOf(value) / (discreteValues.length - 1);
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Create the path segments from given parameters.
|
||||
* @param {Number} startTime - Starting time of animation.
|
||||
* @param {Number} endTime - Ending time of animation.
|
||||
* @param {Number} minSegmentDuration - Minimum segment duration.
|
||||
* @param {Number} minProgressThreshold - Minimum progress threshold.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} progress}, ...]
|
||||
*/
|
||||
createPathSegments: function (startTime, endTime,
|
||||
minSegmentDuration, minProgressThreshold) {
|
||||
return !this.valueHelperFunction
|
||||
? createKeyframesPathSegments(endTime - startTime, this.devtoolsKeyframes)
|
||||
: createPathSegments(startTime, endTime,
|
||||
minSegmentDuration, minProgressThreshold, this);
|
||||
},
|
||||
};
|
||||
|
||||
exports.ProgressGraphHelper = ProgressGraphHelper;
|
||||
|
||||
/**
|
||||
* Create the path segments from given parameters.
|
||||
* @param {Number} startTime - Starting time of animation.
|
||||
* @param {Number} endTime - Ending time of animation.
|
||||
* @param {Number} minSegmentDuration - Minimum segment duration.
|
||||
* @param {Number} minProgressThreshold - Minimum progress threshold.
|
||||
* @param {Object} segmentHelper
|
||||
* - getSegment(time): Helper function that, given a time,
|
||||
* will calculate the animation progress.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} progress}, ...]
|
||||
*/
|
||||
function createPathSegments(startTime, endTime, minSegmentDuration,
|
||||
minProgressThreshold, segmentHelper) {
|
||||
// If the duration is too short, early return.
|
||||
if (endTime - startTime < minSegmentDuration) {
|
||||
return [segmentHelper.getSegment(startTime),
|
||||
segmentHelper.getSegment(endTime)];
|
||||
}
|
||||
|
||||
// Otherwise, start creating segments.
|
||||
let pathSegments = [];
|
||||
|
||||
// Append the segment for the startTime position.
|
||||
const startTimeSegment = segmentHelper.getSegment(startTime);
|
||||
pathSegments.push(startTimeSegment);
|
||||
let previousSegment = startTimeSegment;
|
||||
|
||||
// Split the duration in equal intervals, and iterate over them.
|
||||
// See the definition of DURATION_RESOLUTION for more information about this.
|
||||
const interval = (endTime - startTime) / DURATION_RESOLUTION;
|
||||
for (let index = 1; index <= DURATION_RESOLUTION; index++) {
|
||||
// Create a segment for this interval.
|
||||
const currentSegment =
|
||||
segmentHelper.getSegment(startTime + index * interval);
|
||||
|
||||
// If the distance between the Y coordinate (the animation's progress) of
|
||||
// the previous segment and the Y coordinate of the current segment is too
|
||||
// large, then recurse with a smaller duration to get more details
|
||||
// in the graph.
|
||||
if (Math.abs(currentSegment.y - previousSegment.y) > minProgressThreshold) {
|
||||
// Divide the current interval (excluding start and end bounds
|
||||
// by adding/subtracting BOUND_EXCLUDING_TIME).
|
||||
pathSegments = pathSegments.concat(
|
||||
createPathSegments(previousSegment.x + BOUND_EXCLUDING_TIME,
|
||||
currentSegment.x - BOUND_EXCLUDING_TIME,
|
||||
minSegmentDuration, minProgressThreshold,
|
||||
segmentHelper));
|
||||
}
|
||||
|
||||
pathSegments.push(currentSegment);
|
||||
previousSegment = currentSegment;
|
||||
}
|
||||
|
||||
return pathSegments;
|
||||
}
|
||||
exports.createPathSegments = createPathSegments;
|
||||
|
||||
/**
|
||||
* Append path element.
|
||||
* @param {Element} parentEl - Parent element of this appended path element.
|
||||
* @param {Array} pathSegments - Path segments. Please see createPathSegments.
|
||||
* @param {String} cls - Class name.
|
||||
* @return {Element} path element.
|
||||
*/
|
||||
function appendPathElement(parentEl, pathSegments, cls) {
|
||||
// Create path string.
|
||||
let path = `M${ pathSegments[0].x },0`;
|
||||
for (let i = 0; i < pathSegments.length; i++) {
|
||||
const pathSegment = pathSegments[i];
|
||||
if (!pathSegment.easing || pathSegment.easing === "linear") {
|
||||
path += createLinePathString(pathSegment);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i + 1 === pathSegments.length) {
|
||||
// We already create steps or cubic-bezier path string in previous.
|
||||
break;
|
||||
}
|
||||
|
||||
const nextPathSegment = pathSegments[i + 1];
|
||||
path += pathSegment.easing.startsWith("steps")
|
||||
? createStepsPathString(pathSegment, nextPathSegment)
|
||||
: createCubicBezierPathString(pathSegment, nextPathSegment);
|
||||
}
|
||||
path += ` L${ pathSegments[pathSegments.length - 1].x },0 Z`;
|
||||
// Append and return the path element.
|
||||
return createSVGNode({
|
||||
parent: parentEl,
|
||||
nodeType: "path",
|
||||
attributes: {
|
||||
"d": path,
|
||||
"class": cls,
|
||||
"vector-effect": "non-scaling-stroke",
|
||||
"transform": "scale(1, -1)"
|
||||
}
|
||||
});
|
||||
}
|
||||
exports.appendPathElement = appendPathElement;
|
||||
|
||||
/**
|
||||
* Create the path segments from given keyframes.
|
||||
* @param {Number} duration - Duration of animation.
|
||||
* @param {Object} Keyframes of devtool's format.
|
||||
* @return {Array} path segments -
|
||||
* [{x: {Number} time, y: {Number} distance,
|
||||
* easing: {String} keyframe's easing,
|
||||
* style: {String} keyframe's value}, ...]
|
||||
*/
|
||||
function createKeyframesPathSegments(duration, keyframes) {
|
||||
return keyframes.map(keyframe => {
|
||||
return {
|
||||
x: keyframe.offset * duration,
|
||||
y: keyframe.distance,
|
||||
easing: keyframe.easing,
|
||||
style: keyframe.value
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a line path string.
|
||||
* @param {Object} segment - e.g. { x: 100, y: 1 }
|
||||
* @return {String} path string - e.g. "L100,1"
|
||||
*/
|
||||
function createLinePathString(segment) {
|
||||
return ` L${ segment.x },${ segment.y }`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a path string to represents a step function.
|
||||
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "steps(2)" }
|
||||
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
|
||||
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
|
||||
*/
|
||||
function createStepsPathString(currentSegment, nextSegment) {
|
||||
const matches =
|
||||
currentSegment.easing.match(/^steps\((\d+)(,\sstart)?\)/);
|
||||
const stepNumber = parseInt(matches[1], 10);
|
||||
const oneStepX = (nextSegment.x - currentSegment.x) / stepNumber;
|
||||
const oneStepY = (nextSegment.y - currentSegment.y) / stepNumber;
|
||||
const isStepStart = matches[2];
|
||||
const stepOffsetY = isStepStart ? 1 : 0;
|
||||
let path = "";
|
||||
for (let step = 0; step < stepNumber; step++) {
|
||||
const sx = currentSegment.x + step * oneStepX;
|
||||
const ex = sx + oneStepX;
|
||||
const y = currentSegment.y + (step + stepOffsetY) * oneStepY;
|
||||
path += ` L${ sx },${ y } L${ ex },${ y }`;
|
||||
}
|
||||
if (!isStepStart) {
|
||||
path += ` L${ nextSegment.x },${ nextSegment.y }`;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a path string to represents a bezier curve.
|
||||
* @param {Object} currentSegment - e.g. { x: 0, y: 0, easing: "ease" }
|
||||
* @param {Object} nextSegment - e.g. { x: 1, y: 1 }
|
||||
* @return {String} path string - e.g. "C 0.25 0.1, 0.25 1, 1 1"
|
||||
*/
|
||||
function createCubicBezierPathString(currentSegment, nextSegment) {
|
||||
const controlPoints = parseTimingFunction(currentSegment.easing);
|
||||
if (!controlPoints) {
|
||||
// Just return line path string since we could not parse this easing.
|
||||
return createLinePathString(currentSegment);
|
||||
}
|
||||
|
||||
const cp1x = controlPoints[0];
|
||||
const cp1y = controlPoints[1];
|
||||
const cp2x = controlPoints[2];
|
||||
const cp2y = controlPoints[3];
|
||||
|
||||
const diffX = nextSegment.x - currentSegment.x;
|
||||
const diffY = nextSegment.y - currentSegment.y;
|
||||
let path =
|
||||
` C ${ currentSegment.x + diffX * cp1x } ${ currentSegment.y + diffY * cp1y }`;
|
||||
path += `, ${ currentSegment.x + diffX * cp2x } ${ currentSegment.y + diffY * cp2y }`;
|
||||
path += `, ${ nextSegment.x } ${ nextSegment.y }`;
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse given RGBA string.
|
||||
* @param {String} colorString - e.g. rgb(0, 0, 0) or rgba(0, 0, 0, 0.5) and so on.
|
||||
* @return {Object} RGBA {r: r, g: g, b: b, a: a}.
|
||||
*/
|
||||
function getRGBA(colorString) {
|
||||
const color = new colorUtils.CssColor(colorString);
|
||||
return color.getRGBATuple();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the distance from give two RGBA.
|
||||
* @param {Object} rgba1 - RGBA (format is same to getRGBA)
|
||||
* @param {Object} rgba2 - RGBA (format is same to getRGBA)
|
||||
* @return {float} distance.
|
||||
*/
|
||||
function getRGBADistance(rgba1, rgba2) {
|
||||
const startA = rgba1.a;
|
||||
const startR = rgba1.r * startA;
|
||||
const startG = rgba1.g * startA;
|
||||
const startB = rgba1.b * startA;
|
||||
const endA = rgba2.a;
|
||||
const endR = rgba2.r * endA;
|
||||
const endG = rgba2.g * endA;
|
||||
const endB = rgba2.b * endA;
|
||||
const diffA = startA - endA;
|
||||
const diffR = startR - endR;
|
||||
const diffG = startG - endG;
|
||||
const diffB = startB - endB;
|
||||
return Math.sqrt(diffA * diffA + diffR * diffR + diffG * diffG + diffB * diffB);
|
||||
}
|
|
@ -12,8 +12,9 @@ DIRS += [
|
|||
]
|
||||
|
||||
DevToolsModules(
|
||||
'utils.js',
|
||||
'graph-helper.js',
|
||||
'utils.js'
|
||||
)
|
||||
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
|
||||
with Files('**'):
|
||||
BUG_COMPONENT = ('Firefox', 'Developer Tools: Animation Inspector')
|
||||
|
|
|
@ -3,6 +3,7 @@ tags = devtools
|
|||
subsuite = devtools
|
||||
support-files =
|
||||
doc_body_animation.html
|
||||
doc_delayed_starttime_animations.html
|
||||
doc_end_delay.html
|
||||
doc_frame_script.js
|
||||
doc_keyframes.html
|
||||
|
@ -13,6 +14,7 @@ support-files =
|
|||
doc_short_duration_animation.html
|
||||
doc_simple_animation.html
|
||||
doc_multiple_animation_types.html
|
||||
doc_multiple_property_types.html
|
||||
doc_timing_combination_animation.html
|
||||
head.js
|
||||
!/devtools/client/commandline/test/helpers.js
|
||||
|
@ -23,11 +25,14 @@ support-files =
|
|||
!/devtools/client/shared/test/test-actor.js
|
||||
|
||||
[browser_animation_animated_properties_displayed.js]
|
||||
[browser_animation_animated_properties_for_delayed_starttime_animations.js]
|
||||
[browser_animation_animated_properties_path.js]
|
||||
[browser_animation_animated_properties_progress_indicator.js]
|
||||
[browser_animation_click_selects_animation.js]
|
||||
[browser_animation_controller_exposes_document_currentTime.js]
|
||||
[browser_animation_detail_displayed.js]
|
||||
skip-if = os == "linux" && !debug # Bug 1234567
|
||||
[browser_animation_empty_on_invalid_nodes.js]
|
||||
[browser_animation_keyframe_click_to_set_time.js]
|
||||
[browser_animation_keyframe_markers.js]
|
||||
[browser_animation_mutations_with_same_names.js]
|
||||
[browser_animation_panel_exists.js]
|
||||
|
|
|
@ -11,6 +11,15 @@ const LAYOUT_ERRORS_L10N =
|
|||
// displayed below it.
|
||||
|
||||
const EXPECTED_PROPERTIES = [
|
||||
"border-bottom-left-radius",
|
||||
"border-bottom-right-radius",
|
||||
"border-top-left-radius",
|
||||
"border-top-right-radius",
|
||||
"filter",
|
||||
"height",
|
||||
"transform",
|
||||
"width",
|
||||
// Unchanged value properties
|
||||
"background-attachment",
|
||||
"background-clip",
|
||||
"background-color",
|
||||
|
@ -19,15 +28,7 @@ const EXPECTED_PROPERTIES = [
|
|||
"background-position-x",
|
||||
"background-position-y",
|
||||
"background-repeat",
|
||||
"background-size",
|
||||
"border-bottom-left-radius",
|
||||
"border-bottom-right-radius",
|
||||
"border-top-left-radius",
|
||||
"border-top-right-radius",
|
||||
"filter",
|
||||
"height",
|
||||
"transform",
|
||||
"width"
|
||||
"background-size"
|
||||
].sort();
|
||||
|
||||
add_task(function* () {
|
||||
|
@ -37,27 +38,22 @@ add_task(function* () {
|
|||
let propertiesList = timeline.rootWrapperEl
|
||||
.querySelector(".animated-properties");
|
||||
|
||||
ok(!isNodeVisible(propertiesList),
|
||||
"The list of properties panel is hidden by default");
|
||||
|
||||
info("Click to select the animation");
|
||||
yield clickOnAnimation(panel, 0);
|
||||
|
||||
// doc_keyframes.html has only one animation,
|
||||
// so the propertiesList shoud be shown.
|
||||
ok(isNodeVisible(propertiesList),
|
||||
"The list of properties panel is shown");
|
||||
"The list of properties panel shoud be shown");
|
||||
|
||||
ok(propertiesList.querySelectorAll(".property").length,
|
||||
"The list of properties panel actually contains properties");
|
||||
ok(hasExpectedProperties(propertiesList),
|
||||
"The list of properties panel contains the right properties");
|
||||
|
||||
ok(hasExpectedWarnings(propertiesList),
|
||||
"The list of properties panel contains the right warnings");
|
||||
|
||||
info("Click to unselect the animation");
|
||||
info("Click same animation again");
|
||||
yield clickOnAnimation(panel, 0, true);
|
||||
|
||||
ok(!isNodeVisible(propertiesList),
|
||||
"The list of properties panel is hidden again");
|
||||
ok(isNodeVisible(propertiesList),
|
||||
"The list of properties panel keeps");
|
||||
});
|
||||
|
||||
function hasExpectedProperties(containerEl) {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test for animations that have different starting time.
|
||||
// We should check progress indicator working well even if the start time is not zero.
|
||||
// Also, check that there is no duplication display.
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab(URL_ROOT + "doc_delayed_starttime_animations.html");
|
||||
const { panel } = yield openAnimationInspector();
|
||||
yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target2");
|
||||
yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target3");
|
||||
yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target4");
|
||||
yield setStyleAndWaitForAnimationSelecting(panel, "animation", "anim 100s", "#target5");
|
||||
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const detailsComponent = timelineComponent.details;
|
||||
const headers =
|
||||
detailsComponent.containerEl.querySelectorAll(".animated-properties-header");
|
||||
is(headers.length, 1, "There should be only one header in the details panel");
|
||||
|
||||
// Check indicator.
|
||||
yield clickOnAnimation(panel, 1);
|
||||
const progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
const startTime = detailsComponent.animation.state.previousStartTime;
|
||||
detailsComponent.indicateProgress(0);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The progress indicator position should be 0% at 0ms");
|
||||
detailsComponent.indicateProgress(startTime);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The progress indicator position should be 0% at start time");
|
||||
detailsComponent.indicateProgress(startTime + 50 * 1000);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The progress indicator position should be 50% at half time of animation");
|
||||
detailsComponent.indicateProgress(startTime + 99 * 1000);
|
||||
is(progressIndicatorEl.style.left, "99%",
|
||||
"The progress indicator position should be 99% at 99s");
|
||||
detailsComponent.indicateProgress(startTime + 100 * 1000);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The progress indicator position should be 0% at end of animation");
|
||||
});
|
||||
|
||||
function* setStyleAndWaitForAnimationSelecting(panel, name, value, selector) {
|
||||
const onSelecting = waitForAnimationSelecting(panel);
|
||||
yield setStyle(null, panel, name, value, selector);
|
||||
yield onSelecting;
|
||||
}
|
|
@ -0,0 +1,306 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Check animated properties's graph.
|
||||
// The graph constructs from SVG, also uses path (for shape), linearGradient,
|
||||
// stop (for color) element and so on.
|
||||
// We test followings.
|
||||
// 1. class name - which represents the animation type.
|
||||
// 2. coordinates of the path - x is time, y is graph y value which should be 0 - 1.
|
||||
// The path of animation types 'color', 'coord', 'opacity' or 'discrete' are created by
|
||||
// createPathSegments. Other types are created by createKeyframesPathSegments.
|
||||
// 3. color - animation type 'color' has linearGradient element.
|
||||
|
||||
requestLongerTimeout(5);
|
||||
|
||||
const TEST_CASES = [
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1, color: "rgb(0, 255, 0)" },
|
||||
{ x: 1000, y: 1, color: "rgb(255, 0, 0)" }
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 500, y: 1, color: "rgb(0, 0, 255)" },
|
||||
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 249.999, y: 0 },
|
||||
{ x: 250, y: 1 },
|
||||
{ x: 749.999, y: 1 },
|
||||
{ x: 750, y: 0 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 0 },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"background-color": {
|
||||
expectedClass: "color",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 499.999, y: 1, color: "rgb(255, 0, 0)" },
|
||||
{ x: 500, y: 1, color: "rgb(128, 128, 0)" },
|
||||
{ x: 999.999, y: 1, color: "rgb(128, 128, 0)" },
|
||||
{ x: 1000, y: 1, color: "rgb(0, 255, 0)" }
|
||||
]
|
||||
},
|
||||
"font-size": {
|
||||
expectedClass: "length",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 1000, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"margin-left": {
|
||||
expectedClass: "coord",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 999.999, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 999.999, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"text-align": {
|
||||
expectedClass: "discrete",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 499.999, y: 0 },
|
||||
{ x: 500, y: 1 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
},
|
||||
"transform": {
|
||||
expectedClass: "transform",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 1000, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"opacity": {
|
||||
expectedClass: "opacity",
|
||||
expectedValues: [
|
||||
{ x: 0, y: 0 },
|
||||
{ x: 500, y: 0.5 },
|
||||
{ x: 1000, y: 1 },
|
||||
]
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab(URL_ROOT + "doc_multiple_property_types.html");
|
||||
const {panel} = yield openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const detailEl = timelineComponent.details.containerEl;
|
||||
|
||||
for (let i = 0; i < TEST_CASES.length; i++) {
|
||||
info(`Click to select the animation[${ i }]`);
|
||||
yield clickOnAnimation(panel, i);
|
||||
const timeBlock = timelineComponent.timeBlocks[0];
|
||||
const state = timeBlock.animation.state;
|
||||
const properties = TEST_CASES[i];
|
||||
for (let property in properties) {
|
||||
const testcase = properties[property];
|
||||
info(`Test path of ${ property }`);
|
||||
const className = testcase.expectedClass;
|
||||
const pathEl = detailEl.querySelector(`path.${ className }`);
|
||||
ok(pathEl, `Path element with class '${ className }' should exis`);
|
||||
checkPathSegments(pathEl, state, testcase.expectedValues);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function checkPathSegments(pathEl, { duration }, expectedValues) {
|
||||
const pathSegList = pathEl.pathSegList;
|
||||
|
||||
const firstPathSeg = pathSegList.getItem(0);
|
||||
is(firstPathSeg.x, 0, "The x of first segment should be 0");
|
||||
is(firstPathSeg.y, 0, "The y of first segment should be 0");
|
||||
|
||||
expectedValues.forEach(expectedValue => {
|
||||
ok(hasSegment(pathSegList, expectedValue.x, expectedValue.y),
|
||||
`The path segment of x ${ expectedValue.x }, y ${ expectedValue.y } should exist`);
|
||||
|
||||
if (expectedValue.color) {
|
||||
checkColor(pathEl.closest("svg"), expectedValue.x / duration, expectedValue.color);
|
||||
}
|
||||
});
|
||||
|
||||
const closePathSeg = pathSegList.getItem(pathSegList.numberOfItems - 1);
|
||||
is(closePathSeg.pathSegType, closePathSeg.PATHSEG_CLOSEPATH,
|
||||
"The actual last segment should be close path");
|
||||
}
|
||||
|
||||
function checkColor(svgEl, offset, color) {
|
||||
const stopEl = findStopElement(svgEl, offset);
|
||||
ok(stopEl, `stop element at offset ${ offset } should exist`);
|
||||
is(stopEl.getAttribute("stop-color"), color,
|
||||
`stop-color of stop element at offset ${ offset } should be ${ color }`);
|
||||
}
|
||||
|
||||
function hasSegment(pathSegList, x, y) {
|
||||
for (let i = 1; i < pathSegList.numberOfItems - 1; i++) {
|
||||
const pathSeg = pathSegList.getItem(i);
|
||||
if (parseFloat(pathSeg.x.toFixed(3)) === x &&
|
||||
parseFloat(pathSeg.y.toFixed(6)) === y) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function findStopElement(svgEl, offset) {
|
||||
return [...svgEl.querySelectorAll("stop")].find(stopEl => {
|
||||
return stopEl.getAttribute("offset") == offset;
|
||||
});
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test progress indicator in animated properties.
|
||||
// Since this indicator works with the timeline, after selecting each animation,
|
||||
// click the timeline header to change the current time and check the change.
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab(URL_ROOT + "doc_multiple_property_types.html");
|
||||
const { panel } = yield openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const detailsComponent = timelineComponent.details;
|
||||
|
||||
info("Click to select the animation");
|
||||
yield clickOnAnimation(panel, 0);
|
||||
let progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
ok(progressIndicatorEl, "The progress indicator should be exist");
|
||||
yield clickOnTimelineHeader(panel, 0);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% at 0ms");
|
||||
yield clickOnTimelineHeader(panel, 0.5);
|
||||
approximate(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be "
|
||||
+ "approximately 50% at 500ms");
|
||||
yield clickOnTimelineHeader(panel, 1);
|
||||
is(progressIndicatorEl.style.left, "100%",
|
||||
"The left style of progress indicator element should be 100% at 1000ms");
|
||||
|
||||
info("Click to select the steps animation");
|
||||
yield clickOnAnimation(panel, 4);
|
||||
// Re-get progressIndicatorEl since this element re-create
|
||||
// in case of select the animation.
|
||||
progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
// Use indicateProgess directly from here since
|
||||
// MouseEvent.clientX may not be able to indicate finely
|
||||
// in case of the width of header element * xPositionRate has a fraction.
|
||||
detailsComponent.indicateProgress(499);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% at 0ms");
|
||||
detailsComponent.indicateProgress(499);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% at 499ms");
|
||||
detailsComponent.indicateProgress(500);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be 50% at 500ms");
|
||||
detailsComponent.indicateProgress(999);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be 50% at 999ms");
|
||||
yield clickOnTimelineHeader(panel, 1);
|
||||
is(progressIndicatorEl.style.left, "100%",
|
||||
"The left style of progress indicator element should be 100% at 1000ms");
|
||||
|
||||
info("Change the playback rate");
|
||||
yield changeTimelinePlaybackRate(panel, 2);
|
||||
yield clickOnAnimation(panel, 0);
|
||||
progressIndicatorEl = detailsComponent.progressIndicatorEl;
|
||||
yield clickOnTimelineHeader(panel, 0);
|
||||
is(progressIndicatorEl.style.left, "0%",
|
||||
"The left style of progress indicator element should be 0% "
|
||||
+ "at 0ms and playback rate 2");
|
||||
detailsComponent.indicateProgress(250);
|
||||
is(progressIndicatorEl.style.left, "50%",
|
||||
"The left style of progress indicator element should be 50% "
|
||||
+ "at 250ms and playback rate 2");
|
||||
detailsComponent.indicateProgress(500);
|
||||
is(progressIndicatorEl.style.left, "100%",
|
||||
"The left style of progress indicator element should be 100% "
|
||||
+ "at 500ms and playback rate 2");
|
||||
|
||||
info("Check the progress indicator position after select another animation");
|
||||
yield changeTimelinePlaybackRate(panel, 1);
|
||||
yield clickOnTimelineHeader(panel, 0.5);
|
||||
const originalIndicatorPosition = progressIndicatorEl.style.left;
|
||||
yield clickOnAnimation(panel, 1);
|
||||
is(progressIndicatorEl.style.left, originalIndicatorPosition,
|
||||
"The animation time should be continued even if another animation selects");
|
||||
});
|
||||
|
||||
function approximate(percentageString1, percentageString2, message) {
|
||||
const val1 = Math.round(parseFloat(percentageString1));
|
||||
const val2 = Math.round(parseFloat(percentageString2));
|
||||
is(val1, val2, message);
|
||||
}
|
|
@ -28,17 +28,18 @@ add_task(function* () {
|
|||
"The selected event was emitted with the right animation");
|
||||
ok(isTimeBlockSelected(timeline, 1),
|
||||
"The second time block has the right selected class");
|
||||
|
||||
info("Click again on the first animation and check if it unselects");
|
||||
yield clickOnAnimation(panel, 0, true);
|
||||
ok(!isTimeBlockSelected(timeline, 0),
|
||||
"The first time block has been unselected");
|
||||
|
||||
info("Click again on the first animation and check if it unselects");
|
||||
yield clickOnAnimation(panel, 0);
|
||||
ok(isTimeBlockSelected(timeline, 0),
|
||||
"The time block has the right selected class again");
|
||||
ok(!isTimeBlockSelected(timeline, 1),
|
||||
"The second time block has been unselected");
|
||||
});
|
||||
|
||||
function isTimeBlockSelected(timeline, index) {
|
||||
let animation = timeline.rootWrapperEl.querySelectorAll(".animation")[index];
|
||||
let animatedProperties = timeline.rootWrapperEl.querySelectorAll(
|
||||
".animated-properties")[index];
|
||||
return animation.classList.contains("selected") &&
|
||||
animatedProperties.classList.contains("selected");
|
||||
return animation.classList.contains("selected");
|
||||
}
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Tests the behavior of animation-detail container.
|
||||
// We test following cases.
|
||||
// 1. Existance of animation-detail element.
|
||||
// 2. Hidden at first if multiple animations were displayed.
|
||||
// 3. Display after click on an animation.
|
||||
// 4. Display from first time if displayed animation is only one.
|
||||
// 5. Close the animation-detail element by clicking on close button.
|
||||
// 6. Stay selected animation even if refresh all UI.
|
||||
|
||||
requestLongerTimeout(5);
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab(URL_ROOT + "doc_multiple_property_types.html");
|
||||
const { panel, inspector } = yield openAnimationInspector();
|
||||
const timelineComponent = panel.animationsTimelineComponent;
|
||||
const animationDetailEl =
|
||||
timelineComponent.rootWrapperEl.querySelector(".animation-detail");
|
||||
const splitboxControlledEl =
|
||||
timelineComponent.rootWrapperEl.querySelector(".controlled");
|
||||
|
||||
// 1. Existance of animation-detail element.
|
||||
ok(animationDetailEl, "The animation-detail element should exist");
|
||||
|
||||
// 2. Hidden at first if multiple animations were displayed.
|
||||
const win = animationDetailEl.ownerDocument.defaultView;
|
||||
is(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"The animation-detail element should be hidden at first "
|
||||
+ "if multiple animations were displayed");
|
||||
|
||||
// 3. Display after click on an animation.
|
||||
yield clickOnAnimation(panel, 0);
|
||||
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"The animation-detail element should be displayed after clicked on an animation");
|
||||
|
||||
// 4. Display from first time if displayed animation is only one.
|
||||
yield selectNodeAndWaitForAnimations("#target1", inspector);
|
||||
ok(animationDetailEl.querySelector(".property"),
|
||||
"The property in animation-detail element should be displayed");
|
||||
|
||||
// 5. Close the animation-detail element by clicking on close button.
|
||||
const previousHeight = animationDetailEl.offsetHeight;
|
||||
const button = animationDetailEl.querySelector(".animation-detail-header button");
|
||||
const onclosed = timelineComponent.once("animation-detail-closed");
|
||||
EventUtils.sendMouseEvent({type: "click"}, button, win);
|
||||
yield onclosed;
|
||||
is(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"animation-detail element should not display");
|
||||
|
||||
// Select another animation.
|
||||
yield selectNodeAndWaitForAnimations("#target2", inspector);
|
||||
isnot(win.getComputedStyle(splitboxControlledEl).display, "none",
|
||||
"animation-detail element should display");
|
||||
is(animationDetailEl.offsetHeight, previousHeight,
|
||||
"The height of animation-detail should keep the height");
|
||||
|
||||
// 6. Stay selected animation even if refresh all UI.
|
||||
yield selectNodeAndWaitForAnimations("#target1", inspector);
|
||||
yield clickTimelineRewindButton(panel);
|
||||
ok(animationDetailEl.querySelector(".property"),
|
||||
"The property in animation-detail element should stay as is");
|
||||
});
|
|
@ -1,52 +0,0 @@
|
|||
/* vim: set ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that animated properties' keyframes can be clicked, and that doing so
|
||||
// sets the current time in the timeline.
|
||||
|
||||
add_task(function* () {
|
||||
yield addTab(URL_ROOT + "doc_keyframes.html");
|
||||
let {panel} = yield openAnimationInspector();
|
||||
let timeline = panel.animationsTimelineComponent;
|
||||
let {scrubberEl} = timeline;
|
||||
|
||||
// XXX: The scrollbar is placed in the timeline in such a way that it causes
|
||||
// the animations to be slightly offset with the header when it appears.
|
||||
// So for now, let's hide the scrollbar. Bug 1229340 should fix this.
|
||||
timeline.animationsEl.style.overflow = "hidden";
|
||||
|
||||
info("Expand the animation");
|
||||
yield clickOnAnimation(panel, 0);
|
||||
|
||||
info("Click on the first keyframe of the first animated property");
|
||||
yield clickKeyframe(panel, 0, "background-color", 0);
|
||||
|
||||
info("Make sure the scrubber stopped moving and is at the right position");
|
||||
yield assertScrubberMoving(panel, false);
|
||||
checkScrubberPos(scrubberEl, 0);
|
||||
|
||||
info("Click on a keyframe in the middle");
|
||||
yield clickKeyframe(panel, 0, "transform", 2);
|
||||
|
||||
info("Make sure the scrubber is at the right position");
|
||||
checkScrubberPos(scrubberEl, 50);
|
||||
});
|
||||
|
||||
function* clickKeyframe(panel, animIndex, property, index) {
|
||||
let keyframeComponent = getKeyframeComponent(panel, animIndex, property);
|
||||
let keyframeEl = getKeyframeEl(panel, animIndex, property, index);
|
||||
|
||||
let onSelect = keyframeComponent.once("frame-selected");
|
||||
EventUtils.sendMouseEvent({type: "click"}, keyframeEl,
|
||||
keyframeEl.ownerDocument.defaultView);
|
||||
yield onSelect;
|
||||
}
|
||||
|
||||
function checkScrubberPos(scrubberEl, pos) {
|
||||
let newPos = Math.round(parseFloat(scrubberEl.style.left));
|
||||
let expectedPos = Math.round(pos);
|
||||
is(newPos, expectedPos, `The scrubber is at ${pos}%`);
|
||||
}
|
|
@ -26,8 +26,8 @@ add_task(function* () {
|
|||
let {panel} = yield openAnimationInspector();
|
||||
let timeline = panel.animationsTimelineComponent;
|
||||
|
||||
info("Expand the animation");
|
||||
yield clickOnAnimation(panel, 0);
|
||||
// doc_keyframes.html has only one animation.
|
||||
// So we don't need to click the animation since already the animation detail shown.
|
||||
|
||||
ok(timeline.rootWrapperEl.querySelectorAll(".frames .keyframes").length,
|
||||
"There are container elements for displaying keyframes");
|
||||
|
|
|
@ -34,14 +34,14 @@ add_task(function* () {
|
|||
let afterNode = getBodyChildNodeFront(1);
|
||||
|
||||
info("Select the ::before pseudo-element in the inspector");
|
||||
yield selectNode(beforeNode, inspector);
|
||||
yield selectNodeAndWaitForAnimations(beforeNode, inspector);
|
||||
is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
|
||||
is(timeline.targetNodes[0].previewer.nodeFront,
|
||||
inspector.selection.nodeFront,
|
||||
"The right node front is displayed in the timeline");
|
||||
|
||||
info("Select the ::after pseudo-element in the inspector");
|
||||
yield selectNode(afterNode, inspector);
|
||||
yield selectNodeAndWaitForAnimations(afterNode, inspector);
|
||||
is(timeline.timeBlocks.length, 1, "There is 1 animation in the timeline");
|
||||
is(timeline.targetNodes[0].previewer.nodeFront,
|
||||
inspector.selection.nodeFront,
|
||||
|
|
|
@ -45,6 +45,6 @@ function* testRefreshOnRemove(inspector, panel) {
|
|||
});
|
||||
yield onPanelUpdated;
|
||||
yield waitForAllAnimationTargets(panel);
|
||||
|
||||
yield waitForAnimationSelecting(panel);
|
||||
assertAnimationsDisplayed(panel, 1);
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ function* testRefresh(inspector, panel) {
|
|||
inspector.sidebar.select("ruleview");
|
||||
|
||||
info("Select the non animated node again");
|
||||
yield selectNodeAndWaitForAnimations(".still", inspector);
|
||||
yield selectNode(".still", inspector);
|
||||
|
||||
assertAnimationsDisplayed(panel, 1,
|
||||
"The panel still shows the previous animation data since it is inactive");
|
||||
|
|
|
@ -70,4 +70,5 @@ add_task(function* () {
|
|||
|
||||
yield onPanelUpdated;
|
||||
yield waitForAllAnimationTargets(panel);
|
||||
yield waitForAnimationSelecting(panel);
|
||||
});
|
||||
|
|
|
@ -11,7 +11,7 @@ add_task(function* () {
|
|||
let {panel} = yield openAnimationInspector();
|
||||
let timelineComponent = panel.animationsTimelineComponent;
|
||||
let timeBlockComponents = timelineComponent.timeBlocks;
|
||||
let detailsComponents = timelineComponent.details;
|
||||
let detailsComponent = timelineComponent.details;
|
||||
|
||||
for (let i = 0; i < timeBlockComponents.length; i++) {
|
||||
info(`Expand time block ${i} so its keyframes are visible`);
|
||||
|
@ -26,7 +26,7 @@ add_task(function* () {
|
|||
// Get the first set of keyframes (there's only one animated property
|
||||
// anyway), and the first frame element from there, we're only interested in
|
||||
// its offset.
|
||||
let keyframeComponent = detailsComponents[i].keyframeComponents[0];
|
||||
let keyframeComponent = detailsComponent.keyframeComponents[0];
|
||||
let frameEl = keyframeComponent.keyframesEl.querySelector(".frame");
|
||||
checkKeyframeOffset(containerEl, frameEl, state);
|
||||
}
|
||||
|
@ -61,11 +61,10 @@ function checkProgressAtStartingTime(el, { iterationStart }) {
|
|||
function checkKeyframeOffset(timeBlockEl, frameEl, {iterationStart}) {
|
||||
info("Check that the first keyframe is offset correctly");
|
||||
|
||||
let start = getIterationStartFromLeft(frameEl);
|
||||
is(start, iterationStart % 1, "The frame offset for iteration start");
|
||||
let start = getKeyframeOffset(frameEl);
|
||||
is(start, 0, "The frame offset for iteration start");
|
||||
}
|
||||
|
||||
function getIterationStartFromLeft(el) {
|
||||
let left = 100 - parseFloat(/(\d+)%/.exec(el.style.left)[1]);
|
||||
return left / 100;
|
||||
function getKeyframeOffset(el) {
|
||||
return parseFloat(/(\d+)%/.exec(el.style.left)[1]);
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ add_task(function* () {
|
|||
for (let i = 0; i < selectors.length; i++) {
|
||||
let selector = selectors[i];
|
||||
yield selectNode(selector, inspector);
|
||||
yield waitForAnimationSelecting(panel);
|
||||
let timelineEl = panel.animationsTimelineComponent.rootWrapperEl;
|
||||
let animationEl = timelineEl.querySelector(".animation");
|
||||
checkEndDelayAndName(animationEl);
|
||||
|
|
|
@ -39,5 +39,12 @@ add_task(function* () {
|
|||
"The name on the timeline is correct");
|
||||
ok(animationEl.querySelector("svg path"),
|
||||
"The timeline has svg and path element as summary graph");
|
||||
|
||||
const expectedBackgroundColor =
|
||||
i % 2 === 0 ? "rgba(128, 128, 128, 0.03)" : "rgba(0, 0, 0, 0)";
|
||||
const backgroundColor =
|
||||
animationEl.ownerDocument.defaultView.getComputedStyle(animationEl).backgroundColor;
|
||||
is(backgroundColor, expectedBackgroundColor,
|
||||
"The background-color shoud be changed to alternate");
|
||||
}
|
||||
});
|
||||
|
|
|
@ -17,9 +17,9 @@ add_task(function* () {
|
|||
yield selectNodeAndWaitForAnimations(".animated", inspector);
|
||||
|
||||
let animation = controller.animationPlayers[0];
|
||||
yield setStyle(animation, panel, "animationDuration", "5.5s");
|
||||
yield setStyle(animation, panel, "animationIterationCount", "300");
|
||||
yield setStyle(animation, panel, "animationDelay", "45s");
|
||||
yield setStyle(animation, panel, "animationDuration", "5.5s", ".animated");
|
||||
yield setStyle(animation, panel, "animationIterationCount", "300", ".animated");
|
||||
yield setStyle(animation, panel, "animationDelay", "45s", ".animated");
|
||||
|
||||
let animationsEl = panel.animationsTimelineComponent.animationsEl;
|
||||
let timeBlockEl = animationsEl.querySelector(".time-block");
|
||||
|
@ -34,20 +34,3 @@ add_task(function* () {
|
|||
is(Math.round(delayWidth * expectedTotalDuration / 100), 45 * 1000,
|
||||
"The timeline has the right delay");
|
||||
});
|
||||
|
||||
function* setStyle(animation, panel, name, value) {
|
||||
info("Change the animation style via the content DOM. Setting " +
|
||||
name + " to " + value);
|
||||
|
||||
let onAnimationChanged = once(animation, "changed");
|
||||
yield executeInContent("devtools:test:setStyle", {
|
||||
selector: ".animated",
|
||||
propertyName: name,
|
||||
propertyValue: value
|
||||
});
|
||||
yield onAnimationChanged;
|
||||
|
||||
// Also wait for the target node previews to be loaded if the panel got
|
||||
// refreshed as a result of this animation mutation.
|
||||
yield waitForAllAnimationTargets(panel);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border: 1px solid gray;
|
||||
}
|
||||
|
||||
#target1 {
|
||||
animation: anim 100s;
|
||||
}
|
||||
|
||||
@keyframes anim {
|
||||
from {
|
||||
transform: translate(-50%, 100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="target1"></div>
|
||||
<div id="target2"></div>
|
||||
<div id="target3"></div>
|
||||
<div id="target4"></div>
|
||||
<div id="target5"></div>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,95 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
div {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id=target1>1</div>
|
||||
<div id=target2>2</div>
|
||||
<div id=target3>3</div>
|
||||
<div id=target4>4</div>
|
||||
<div id=target5>5</div>
|
||||
|
||||
<script>
|
||||
"use strict";
|
||||
|
||||
const timing = {
|
||||
duration: 1000,
|
||||
fill: "forwards"
|
||||
};
|
||||
|
||||
document.querySelector("#target1").animate(
|
||||
[{ backgroundColor: "red",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" },
|
||||
{ backgroundColor: "lime",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" }], timing).pause();
|
||||
|
||||
document.querySelector("#target2").animate(
|
||||
[{ backgroundColor: "lime",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" },
|
||||
{ backgroundColor: "red",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" }], timing).pause();
|
||||
|
||||
document.querySelector("#target3").animate(
|
||||
[{ backgroundColor: "red",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" },
|
||||
{ backgroundColor: "blue",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" },
|
||||
{ backgroundColor: "lime",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)" }], timing).pause();
|
||||
|
||||
document.querySelector("#target4").animate(
|
||||
[{ backgroundColor: "red",
|
||||
fontSize: "10px",
|
||||
marginLeft: "0px",
|
||||
opacity: 0,
|
||||
textAlign: "right",
|
||||
transform: "translate(0px)",
|
||||
easing: "steps(2)" },
|
||||
{ backgroundColor: "lime",
|
||||
fontSize: "20px",
|
||||
marginLeft: "100px",
|
||||
opacity: 1,
|
||||
textAlign: "center",
|
||||
transform: "translate(100px)" }], timing).pause();
|
||||
|
||||
timing.easing = "steps(2)";
|
||||
document.querySelector("#target5").animate(
|
||||
[{ opacity: 0 }, { opacity: 1 }], timing).pause();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
|
@ -93,6 +93,11 @@ var selectNodeAndWaitForAnimations = Task.async(
|
|||
// be properly displayed (wait for all target DOM nodes to be previewed).
|
||||
let {AnimationsPanel} = inspector.sidebar.getWindowForTab(TAB_NAME);
|
||||
yield waitForAllAnimationTargets(AnimationsPanel);
|
||||
|
||||
if (AnimationsPanel.animationsTimelineComponent.animations.length === 1) {
|
||||
// Wait for selecting the animation since there is only one animation.
|
||||
yield waitForAnimationSelecting(AnimationsPanel);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -159,6 +164,11 @@ var openAnimationInspector = Task.async(function* () {
|
|||
// animations displayed.
|
||||
yield waitForAllAnimationTargets(AnimationsPanel);
|
||||
|
||||
if (AnimationsPanel.animationsTimelineComponent.animations.length === 1) {
|
||||
// Wait for selecting the animation since there is only one animation.
|
||||
yield waitForAnimationSelecting(AnimationsPanel);
|
||||
}
|
||||
|
||||
return {
|
||||
toolbox: toolbox,
|
||||
inspector: inspector,
|
||||
|
@ -350,6 +360,37 @@ function* changeTimelinePlaybackRate(panel, rate) {
|
|||
win.document.querySelector("#timeline-toolbar"), {type: "mousemove"}, win);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for animation selecting.
|
||||
* @param {AnimationsPanel} panel
|
||||
*/
|
||||
function* waitForAnimationSelecting(panel) {
|
||||
yield panel.animationsTimelineComponent.once("animation-selected");
|
||||
}
|
||||
|
||||
/**
|
||||
+ * Click the timeline header to update the animation current time.
|
||||
+ * @param {AnimationsPanel} panel
|
||||
+ * @param {Number} x position rate on timeline header.
|
||||
+ * This method calculates
|
||||
+ * `position * offsetWidth + offsetLeft of timeline header`
|
||||
+ * as the clientX of MouseEvent.
|
||||
+ * This parameter should be from 0.0 to 1.0.
|
||||
+ */
|
||||
function* clickOnTimelineHeader(panel, position) {
|
||||
const timeline = panel.animationsTimelineComponent;
|
||||
const onTimelineDataChanged = timeline.once("timeline-data-changed");
|
||||
|
||||
const header = timeline.timeHeaderEl;
|
||||
const clientX = header.offsetLeft + header.offsetWidth * position;
|
||||
EventUtils.sendMouseEvent({ type: "mousedown", clientX: clientX },
|
||||
header, header.ownerDocument.defaultView);
|
||||
info(`Click at (${ clientX }, 0) on timeline header`);
|
||||
EventUtils.sendMouseEvent({ type: "mouseup", clientX: clientX }, header,
|
||||
header.ownerDocument.defaultView);
|
||||
return yield onTimelineDataChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent the toolbox common highlighter from making backend requests.
|
||||
* @param {Toolbox} toolbox
|
||||
|
@ -369,43 +410,35 @@ function disableHighlighter(toolbox) {
|
|||
* Click on an animation in the timeline to select/unselect it.
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @param {Number} index The index of the animation to click on.
|
||||
* @param {Boolean} shouldClose Set to true if clicking should close the
|
||||
* animation.
|
||||
* @param {Boolean} shouldAlreadySelected Set to true
|
||||
* if the clicked animation is already selected.
|
||||
* @return {Promise} resolves to the animation whose state has changed.
|
||||
*/
|
||||
function* clickOnAnimation(panel, index, shouldClose) {
|
||||
function* clickOnAnimation(panel, index, shouldAlreadySelected) {
|
||||
let timeline = panel.animationsTimelineComponent;
|
||||
|
||||
// Expect a selection event.
|
||||
let onSelectionChanged = timeline.once(shouldClose
|
||||
? "animation-unselected"
|
||||
let onSelectionChanged = timeline.once(shouldAlreadySelected
|
||||
? "animation-already-selected"
|
||||
: "animation-selected");
|
||||
|
||||
// If we're opening the animation, also wait for the keyframes-retrieved
|
||||
// event.
|
||||
let onReady = shouldClose
|
||||
? Promise.resolve()
|
||||
: timeline.details[index].once("keyframes-retrieved");
|
||||
|
||||
info("Click on animation " + index + " in the timeline");
|
||||
let timeBlock = timeline.rootWrapperEl.querySelectorAll(".time-block")[index];
|
||||
EventUtils.sendMouseEvent({type: "click"}, timeBlock,
|
||||
timeBlock.ownerDocument.defaultView);
|
||||
|
||||
yield onReady;
|
||||
return yield onSelectionChanged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an instance of the Keyframes component from the timeline.
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @param {Number} animationIndex The index of the animation in the timeline.
|
||||
* @param {String} propertyName The name of the animated property.
|
||||
* @return {Keyframes} The Keyframes component instance.
|
||||
*/
|
||||
function getKeyframeComponent(panel, animationIndex, propertyName) {
|
||||
function getKeyframeComponent(panel, propertyName) {
|
||||
let timeline = panel.animationsTimelineComponent;
|
||||
let detailsComponent = timeline.details[animationIndex];
|
||||
let detailsComponent = timeline.details;
|
||||
return detailsComponent.keyframeComponents
|
||||
.find(c => c.propertyName === propertyName);
|
||||
}
|
||||
|
@ -413,14 +446,37 @@ function getKeyframeComponent(panel, animationIndex, propertyName) {
|
|||
/**
|
||||
* Get a keyframe element from the timeline.
|
||||
* @param {AnimationsPanel} panel The panel instance.
|
||||
* @param {Number} animationIndex The index of the animation in the timeline.
|
||||
* @param {String} propertyName The name of the animated property.
|
||||
* @param {Index} keyframeIndex The index of the keyframe.
|
||||
* @return {DOMNode} The keyframe element.
|
||||
*/
|
||||
function getKeyframeEl(panel, animationIndex, propertyName, keyframeIndex) {
|
||||
let keyframeComponent = getKeyframeComponent(panel, animationIndex,
|
||||
propertyName);
|
||||
function getKeyframeEl(panel, propertyName, keyframeIndex) {
|
||||
let keyframeComponent = getKeyframeComponent(panel, propertyName);
|
||||
return keyframeComponent.keyframesEl
|
||||
.querySelectorAll(".frame")[keyframeIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Set style to test document.
|
||||
* @param {Animation} animation - animation object.
|
||||
* @param {AnimationsPanel} panel - The panel instance.
|
||||
* @param {String} name - property name.
|
||||
* @param {String} value - property value.
|
||||
* @param {String} selector - selector for test document.
|
||||
*/
|
||||
function* setStyle(animation, panel, name, value, selector) {
|
||||
info("Change the animation style via the content DOM. Setting " +
|
||||
name + " to " + value + " of " + selector);
|
||||
|
||||
const onAnimationChanged = animation ? once(animation, "changed") : Promise.resolve();
|
||||
yield executeInContent("devtools:test:setStyle", {
|
||||
selector: selector,
|
||||
propertyName: name,
|
||||
propertyValue: value
|
||||
});
|
||||
yield onAnimationChanged;
|
||||
|
||||
// Also wait for the target node previews to be loaded if the panel got
|
||||
// refreshed as a result of this animation mutation.
|
||||
yield waitForAllAnimationTargets(panel);
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
var Cu = Components.utils;
|
||||
const {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
const {getCssPropertyName} = require("devtools/client/animationinspector/components/animation-details");
|
||||
const {getCssPropertyName} = require("devtools/client/animationinspector/utils");
|
||||
|
||||
const TEST_DATA = [{
|
||||
jsName: "alllowercase",
|
||||
|
|
|
@ -20,6 +20,9 @@ const OPTIMAL_TIME_INTERVAL_MULTIPLES = [1, 2.5, 5];
|
|||
|
||||
const MILLIS_TIME_FORMAT_MAX_DURATION = 4000;
|
||||
|
||||
// SVG namespace
|
||||
const SVG_NS = "http://www.w3.org/2000/svg";
|
||||
|
||||
/**
|
||||
* DOM node creation helper function.
|
||||
* @param {Object} Options to customize the node to be created.
|
||||
|
@ -57,6 +60,22 @@ function createNode(options) {
|
|||
|
||||
exports.createNode = createNode;
|
||||
|
||||
/**
|
||||
* SVG DOM node creation helper function.
|
||||
* @param {Object} Options to customize the node to be created.
|
||||
* - nodeType {String} Optional, defaults to "div",
|
||||
* - attributes {Object} Optional attributes object like
|
||||
* {attrName1:value1, attrName2: value2, ...}
|
||||
* - parent {DOMNode} Mandatory node to append the newly created node to.
|
||||
* - textContent {String} Optional text for the node.
|
||||
* @return {DOMNode} The newly created node.
|
||||
*/
|
||||
function createSVGNode(options) {
|
||||
options.namespace = SVG_NS;
|
||||
return createNode(options);
|
||||
}
|
||||
exports.createSVGNode = createSVGNode;
|
||||
|
||||
/**
|
||||
* Find the optimal interval between time graduations in the animation timeline
|
||||
* graph based on a minimum time interval
|
||||
|
@ -273,3 +292,55 @@ var TimeScale = {
|
|||
};
|
||||
|
||||
exports.TimeScale = TimeScale;
|
||||
|
||||
/**
|
||||
* Convert given CSS property name to JavaScript CSS name.
|
||||
* @param {String} CSS property name (e.g. background-color).
|
||||
* @return {String} JavaScript CSS property name (e.g. backgroundColor).
|
||||
*/
|
||||
function getJsPropertyName(cssPropertyName) {
|
||||
if (cssPropertyName == "float") {
|
||||
return "cssFloat";
|
||||
}
|
||||
// https://drafts.csswg.org/cssom/#css-property-to-idl-attribute
|
||||
return cssPropertyName.replace(/-([a-z])/gi, (str, group) => {
|
||||
return group.toUpperCase();
|
||||
});
|
||||
}
|
||||
exports.getJsPropertyName = getJsPropertyName;
|
||||
|
||||
/**
|
||||
* Turn propertyName into property-name.
|
||||
* @param {String} jsPropertyName A camelcased CSS property name. Typically
|
||||
* something that comes out of computed styles. E.g. borderBottomColor
|
||||
* @return {String} The corresponding CSS property name: border-bottom-color
|
||||
*/
|
||||
function getCssPropertyName(jsPropertyName) {
|
||||
return jsPropertyName.replace(/[A-Z]/g, "-$&").toLowerCase();
|
||||
}
|
||||
exports.getCssPropertyName = getCssPropertyName;
|
||||
|
||||
/**
|
||||
* Get a formatted title for this animation. This will be either:
|
||||
* "some-name", "some-name : CSS Transition", "some-name : CSS Animation",
|
||||
* "some-name : Script Animation", or "Script Animation", depending
|
||||
* if the server provides the type, what type it is and if the animation
|
||||
* has a name
|
||||
* @param {AnimationPlayerFront} animation
|
||||
*/
|
||||
function getFormattedAnimationTitle({state}) {
|
||||
// Older servers don't send a type, and only know about
|
||||
// CSSAnimations and CSSTransitions, so it's safe to use
|
||||
// just the name.
|
||||
if (!state.type) {
|
||||
return state.name;
|
||||
}
|
||||
|
||||
// Script-generated animations may not have a name.
|
||||
if (state.type === "scriptanimation" && !state.name) {
|
||||
return L10N.getStr("timeline.scriptanimation.unnamedLabel");
|
||||
}
|
||||
|
||||
return L10N.getFormatStr(`timeline.${state.type}.nameLabel`, state.name);
|
||||
}
|
||||
exports.getFormattedAnimationTitle = getFormattedAnimationTitle;
|
||||
|
|
|
@ -96,6 +96,6 @@ skip-if = os == "mac" && os_version == "10.8" || os == "win" && os_version == "5
|
|||
[browser_two_tabs.js]
|
||||
# We want these tests to run for mochitest-dt as well, so we include them here:
|
||||
[../../../../browser/base/content/test/static/browser_parsable_css.js]
|
||||
skip-if = debug # no point in running on both opt and debug, and will likely intermittently timeout on debug
|
||||
skip-if = debug || asan # no point in running on both opt and debug, and will likely intermittently timeout on debug
|
||||
[../../../../browser/base/content/test/static/browser_all_files_referenced.js]
|
||||
skip-if = debug # no point in running on both opt and debug, and will likely intermittently timeout on debug
|
||||
skip-if = debug || asan # no point in running on both opt and debug, and will likely intermittently timeout on debug
|
||||
|
|
|
@ -60,20 +60,14 @@ module.exports = createClass({
|
|||
className: "boxmodel-properties-header",
|
||||
onDoubleClick: this.onToggleExpander,
|
||||
},
|
||||
dom.div(
|
||||
dom.span(
|
||||
{
|
||||
className: "boxmodel-properties-expander theme-twisty",
|
||||
open: this.state.isOpen,
|
||||
onClick: this.onToggleExpander,
|
||||
}
|
||||
),
|
||||
dom.span(
|
||||
{
|
||||
className: "boxmodel-properties-label",
|
||||
title: BOXMODEL_L10N.getStr("boxmodel.propertiesLabel"),
|
||||
},
|
||||
BOXMODEL_L10N.getStr("boxmodel.propertiesLabel")
|
||||
)
|
||||
BOXMODEL_L10N.getStr("boxmodel.propertiesLabel")
|
||||
),
|
||||
dom.div(
|
||||
{
|
||||
|
|
|
@ -56,7 +56,9 @@ module.exports = createClass({
|
|||
dom.ul(
|
||||
{},
|
||||
dom.li(
|
||||
{},
|
||||
{
|
||||
className: "grid-settings-item",
|
||||
},
|
||||
dom.label(
|
||||
{},
|
||||
dom.input(
|
||||
|
@ -70,7 +72,9 @@ module.exports = createClass({
|
|||
)
|
||||
),
|
||||
dom.li(
|
||||
{},
|
||||
{
|
||||
className: "grid-settings-item",
|
||||
},
|
||||
dom.label(
|
||||
{},
|
||||
dom.input(
|
||||
|
|
|
@ -81,6 +81,8 @@ module.exports = createClass({
|
|||
preview: {
|
||||
attributes: attributesMap,
|
||||
attributesLength: attributes.length,
|
||||
// All the grid containers are assumed to be in the DOM tree.
|
||||
isConnected: true,
|
||||
// nodeName is already lowerCased in Node grips
|
||||
nodeName: nodeFront.nodeName.toLowerCase(),
|
||||
nodeType: nodeFront.nodeType,
|
||||
|
@ -116,9 +118,7 @@ module.exports = createClass({
|
|||
let { nodeFront } = grid;
|
||||
|
||||
return dom.li(
|
||||
{
|
||||
className: "grid-item",
|
||||
},
|
||||
{},
|
||||
dom.label(
|
||||
{},
|
||||
dom.input(
|
||||
|
|
|
@ -52,6 +52,7 @@ const App = createClass({
|
|||
return dom.div(
|
||||
{
|
||||
id: "layout-container",
|
||||
className: "devtools-monospace",
|
||||
},
|
||||
Accordion({
|
||||
items: [
|
||||
|
|
|
@ -16,20 +16,20 @@ add_task(function* () {
|
|||
let target = img.editor.getAttributeElement("src").querySelector(".link");
|
||||
|
||||
info("Check that the src attribute of the image is a valid tooltip target");
|
||||
let isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target);
|
||||
ok(isValid, "The element is a valid tooltip target");
|
||||
yield assertTooltipShownOnHover(markup.imagePreviewTooltip, target);
|
||||
yield assertTooltipHiddenOnMouseOut(markup.imagePreviewTooltip, target);
|
||||
|
||||
info("Start dragging the test div");
|
||||
yield simulateNodeDrag(inspector, "div");
|
||||
|
||||
info("Now check that the src attribute of the image isn't a valid target");
|
||||
isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target);
|
||||
let isValid = yield markup.imagePreviewTooltip._toggle.isValidHoverTarget(target);
|
||||
ok(!isValid, "The element is not a valid tooltip target");
|
||||
|
||||
info("Stop dragging the test div");
|
||||
yield simulateNodeDrop(inspector, "div");
|
||||
|
||||
info("Check again the src attribute of the image");
|
||||
isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, target);
|
||||
ok(isValid, "The element is a valid tooltip target");
|
||||
yield assertTooltipShownOnHover(markup.imagePreviewTooltip, target);
|
||||
yield assertTooltipHiddenOnMouseOut(markup.imagePreviewTooltip, target);
|
||||
});
|
||||
|
|
|
@ -23,8 +23,9 @@ add_task(function* () {
|
|||
|
||||
for (let testNode of TEST_NODES) {
|
||||
let target = yield getImageTooltipTarget(testNode, inspector);
|
||||
yield assertTooltipShownOn(target, inspector);
|
||||
yield assertTooltipShownOnHover(inspector.markup.imagePreviewTooltip, target);
|
||||
checkImageTooltip(testNode, inspector);
|
||||
yield assertTooltipHiddenOnMouseOut(inspector.markup.imagePreviewTooltip, target);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -41,12 +42,6 @@ function* getImageTooltipTarget({selector}, inspector) {
|
|||
return target;
|
||||
}
|
||||
|
||||
function* assertTooltipShownOn(element, {markup}) {
|
||||
info("Is the element a valid hover target");
|
||||
let isValid = yield isHoverTooltipTarget(markup.imagePreviewTooltip, element);
|
||||
ok(isValid, "The element is a valid hover target for the image tooltip");
|
||||
}
|
||||
|
||||
function checkImageTooltip({selector, size}, {markup}) {
|
||||
let panel = markup.imagePreviewTooltip.panel;
|
||||
let images = panel.getElementsByTagName("img");
|
||||
|
|
|
@ -37,10 +37,12 @@ add_task(function* () {
|
|||
ok(target, "Found the src attribute in the markup view.");
|
||||
|
||||
info("Showing tooltip on the src link.");
|
||||
yield isHoverTooltipTarget(inspector.markup.imagePreviewTooltip, target);
|
||||
yield assertTooltipShownOnHover(inspector.markup.imagePreviewTooltip, target);
|
||||
|
||||
checkImageTooltip(INITIAL_SRC_SIZE, inspector);
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(inspector.markup.imagePreviewTooltip, target);
|
||||
|
||||
info("Updating the image src.");
|
||||
yield updateImageSrc(img, UPDATED_SRC, inspector);
|
||||
|
||||
|
@ -48,10 +50,12 @@ add_task(function* () {
|
|||
ok(target, "Found the src attribute in the markup view after mutation.");
|
||||
|
||||
info("Showing tooltip on the src link.");
|
||||
yield isHoverTooltipTarget(inspector.markup.imagePreviewTooltip, target);
|
||||
yield assertTooltipShownOnHover(inspector.markup.imagePreviewTooltip, target);
|
||||
|
||||
info("Checking that the new image was shown.");
|
||||
checkImageTooltip(UPDATED_SRC_SIZE, inspector);
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(inspector.markup.imagePreviewTooltip, target);
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
|
@ -75,6 +75,10 @@ function* checkNode(inspector, testActor, {desc, selector, inline, value}) {
|
|||
if (inline) {
|
||||
textContainer = container.elt.querySelector("pre");
|
||||
ok(!!textContainer, "Text container is already rendered for inline text elements");
|
||||
ok(textContainer.parentNode.previousSibling.classList.contains("open"),
|
||||
"Text container is after the open tag");
|
||||
ok(textContainer.parentNode.nextSibling.classList.contains("close"),
|
||||
"Text container is before the close tag");
|
||||
} else {
|
||||
textContainer = container.elt.querySelector("pre");
|
||||
ok(!textContainer, "Text container is not rendered for collapsed text nodes");
|
||||
|
|
|
@ -271,8 +271,7 @@ ElementEditor.prototype = {
|
|||
// This editor won't receive an update automatically, so we rely on
|
||||
// child text editors to let us know that we need updating.
|
||||
this.textEditor = new TextEditor(this.container, node, "text");
|
||||
this.elt.insertBefore(this.textEditor.elt,
|
||||
this.elt.firstChild.nextSibling.nextSibling);
|
||||
this.elt.insertBefore(this.textEditor.elt, this.elt.querySelector(".close"));
|
||||
}
|
||||
|
||||
if (this.textEditor) {
|
||||
|
|
|
@ -27,11 +27,9 @@ add_task(function* () {
|
|||
});
|
||||
|
||||
function* testImageTooltipAfterColorChange(swatch, url, ruleView) {
|
||||
let previewTooltip = ruleView.tooltips.getTooltip("previewTooltip");
|
||||
info("First, verify that the image preview tooltip works");
|
||||
let anchor = yield isHoverTooltipTarget(previewTooltip, url);
|
||||
ok(anchor, "The image preview tooltip is shown on the url span");
|
||||
is(anchor, url, "The anchor returned by the showOnHover callback is correct");
|
||||
let previewTooltip = yield assertShowPreviewTooltip(ruleView, url);
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, url);
|
||||
|
||||
info("Open the color picker tooltip and change the color");
|
||||
let picker = ruleView.tooltips.getTooltip("colorPicker");
|
||||
|
@ -57,7 +55,7 @@ function* testImageTooltipAfterColorChange(swatch, url, ruleView) {
|
|||
// dom node
|
||||
url = getRuleViewProperty(ruleView, "body", "background").valueSpan
|
||||
.querySelector(".theme-link");
|
||||
anchor = yield isHoverTooltipTarget(previewTooltip, url);
|
||||
ok(anchor, "The image preview tooltip is shown on the url span");
|
||||
is(anchor, url, "The anchor returned by the showOnHover callback is correct");
|
||||
previewTooltip = yield assertShowPreviewTooltip(ruleView, url);
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, url);
|
||||
}
|
||||
|
|
|
@ -53,15 +53,13 @@ function* testColorChangeIsntRevertedWhenOtherTooltipIsShown(ruleView) {
|
|||
info("Open the image preview tooltip");
|
||||
let value = getRuleViewProperty(ruleView, "body", "background").valueSpan;
|
||||
let url = value.querySelector(".theme-link");
|
||||
let previewTooltip = ruleView.tooltips.getTooltip("previewTooltip");
|
||||
let onShown = previewTooltip.once("shown");
|
||||
let anchor = yield isHoverTooltipTarget(previewTooltip, url);
|
||||
previewTooltip.show(anchor);
|
||||
yield onShown;
|
||||
let previewTooltip = yield assertShowPreviewTooltip(ruleView, url);
|
||||
|
||||
info("Image tooltip is shown, verify that the swatch is still correct");
|
||||
swatch = value.querySelector(".ruleview-colorswatch");
|
||||
is(swatch.style.backgroundColor, "black",
|
||||
"The swatch's color is correct");
|
||||
is(swatch.nextSibling.textContent, "black", "The color name is correct");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, url);
|
||||
}
|
||||
|
|
|
@ -38,9 +38,11 @@ add_task(function* () {
|
|||
let onHidden = colorPicker.tooltip.once("hidden");
|
||||
// Hiding the color picker refreshes the value.
|
||||
let onRuleViewChanged = view.once("ruleview-changed");
|
||||
yield assertHoverTooltipOn(view.tooltips.getTooltip("previewTooltip"), uriSpan);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(view, uriSpan);
|
||||
yield onHidden;
|
||||
yield onRuleViewChanged;
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, uriSpan);
|
||||
|
||||
ok(true, "The color picker closed when the image preview tooltip appeared");
|
||||
});
|
||||
|
|
|
@ -52,39 +52,35 @@ add_task(function* () {
|
|||
|
||||
function* testBodyRuleView(view) {
|
||||
info("Testing tooltips in the rule view");
|
||||
let panel = view.tooltips.getTooltip("previewTooltip").panel;
|
||||
|
||||
// Check that the rule view has a tooltip and that a XUL panel has
|
||||
// been created
|
||||
ok(view.tooltips.getTooltip("previewTooltip"), "Tooltip instance exists");
|
||||
ok(panel, "XUL panel exists");
|
||||
|
||||
// Get the background-image property inside the rule view
|
||||
let {valueSpan} = getRuleViewProperty(view, "body", "background-image");
|
||||
let uriSpan = valueSpan.querySelector(".theme-link");
|
||||
|
||||
yield assertHoverTooltipOn(view.tooltips.getTooltip("previewTooltip"), uriSpan);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(view, uriSpan);
|
||||
|
||||
let images = panel.getElementsByTagName("img");
|
||||
let images = previewTooltip.panel.getElementsByTagName("img");
|
||||
is(images.length, 1, "Tooltip contains an image");
|
||||
ok(images[0].getAttribute("src")
|
||||
.indexOf("iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHe") !== -1,
|
||||
"The image URL seems fine");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, uriSpan);
|
||||
}
|
||||
|
||||
function* testDivRuleView(view) {
|
||||
let panel = view.tooltips.getTooltip("previewTooltip").panel;
|
||||
|
||||
// Get the background property inside the rule view
|
||||
let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
|
||||
let uriSpan = valueSpan.querySelector(".theme-link");
|
||||
|
||||
yield assertHoverTooltipOn(view.tooltips.getTooltip("previewTooltip"), uriSpan);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(view, uriSpan);
|
||||
|
||||
let images = panel.getElementsByTagName("img");
|
||||
let images = previewTooltip.panel.getElementsByTagName("img");
|
||||
is(images.length, 1, "Tooltip contains an image");
|
||||
ok(images[0].getAttribute("src").startsWith("data:"),
|
||||
"Tooltip contains a data-uri image as expected");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, uriSpan);
|
||||
}
|
||||
|
||||
function* testTooltipAppearsEvenInEditMode(view) {
|
||||
|
@ -94,10 +90,13 @@ function* testTooltipAppearsEvenInEditMode(view) {
|
|||
info("Now trying to show the preview tooltip");
|
||||
let {valueSpan} = getRuleViewProperty(view, ".test-element", "background");
|
||||
let uriSpan = valueSpan.querySelector(".theme-link");
|
||||
yield assertHoverTooltipOn(view.tooltips.getTooltip("previewTooltip"), uriSpan);
|
||||
|
||||
let previewTooltip = yield assertShowPreviewTooltip(view, uriSpan);
|
||||
|
||||
is(view.styleDocument.activeElement, editor.input,
|
||||
"Tooltip was shown in edit mode, and inplace-editor still focused");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, uriSpan);
|
||||
}
|
||||
|
||||
function turnToEditMode(ruleView) {
|
||||
|
@ -106,20 +105,19 @@ function turnToEditMode(ruleView) {
|
|||
}
|
||||
|
||||
function* testComputedView(view) {
|
||||
let tooltip = view.tooltips.getTooltip("previewTooltip");
|
||||
ok(tooltip, "The computed-view has a tooltip defined");
|
||||
|
||||
let panel = tooltip.panel;
|
||||
ok(panel, "The computed-view tooltip has a XUL panel");
|
||||
|
||||
let {valueSpan} = getComputedViewProperty(view, "background-image");
|
||||
let uriSpan = valueSpan.querySelector(".theme-link");
|
||||
|
||||
yield assertHoverTooltipOn(view.tooltips.getTooltip("previewTooltip"), uriSpan);
|
||||
// Scroll to ensure the line is visible as we see the box model by default
|
||||
valueSpan.scrollIntoView();
|
||||
|
||||
let images = panel.getElementsByTagName("img");
|
||||
let previewTooltip = yield assertShowPreviewTooltip(view, uriSpan);
|
||||
|
||||
let images = previewTooltip.panel.getElementsByTagName("img");
|
||||
is(images.length, 1, "Tooltip contains an image");
|
||||
|
||||
ok(images[0].getAttribute("src").startsWith("data:"),
|
||||
"Tooltip contains a data-uri in the computed-view too");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, uriSpan);
|
||||
}
|
||||
|
|
|
@ -49,7 +49,7 @@ function* testRuleView(ruleView, nodeFront) {
|
|||
"font-family");
|
||||
|
||||
// And verify that the tooltip gets shown on this property
|
||||
yield assertHoverTooltipOn(tooltip, valueSpan);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(ruleView, valueSpan);
|
||||
|
||||
let images = panel.getElementsByTagName("img");
|
||||
is(images.length, 1, "Tooltip contains an image");
|
||||
|
@ -59,6 +59,8 @@ function* testRuleView(ruleView, nodeFront) {
|
|||
let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
|
||||
is(images[0].getAttribute("src"), dataURL,
|
||||
"Tooltip contains the correct data-uri image");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, valueSpan);
|
||||
}
|
||||
|
||||
function* testComputedView(computedView, nodeFront) {
|
||||
|
@ -68,7 +70,7 @@ function* testComputedView(computedView, nodeFront) {
|
|||
let panel = tooltip.panel;
|
||||
let {valueSpan} = getComputedViewProperty(computedView, "font-family");
|
||||
|
||||
yield assertHoverTooltipOn(tooltip, valueSpan);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(computedView, valueSpan);
|
||||
|
||||
let images = panel.getElementsByTagName("img");
|
||||
is(images.length, 1, "Tooltip contains an image");
|
||||
|
@ -78,6 +80,8 @@ function* testComputedView(computedView, nodeFront) {
|
|||
let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
|
||||
is(images[0].getAttribute("src"), dataURL,
|
||||
"Tooltip contains the correct data-uri image");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, valueSpan);
|
||||
}
|
||||
|
||||
function* testExpandedComputedViewProperty(computedView, nodeFront) {
|
||||
|
@ -95,7 +99,7 @@ function* testExpandedComputedViewProperty(computedView, nodeFront) {
|
|||
let tooltip = computedView.tooltips.getTooltip("previewTooltip");
|
||||
let panel = tooltip.panel;
|
||||
|
||||
yield assertHoverTooltipOn(tooltip, valueSpan);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(computedView, valueSpan);
|
||||
|
||||
let images = panel.getElementsByTagName("img");
|
||||
is(images.length, 1, "Tooltip contains an image");
|
||||
|
@ -105,6 +109,8 @@ function* testExpandedComputedViewProperty(computedView, nodeFront) {
|
|||
let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
|
||||
is(images[0].getAttribute("src"), dataURL,
|
||||
"Tooltip contains the correct data-uri image");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, valueSpan);
|
||||
}
|
||||
|
||||
function getPropertyView(computedView, name) {
|
||||
|
|
|
@ -51,13 +51,17 @@ function* performChecks(view, propertyValue) {
|
|||
}
|
||||
|
||||
let links = propertyValue.querySelectorAll(".theme-link");
|
||||
let panel = view.tooltips.getTooltip("previewTooltip").panel;
|
||||
|
||||
info("Checking first link tooltip");
|
||||
yield assertHoverTooltipOn(view.tooltips.getTooltip("previewTooltip"), links[0]);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(view, links[0]);
|
||||
let panel = view.tooltips.getTooltip("previewTooltip").panel;
|
||||
checkTooltip(panel, YELLOW_DOT);
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, links[0]);
|
||||
|
||||
info("Checking second link tooltip");
|
||||
yield assertHoverTooltipOn(view.tooltips.getTooltip("previewTooltip"), links[1]);
|
||||
previewTooltip = yield assertShowPreviewTooltip(view, links[1]);
|
||||
checkTooltip(panel, BLUE_DOT);
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, links[1]);
|
||||
}
|
||||
|
|
|
@ -45,7 +45,7 @@ function* testRuleView(ruleView, nodeFront) {
|
|||
.querySelector(".ruleview-computed .ruleview-propertyvalue");
|
||||
|
||||
// And verify that the tooltip gets shown on this property
|
||||
yield assertHoverTooltipOn(tooltip, valueSpan);
|
||||
let previewTooltip = yield assertShowPreviewTooltip(ruleView, valueSpan);
|
||||
|
||||
let images = panel.getElementsByTagName("img");
|
||||
is(images.length, 1, "Tooltip contains an image");
|
||||
|
@ -55,4 +55,6 @@ function* testRuleView(ruleView, nodeFront) {
|
|||
let dataURL = yield getFontFamilyDataURL(valueSpan.textContent, nodeFront);
|
||||
is(images[0].getAttribute("src"), dataURL,
|
||||
"Tooltip contains the correct data-uri image");
|
||||
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, valueSpan);
|
||||
}
|
||||
|
|
|
@ -35,12 +35,7 @@ function* testImageDimension(ruleView) {
|
|||
|
||||
// Make sure there is a hover tooltip for this property, this also will fill
|
||||
// the tooltip with its content
|
||||
yield assertHoverTooltipOn(tooltip, uriSpan);
|
||||
|
||||
info("Showing the tooltip");
|
||||
let onShown = tooltip.once("shown");
|
||||
tooltip.show(uriSpan);
|
||||
yield onShown;
|
||||
let previewTooltip = yield assertShowPreviewTooltip(ruleView, uriSpan);
|
||||
|
||||
// Let's not test for a specific size, but instead let's make sure it's at
|
||||
// least as big as the image
|
||||
|
@ -52,9 +47,7 @@ function* testImageDimension(ruleView) {
|
|||
ok(panelRect.height >= imageRect.height,
|
||||
"The panel is high enough to show the image");
|
||||
|
||||
let onHidden = tooltip.once("hidden");
|
||||
tooltip.hide();
|
||||
yield onHidden;
|
||||
yield assertTooltipHiddenOnMouseOut(previewTooltip, uriSpan);
|
||||
}
|
||||
|
||||
function* testPickerDimension(ruleView) {
|
||||
|
|
|
@ -644,33 +644,83 @@ function synthesizeKeys(input, win) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Given a tooltip object instance (see Tooltip.js), checks if it is set to
|
||||
* toggle and hover and if so, checks if the given target is a valid hover
|
||||
* target. This won't actually show the tooltip (the less we interact with XUL
|
||||
* panels during test runs, the better).
|
||||
* Given a Tooltip instance, fake a mouse event on the `target` DOM Element
|
||||
* and assert that the `tooltip` is correctly displayed.
|
||||
*
|
||||
* @return a promise that resolves when the answer is known
|
||||
* @param {Tooltip} tooltip
|
||||
* The tooltip instance
|
||||
* @param {DOMElement} target
|
||||
* The DOM Element on which a tooltip should appear
|
||||
*
|
||||
* @return a promise that resolves with the tooltip object
|
||||
*/
|
||||
function isHoverTooltipTarget(tooltip, target) {
|
||||
if (!tooltip._toggle._baseNode || !tooltip.panel) {
|
||||
return promise.reject(new Error(
|
||||
"The tooltip passed isn't set to toggle on hover or is not a tooltip"));
|
||||
function* assertTooltipShownOnHover(tooltip, target) {
|
||||
let mouseEvent = new target.ownerDocument.defaultView.MouseEvent("mousemove", {
|
||||
bubbles: true,
|
||||
});
|
||||
target.dispatchEvent(mouseEvent);
|
||||
|
||||
if (!tooltip.isVisible()) {
|
||||
info("Waiting for tooltip to be shown");
|
||||
yield tooltip.once("shown");
|
||||
}
|
||||
return tooltip._toggle.isValidHoverTarget(target);
|
||||
|
||||
ok(tooltip.isVisible(), `The tooltip is visible`);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as isHoverTooltipTarget except that it will fail the test if there is no
|
||||
* tooltip defined on hover of the given element
|
||||
* Given an inspector `view` object, fake a mouse event on the `target` DOM
|
||||
* Element and assert that the preview tooltip is correctly displayed.
|
||||
*
|
||||
* @return a promise
|
||||
* @param {CssRuleView|ComputedView|...} view
|
||||
* The instance of an inspector panel
|
||||
* @param {DOMElement} target
|
||||
* The DOM Element on which a tooltip should appear
|
||||
*
|
||||
* @return a promise that resolves with the tooltip object
|
||||
*/
|
||||
function assertHoverTooltipOn(tooltip, element) {
|
||||
return isHoverTooltipTarget(tooltip, element).then(() => {
|
||||
ok(true, "A tooltip is defined on hover of the given element");
|
||||
}, () => {
|
||||
ok(false, "No tooltip is defined on hover of the given element");
|
||||
function* assertShowPreviewTooltip(view, target) {
|
||||
let mouseEvent = new target.ownerDocument.defaultView.MouseEvent("mousemove", {
|
||||
bubbles: true,
|
||||
});
|
||||
target.dispatchEvent(mouseEvent);
|
||||
|
||||
let name = "previewTooltip";
|
||||
ok(view.tooltips._instances.has(name),
|
||||
`Tooltip '${name}' has been instanciated`);
|
||||
let tooltip = view.tooltips.getTooltip(name);
|
||||
|
||||
if (!tooltip.isVisible()) {
|
||||
info("Waiting for tooltip to be shown");
|
||||
yield tooltip.once("shown");
|
||||
}
|
||||
|
||||
ok(tooltip.isVisible(), `The tooltip '${name}' is visible`);
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `tooltip` instance, fake a mouse event on `target` DOM element
|
||||
* and check that the tooltip correctly disappear.
|
||||
*
|
||||
* @param {Tooltip} tooltip
|
||||
* The tooltip instance
|
||||
* @param {DOMElement} target
|
||||
* The DOM Element on which a tooltip should appear
|
||||
*/
|
||||
function* assertTooltipHiddenOnMouseOut(tooltip, target) {
|
||||
let mouseEvent = new target.ownerDocument.defaultView.MouseEvent("mouseout", {
|
||||
bubbles: true,
|
||||
relatedTarget: target
|
||||
});
|
||||
target.dispatchEvent(mouseEvent);
|
||||
|
||||
yield tooltip.once("hidden");
|
||||
|
||||
ok(!tooltip.isVisible(), "The tooltip is hidden on mouseout");
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -172,3 +172,17 @@ timeline.scriptanimation.unnamedLabel=Script Animation
|
|||
# This can happen if devtools couldn't figure out the type of the animation.
|
||||
# %S will be replaced by the name of the transition at run-time.
|
||||
timeline.unknown.nameLabel=%S
|
||||
|
||||
# LOCALIZATION NOTE (detail.propertiesHeader.percentage):
|
||||
# This string is displayed on header label in .animated-properties-header.
|
||||
# %S represents the value in percentage with two decimal points, localized.
|
||||
# there are two "%" after %S to escape and display "%"
|
||||
detail.propertiesHeader.percentage=%S%%
|
||||
|
||||
# LOCALIZATION NOTE (detail.headerTitle):
|
||||
# This string is displayed on header label in .animation-detail-header.
|
||||
detail.headerTitle=Animated properties for
|
||||
|
||||
# LOCALIZATION NOTE (detail.header.closeLabel):
|
||||
# This string is displayed in a tooltip of close button for animated properties
|
||||
detail.header.closeLabel=Close animated properties panel
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
var Cu = Components.utils;
|
||||
var {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
|
||||
var {CubicBezier, _parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
|
||||
var {CubicBezier, parseTimingFunction} = require("devtools/client/shared/widgets/CubicBezierWidget");
|
||||
|
||||
function run_test() {
|
||||
throwsWhenMissingCoordinates();
|
||||
|
@ -112,22 +112,22 @@ function testParseTimingFunction() {
|
|||
do_print("test parseTimingFunction");
|
||||
|
||||
for (let test of ["ease", "linear", "ease-in", "ease-out", "ease-in-out"]) {
|
||||
ok(_parseTimingFunction(test), test);
|
||||
ok(parseTimingFunction(test), test);
|
||||
}
|
||||
|
||||
ok(!_parseTimingFunction("something"), "non-function token");
|
||||
ok(!_parseTimingFunction("something()"), "non-cubic-bezier function");
|
||||
ok(!_parseTimingFunction("cubic-bezier(something)",
|
||||
ok(!parseTimingFunction("something"), "non-function token");
|
||||
ok(!parseTimingFunction("something()"), "non-cubic-bezier function");
|
||||
ok(!parseTimingFunction("cubic-bezier(something)",
|
||||
"cubic-bezier with non-numeric argument"));
|
||||
ok(!_parseTimingFunction("cubic-bezier(1,2,3:7)",
|
||||
ok(!parseTimingFunction("cubic-bezier(1,2,3:7)",
|
||||
"did not see comma"));
|
||||
ok(!_parseTimingFunction("cubic-bezier(1,2,3,7:",
|
||||
"did not see close paren"));
|
||||
ok(!_parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
|
||||
ok(!_parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
|
||||
deepEqual(_parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
|
||||
ok(!parseTimingFunction("cubic-bezier(1,2,3,7:",
|
||||
"did not see close paren"));
|
||||
ok(!parseTimingFunction("cubic-bezier(1,2", "early EOF after number"));
|
||||
ok(!parseTimingFunction("cubic-bezier(1,2,", "early EOF after comma"));
|
||||
deepEqual(parseTimingFunction("cubic-bezier(1,2,3,7)"), [1, 2, 3, 7],
|
||||
"correct invocation");
|
||||
deepEqual(_parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
|
||||
deepEqual(parseTimingFunction("cubic-bezier(1, /* */ 2,3, 7 )"),
|
||||
[1, 2, 3, 7],
|
||||
"correct with comments and whitespace");
|
||||
}
|
||||
|
|
Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше
Загрузка…
Ссылка в новой задаче