MozReview-Commit-ID: 8Oqr9Nbqsn0
This commit is contained in:
Kartikaya Gupta 2017-04-19 07:02:32 -04:00
Родитель e61c3ec040 284cbb308e
Коммит 1be864c6ad
451 изменённых файлов: 80002 добавлений и 33274 удалений

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

@ -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:

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

@ -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="&notificationsPolicyLearnMore.label;"/>
</hbox>
<hbox pack="end">
<button id="notificationsPolicyButton" label="&notificationsPolicyButton.label;"
<button id="notificationsPolicyButton"
class="accessory-button"
label="&notificationsPolicyButton.label;"
accesskey="&notificationsPolicyButton.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();
}
};

80
browser/extensions/e10srollout/bootstrap.js поставляемый
Просмотреть файл

@ -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");
}

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше