Update On Tue Aug 30 20:54:07 CEST 2022
This commit is contained in:
parent
88f9da8c3b
commit
432ab9058e
185 changed files with 4425 additions and 1780 deletions
|
@ -31,6 +31,10 @@ class CacheDomain {
|
|||
static constexpr uint64_t Viewport = ((uint64_t)0x1) << 14;
|
||||
static constexpr uint64_t ARIA = ((uint64_t)0x1) << 15;
|
||||
static constexpr uint64_t Relations = ((uint64_t)0x1) << 16;
|
||||
#ifdef XP_WIN
|
||||
// Used for MathML.
|
||||
static constexpr uint64_t InnerHTML = ((uint64_t)0x1) << 17;
|
||||
#endif
|
||||
static constexpr uint64_t All = ~((uint64_t)0x0);
|
||||
};
|
||||
|
||||
|
|
|
@ -1367,6 +1367,10 @@ void LocalAccessible::DOMAttributeChanged(int32_t aNameSpaceID,
|
|||
SendCache(CacheDomain::Actions, CacheUpdateType::Update);
|
||||
}
|
||||
|
||||
if (aAttribute == nsGkAtoms::href) {
|
||||
mDoc->QueueCacheUpdate(this, CacheDomain::Value);
|
||||
}
|
||||
|
||||
if (aAttribute == nsGkAtoms::aria_controls ||
|
||||
aAttribute == nsGkAtoms::aria_flowto) {
|
||||
mDoc->QueueCacheUpdate(this, CacheDomain::Relations);
|
||||
|
@ -2498,6 +2502,20 @@ void LocalAccessible::BindToParent(LocalAccessible* aParent,
|
|||
// for the next tick before the cache update is sent.
|
||||
mDoc->QueueCacheUpdate(aParent, CacheDomain::Table);
|
||||
}
|
||||
|
||||
#if defined(XP_WIN)
|
||||
if (StaticPrefs::accessibility_cache_enabled_AtStartup() &&
|
||||
aParent->HasOwnContent() && aParent->mContent->IsMathMLElement()) {
|
||||
// For any change in a MathML subtree, update the innerHTML cache on the
|
||||
// root math element.
|
||||
for (LocalAccessible* acc = aParent; acc; acc = acc->LocalParent()) {
|
||||
if (acc->HasOwnContent() &&
|
||||
acc->mContent->IsMathMLElement(nsGkAtoms::math)) {
|
||||
mDoc->QueueCacheUpdate(acc, CacheDomain::InnerHTML);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif // defined(XP_WIN)
|
||||
}
|
||||
|
||||
// LocalAccessible protected
|
||||
|
@ -3149,6 +3167,7 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
|
|||
// 1. Accessible is an HTML input type that holds a number.
|
||||
// 2. Accessible has a numeric value and an aria-valuetext.
|
||||
// 3. Accessible is an HTML input type that holds text.
|
||||
// 4. Accessible is a link, in which case value is the target URL.
|
||||
// ... for all other cases we divine the value remotely.
|
||||
bool cacheValueText = false;
|
||||
if (HasNumericValue()) {
|
||||
|
@ -3161,7 +3180,7 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
|
|||
mContent->AsElement()->HasAttr(
|
||||
kNameSpaceID_None, nsGkAtoms::aria_valuetext));
|
||||
} else {
|
||||
cacheValueText = IsTextField();
|
||||
cacheValueText = IsTextField() || IsHTMLLink();
|
||||
}
|
||||
|
||||
if (cacheValueText) {
|
||||
|
@ -3593,6 +3612,15 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
|
|||
}
|
||||
}
|
||||
|
||||
#if defined(XP_WIN)
|
||||
if (aCacheDomain & CacheDomain::InnerHTML && HasOwnContent() &&
|
||||
mContent->IsMathMLElement(nsGkAtoms::math)) {
|
||||
nsString innerHTML;
|
||||
mContent->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors());
|
||||
fields->SetAttribute(nsGkAtoms::html, std::move(innerHTML));
|
||||
}
|
||||
#endif // defined(XP_WIN)
|
||||
|
||||
if (aUpdateType == CacheUpdateType::Initial) {
|
||||
// Add fields which never change and thus only need to be included in the
|
||||
// initial cache push.
|
||||
|
|
|
@ -276,6 +276,17 @@ void RemoteAccessibleBase<Derived>::Value(nsString& aValue) const {
|
|||
if (option) {
|
||||
option->Name(aValue);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (IsTextLeaf() || IsImage()) {
|
||||
if (const Accessible* actionAcc = ActionAncestor()) {
|
||||
if (const_cast<Accessible*>(actionAcc)->State() & states::LINKED) {
|
||||
// Text and image descendants of links expose the link URL as the
|
||||
// value.
|
||||
return actionAcc->Value(aValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -408,6 +408,9 @@ class RemoteAccessibleBase : public Accessible, public HyperTextAccessibleBase {
|
|||
friend HyperTextAccessibleBase;
|
||||
friend class xpcAccessible;
|
||||
friend class CachedTableCellAccessible;
|
||||
#ifdef XP_WIN
|
||||
friend class sdnAccessible;
|
||||
#endif
|
||||
|
||||
nsTArray<Derived*> mChildren;
|
||||
DocAccessibleParent* mDoc;
|
||||
|
|
|
@ -21,6 +21,8 @@ skip-if =
|
|||
[browser_caching_attributes.js]
|
||||
[browser_caching_description.js]
|
||||
[browser_caching_document_props.js]
|
||||
[browser_caching_innerHTML.js]
|
||||
skip-if = os != 'win'
|
||||
[browser_caching_name.js]
|
||||
skip-if = (os == "linux" && bits == 64) || (debug && os == "mac") || (debug && os == "win") #Bug 1388256
|
||||
[browser_caching_relations.js]
|
||||
|
|
55
accessible/tests/browser/e10s/browser_caching_innerHTML.js
Normal file
55
accessible/tests/browser/e10s/browser_caching_innerHTML.js
Normal file
|
@ -0,0 +1,55 @@
|
|||
/* 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";
|
||||
|
||||
/**
|
||||
* Test caching of innerHTML on math elements for Windows clients.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`
|
||||
<p id="p">test</p>
|
||||
<math id="math"><mfrac><mi>x</mi><mi>y</mi></mfrac></math>
|
||||
`,
|
||||
async function(browser, docAcc) {
|
||||
if (!isCacheEnabled) {
|
||||
// Stop the harness from complaining that this file is empty when run with
|
||||
// the cache disabled.
|
||||
todo(false, "Cache disabled for a cache only test");
|
||||
return;
|
||||
}
|
||||
|
||||
const p = findAccessibleChildByID(docAcc, "p");
|
||||
let hasHtml;
|
||||
try {
|
||||
p.cache.getStringProperty("html");
|
||||
hasHtml = true;
|
||||
} catch (e) {
|
||||
hasHtml = false;
|
||||
}
|
||||
ok(!hasHtml, "p doesn't have cached html");
|
||||
|
||||
const math = findAccessibleChildByID(docAcc, "math");
|
||||
is(
|
||||
math.cache.getStringProperty("html"),
|
||||
"<mfrac><mi>x</mi><mi>y</mi></mfrac>",
|
||||
"math cached html is correct"
|
||||
);
|
||||
|
||||
info("Mutating math");
|
||||
await invokeContentTask(browser, [], () => {
|
||||
content.document.querySelectorAll("mi")[1].textContent = "z";
|
||||
});
|
||||
await untilCacheIs(
|
||||
() => math.cache.getStringProperty("html"),
|
||||
"<mfrac><mi>x</mi><mi>z</mi></mfrac>",
|
||||
"math cached html is correct after mutation"
|
||||
);
|
||||
},
|
||||
{
|
||||
topLevel: true,
|
||||
iframe: isCacheEnabled,
|
||||
remoteIframe: isCacheEnabled,
|
||||
}
|
||||
);
|
|
@ -212,3 +212,37 @@ addAccessibleTask(
|
|||
},
|
||||
{ iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
||||
/**
|
||||
* Test caching of link URL values.
|
||||
*/
|
||||
addAccessibleTask(
|
||||
`<a id="link" href="https://example.com/">Test</a>`,
|
||||
async function(browser, docAcc) {
|
||||
const link = findAccessibleChildByID(docAcc, "link");
|
||||
is(link.value, "https://example.com/", "link initial value correct");
|
||||
const textLeaf = link.firstChild;
|
||||
is(textLeaf.value, "https://example.com/", "link initial value correct");
|
||||
|
||||
info("Changing link href");
|
||||
await invokeSetAttribute(browser, "link", "href", "https://example.net/");
|
||||
await untilCacheIs(
|
||||
() => link.value,
|
||||
"https://example.net/",
|
||||
"link value correct after change"
|
||||
);
|
||||
|
||||
info("Removing link href");
|
||||
await invokeSetAttribute(browser, "link", "href");
|
||||
await untilCacheIs(() => link.value, "", "link value empty after removal");
|
||||
|
||||
info("Setting link href");
|
||||
await invokeSetAttribute(browser, "link", "href", "https://example.com/");
|
||||
await untilCacheIs(
|
||||
() => link.value,
|
||||
"https://example.com/",
|
||||
"link value correct after change"
|
||||
);
|
||||
},
|
||||
{ chrome: true, topLevel: true, iframe: true, remoteIframe: true }
|
||||
);
|
||||
|
|
|
@ -862,8 +862,8 @@ MsaaAccessible::QueryInterface(REFIID iid, void** ppv) {
|
|||
return E_NOINTERFACE;
|
||||
}
|
||||
*ppv = static_cast<IEnumVARIANT*>(new ChildrenEnumVariant(this));
|
||||
} else if (IID_ISimpleDOMNode == iid && localAcc) {
|
||||
if (!localAcc->HasOwnContent() && !localAcc->IsDoc()) {
|
||||
} else if (IID_ISimpleDOMNode == iid) {
|
||||
if (mAcc->IsDoc() || (localAcc && !localAcc->HasOwnContent())) {
|
||||
return E_NOINTERFACE;
|
||||
}
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ namespace mozilla {
|
|||
namespace a11y {
|
||||
|
||||
inline DocAccessible* sdnAccessible::GetDocument() const {
|
||||
MOZ_ASSERT(mNode);
|
||||
return GetExistingDocAccessible(mNode->OwnerDoc());
|
||||
}
|
||||
|
||||
|
|
|
@ -85,6 +85,10 @@ sdnAccessible::get_nodeInfo(BSTR __RPC_FAR* aNodeName,
|
|||
*aNodeType = 0;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
uint16_t nodeType = mNode->NodeType();
|
||||
*aNodeType = static_cast<unsigned short>(nodeType);
|
||||
|
@ -133,6 +137,10 @@ sdnAccessible::get_attributes(unsigned short aMaxAttribs,
|
|||
*aNumAttribs = 0;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
if (!mNode->IsElement()) return S_FALSE;
|
||||
|
||||
|
@ -167,6 +175,13 @@ sdnAccessible::get_attributesForNames(unsigned short aMaxAttribs,
|
|||
if (!aAttribNames || !aNameSpaceID || !aAttribValues) return E_INVALIDARG;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
// NVDA expects this to succeed for MathML and won't call innerHTML if this
|
||||
// fails. Therefore, return S_FALSE here instead of E_NOTIMPL, indicating
|
||||
// that the attributes aren't present.
|
||||
return S_FALSE;
|
||||
}
|
||||
|
||||
if (!mNode->IsElement()) return S_FALSE;
|
||||
|
||||
|
@ -205,6 +220,10 @@ sdnAccessible::get_computedStyle(
|
|||
return E_INVALIDARG;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
*aNumStyleProperties = 0;
|
||||
|
||||
|
@ -248,6 +267,10 @@ sdnAccessible::get_computedStyleForProperties(
|
|||
if (!aStyleProperties || !aStyleValues) return E_INVALIDARG;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
if (mNode->IsDocument()) return S_FALSE;
|
||||
|
||||
|
@ -271,10 +294,16 @@ sdnAccessible::get_computedStyleForProperties(
|
|||
// XXX Use MOZ_CAN_RUN_SCRIPT_BOUNDARY for now due to bug 1543294.
|
||||
MOZ_CAN_RUN_SCRIPT_BOUNDARY STDMETHODIMP
|
||||
sdnAccessible::scrollTo(boolean aScrollTopLeft) {
|
||||
DocAccessible* document = GetDocument();
|
||||
if (!document) // that's IsDefunct check
|
||||
if (IsDefunct()) {
|
||||
return CO_E_OBJNOTCONNECTED;
|
||||
}
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
DocAccessible* document = GetDocument();
|
||||
MOZ_ASSERT(document);
|
||||
if (!mNode->IsContent()) return S_FALSE;
|
||||
|
||||
uint32_t scrollType = aScrollTopLeft
|
||||
|
@ -293,6 +322,10 @@ sdnAccessible::get_parentNode(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
|
|||
*aNode = nullptr;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
nsINode* resultNode = mNode->GetParentNode();
|
||||
if (resultNode) {
|
||||
|
@ -309,6 +342,10 @@ sdnAccessible::get_firstChild(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
|
|||
*aNode = nullptr;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
nsINode* resultNode = mNode->GetFirstChild();
|
||||
if (resultNode) {
|
||||
|
@ -325,6 +362,10 @@ sdnAccessible::get_lastChild(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
|
|||
*aNode = nullptr;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
nsINode* resultNode = mNode->GetLastChild();
|
||||
if (resultNode) {
|
||||
|
@ -341,6 +382,10 @@ sdnAccessible::get_previousSibling(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
|
|||
*aNode = nullptr;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
nsINode* resultNode = mNode->GetPreviousSibling();
|
||||
if (resultNode) {
|
||||
|
@ -357,6 +402,10 @@ sdnAccessible::get_nextSibling(ISimpleDOMNode __RPC_FAR* __RPC_FAR* aNode) {
|
|||
*aNode = nullptr;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
nsINode* resultNode = mNode->GetNextSibling();
|
||||
if (resultNode) {
|
||||
|
@ -374,6 +423,10 @@ sdnAccessible::get_childAt(unsigned aChildIndex,
|
|||
*aNode = nullptr;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
nsINode* resultNode = mNode->GetChildAt_Deprecated(aChildIndex);
|
||||
if (resultNode) {
|
||||
|
@ -391,10 +444,21 @@ sdnAccessible::get_innerHTML(BSTR __RPC_FAR* aInnerHTML) {
|
|||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
|
||||
if (!mNode->IsElement()) return S_FALSE;
|
||||
|
||||
nsAutoString innerHTML;
|
||||
mNode->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors());
|
||||
if (!mNode) {
|
||||
RemoteAccessible* remoteAcc = mMsaa->Acc()->AsRemote();
|
||||
MOZ_ASSERT(remoteAcc);
|
||||
if (!remoteAcc->mCachedFields) {
|
||||
return S_FALSE;
|
||||
}
|
||||
remoteAcc->mCachedFields->GetAttribute(nsGkAtoms::html, innerHTML);
|
||||
} else {
|
||||
if (!mNode->IsElement()) {
|
||||
return S_FALSE;
|
||||
}
|
||||
mNode->AsElement()->GetInnerHTML(innerHTML, IgnoreErrors());
|
||||
}
|
||||
|
||||
if (innerHTML.IsEmpty()) return S_FALSE;
|
||||
|
||||
*aInnerHTML = ::SysAllocStringLen(innerHTML.get(), innerHTML.Length());
|
||||
|
@ -422,6 +486,10 @@ sdnAccessible::get_language(BSTR __RPC_FAR* aLanguage) {
|
|||
*aLanguage = nullptr;
|
||||
|
||||
if (IsDefunct()) return CO_E_OBJNOTCONNECTED;
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return E_NOTIMPL;
|
||||
}
|
||||
|
||||
nsAutoString language;
|
||||
if (mNode->IsContent())
|
||||
|
|
|
@ -25,18 +25,32 @@ class sdnAccessible final : public ISimpleDOMNode {
|
|||
if (!mNode) MOZ_CRASH();
|
||||
}
|
||||
|
||||
explicit sdnAccessible(NotNull<MsaaAccessible*> aMsaa)
|
||||
: mNode(aMsaa->LocalAcc()->GetNode()), mMsaa(aMsaa) {}
|
||||
explicit sdnAccessible(NotNull<MsaaAccessible*> aMsaa) : mMsaa(aMsaa) {
|
||||
Accessible* acc = aMsaa->Acc();
|
||||
MOZ_ASSERT(acc);
|
||||
if (LocalAccessible* localAcc = acc->AsLocal()) {
|
||||
mNode = localAcc->GetNode();
|
||||
}
|
||||
}
|
||||
|
||||
~sdnAccessible();
|
||||
|
||||
/**
|
||||
* Return if the object is defunct.
|
||||
*/
|
||||
bool IsDefunct() const { return !GetDocument(); }
|
||||
bool IsDefunct() const {
|
||||
if (mMsaa && !mMsaa->Acc()) {
|
||||
return true;
|
||||
}
|
||||
if (!mNode) {
|
||||
MOZ_ASSERT(mMsaa && mMsaa->Acc()->IsRemote());
|
||||
return false;
|
||||
}
|
||||
return !GetDocument();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a document accessible it belongs to if any.
|
||||
* Return a local document accessible it belongs to if any.
|
||||
*/
|
||||
DocAccessible* GetDocument() const;
|
||||
|
||||
|
@ -122,6 +136,8 @@ class sdnAccessible final : public ISimpleDOMNode {
|
|||
/* [out][retval] */ BSTR __RPC_FAR* aLanguage);
|
||||
|
||||
private:
|
||||
// mNode will be null for a RemoteAccessible. In that case, we only partially
|
||||
// implement this interface using data from the RemoteAccessible cache.
|
||||
nsCOMPtr<nsINode> mNode;
|
||||
RefPtr<MsaaAccessible> mMsaa;
|
||||
Maybe<uint32_t> mUniqueId;
|
||||
|
|
|
@ -139,6 +139,12 @@ var gTabsPanel = {
|
|||
},
|
||||
|
||||
showAllTabsPanel(event) {
|
||||
// Note that event may be null.
|
||||
|
||||
// Only space and enter should open the popup, ignore other keypresses:
|
||||
if (event?.type == "keypress" && event.key != "Enter" && event.key != " ") {
|
||||
return;
|
||||
}
|
||||
this.init();
|
||||
if (this.canOpen) {
|
||||
PanelUI.showSubView(
|
||||
|
|
|
@ -167,7 +167,7 @@ var SidebarUI = {
|
|||
// support live switching the app locale. Reload the entire sidebar to
|
||||
// invalidate any old text.
|
||||
this.hide();
|
||||
this._show(this.lastOpenedId);
|
||||
this.showInitially(this.lastOpenedId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -92,6 +92,7 @@
|
|||
<toolbarbutton id="alltabs-button"
|
||||
class="toolbarbutton-1 chromeclass-toolbar-additional tabs-alltabs-button"
|
||||
badged="true"
|
||||
onkeypress="gTabsPanel.showAllTabsPanel(event);"
|
||||
onmousedown="gTabsPanel.showAllTabsPanel(event);"
|
||||
data-l10n-id="tabs-toolbar-list-all-tabs"
|
||||
removable="false"/>
|
||||
|
|
|
@ -2324,6 +2324,10 @@ class nsContextMenu {
|
|||
getImageText() {
|
||||
let dialogBox = gBrowser.getTabDialogBox(this.browser);
|
||||
const imageTextResult = this.actor.getImageText(this.targetIdentifier);
|
||||
TelemetryStopwatch.start(
|
||||
"TEXT_RECOGNITION_API_PERFORMANCE",
|
||||
imageTextResult
|
||||
);
|
||||
const { dialog } = dialogBox.open(
|
||||
"chrome://browser/content/textrecognition/textrecognition.html",
|
||||
{
|
||||
|
|
|
@ -54,3 +54,58 @@ add_task(async function test_bookmarks_sidebar() {
|
|||
add_task(async function test_history_sidebar() {
|
||||
await testLiveReloading("viewHistorySidebar");
|
||||
});
|
||||
|
||||
add_task(async function test_ext_sidebar_panel_reloaded_on_locale_changes() {
|
||||
let extension = ExtensionTestUtils.loadExtension({
|
||||
manifest: {
|
||||
sidebar_action: {
|
||||
default_panel: "sidebar.html",
|
||||
},
|
||||
},
|
||||
useAddonManager: "temporary",
|
||||
|
||||
files: {
|
||||
"sidebar.html": `<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<script src="sidebar.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
A Test Sidebar
|
||||
</body>
|
||||
</html>`,
|
||||
"sidebar.js": function() {
|
||||
const { browser } = this;
|
||||
window.onload = () => {
|
||||
browser.test.sendMessage("sidebar");
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
await extension.startup();
|
||||
// Test sidebar is opened on install
|
||||
await extension.awaitMessage("sidebar");
|
||||
|
||||
// Test sidebar is opened on simulated locale changes.
|
||||
info("Switch browser to bidi and expect the sidebar panel to be reloaded");
|
||||
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["intl.l10n.pseudo", "bidi"]],
|
||||
});
|
||||
await extension.awaitMessage("sidebar");
|
||||
is(
|
||||
window.document.documentElement.getAttribute("dir"),
|
||||
"rtl",
|
||||
"browser window changed direction to rtl as expected"
|
||||
);
|
||||
|
||||
await SpecialPowers.popPrefEnv();
|
||||
await extension.awaitMessage("sidebar");
|
||||
is(
|
||||
window.document.documentElement.getAttribute("dir"),
|
||||
"ltr",
|
||||
"browser window changed direction to ltr as expected"
|
||||
);
|
||||
|
||||
await extension.unload();
|
||||
});
|
||||
|
|
|
@ -59,6 +59,7 @@ support-files = tab_that_closes.html
|
|||
[browser_multiselect_tabs_move_to_another_window_drag.js]
|
||||
[browser_multiselect_tabs_move_to_new_window_contextmenu.js]
|
||||
https_first_disabled = true
|
||||
[browser_tab_manager_keyboard_access.js]
|
||||
[browser_tab_play.js]
|
||||
[browser_multiselect_tabs_move.js]
|
||||
[browser_multiselect_tabs_mute_unmute.js]
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Check we can open the tab manager using the keyboard.
|
||||
* Note that navigation to buttons in the toolbar is covered
|
||||
* by other tests.
|
||||
*/
|
||||
add_task(async function test_open_tabmanager_keyboard() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [["browser.tabs.tabmanager.enabled", true]],
|
||||
});
|
||||
let newWindow = await BrowserTestUtils.openNewWindowWithFlushedCacheForMozSupports();
|
||||
let elem = newWindow.document.getElementById("alltabs-button");
|
||||
|
||||
// Borrowed from forceFocus() in the keyboard directory head.js
|
||||
elem.setAttribute("tabindex", "-1");
|
||||
elem.focus();
|
||||
elem.removeAttribute("tabindex");
|
||||
|
||||
let focused = BrowserTestUtils.waitForEvent(newWindow, "focus", true);
|
||||
EventUtils.synthesizeKey(" ", {}, newWindow);
|
||||
let event = await focused;
|
||||
ok(
|
||||
event.originalTarget.closest("#allTabsMenu-allTabsView"),
|
||||
"Focus inside all tabs menu after toolbar button pressed"
|
||||
);
|
||||
let hidden = BrowserTestUtils.waitForEvent(
|
||||
event.target.closest("panel"),
|
||||
"popuphidden"
|
||||
);
|
||||
EventUtils.synthesizeKey("KEY_Escape", { shiftKey: false }, newWindow);
|
||||
await hidden;
|
||||
await BrowserTestUtils.closeWindow(newWindow);
|
||||
});
|
|
@ -345,7 +345,7 @@ const TestCasesMultipleFiles = [
|
|||
},
|
||||
];
|
||||
|
||||
add_task(async function test_setUp() {
|
||||
add_setup(async function() {
|
||||
// remove download files, empty out collections
|
||||
let downloadList = await Downloads.getList(Downloads.ALL);
|
||||
let downloadCount = (await downloadList.getAll()).length;
|
||||
|
|
|
@ -89,7 +89,7 @@ async function updateTopSites(condition, searchShortcuts = false) {
|
|||
}, "Waiting for top sites to be updated");
|
||||
}
|
||||
|
||||
add_task(async function setUp() {
|
||||
add_setup(async function() {
|
||||
Services.prefs.setBoolPref("browser.urlbar.suggest.quickactions", false);
|
||||
registerCleanupFunction(async () => {
|
||||
Services.prefs.clearUserPref("browser.urlbar.suggest.quickactions");
|
||||
|
|
|
@ -649,25 +649,26 @@ button.close {
|
|||
grid-template-columns: min-content repeat(2, 1fr) minmax(min-content, auto);
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:
|
||||
"favicon title title title"
|
||||
"favicon domain domain domain"
|
||||
"favicon device device time"
|
||||
"favicon title title title"
|
||||
"favicon domain domain domain"
|
||||
"favicon device device time"
|
||||
}
|
||||
|
||||
.synced-tab-a:hover {
|
||||
box-shadow: 0px 2px 14px var(--fxview-contrast-border);
|
||||
}
|
||||
|
||||
.synced-tab-a:not(:first-child) {
|
||||
.synced-tab-li:not(:first-child) > .synced-tab-a {
|
||||
align-content: center;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 60rem) {
|
||||
.synced-tab-li {
|
||||
.synced-tab-li > .synced-tab-a,
|
||||
.synced-tab-li-placeholder {
|
||||
grid-template-areas:
|
||||
"favicon title title title"
|
||||
"favicon domain domain domain"
|
||||
"favicon device device device"
|
||||
"favicon title title title"
|
||||
"favicon domain domain domain"
|
||||
"favicon device device device"
|
||||
}
|
||||
.synced-tab-li:not(:first-child) > .synced-tab-a > .synced-tab-li-time {
|
||||
display: none;
|
||||
|
@ -693,7 +694,7 @@ button.close {
|
|||
}
|
||||
|
||||
.li-placeholder-domain {
|
||||
grid-area: url;
|
||||
grid-area: domain;
|
||||
height: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -715,10 +716,10 @@ button.close {
|
|||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: auto;
|
||||
grid-template-areas:
|
||||
"favicon favicon badge badge"
|
||||
"title title title title"
|
||||
"domain domain domain domain"
|
||||
"device device device time";
|
||||
"favicon favicon badge badge"
|
||||
"title title title title"
|
||||
"domain domain domain domain"
|
||||
"device device device time";
|
||||
}
|
||||
|
||||
.synced-tab-li:nth-child(2) {
|
||||
|
@ -738,14 +739,11 @@ button.close {
|
|||
|
||||
.synced-tab-li-url {
|
||||
text-decoration-line: underline;
|
||||
}
|
||||
|
||||
.synced-tab-li:first-child > .synced-tab-a > .synced-tab-li-url {
|
||||
align-self: end;
|
||||
grid-area: domain;
|
||||
align-self: end;
|
||||
}
|
||||
|
||||
.synced-tab-li:not(:first-child) > .synced-tab-li-url {
|
||||
.synced-tab-li:not(:first-child) > .synced-tab-a > .synced-tab-li-url {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
|
|
|
@ -671,14 +671,20 @@ const MultiStageProtonScreen = props => {
|
|||
});
|
||||
};
|
||||
const ProtonScreenActionButtons = props => {
|
||||
var _content$primary_butt, _content$primary_butt2;
|
||||
var _content$checkbox, _content$primary_butt, _content$primary_butt2;
|
||||
|
||||
const {
|
||||
content
|
||||
} = props;
|
||||
const [isChecked, setIsChecked] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
|
||||
const defaultValue = (_content$checkbox = content.checkbox) === null || _content$checkbox === void 0 ? void 0 : _content$checkbox.defaultValue;
|
||||
const [isChecked, setIsChecked] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(defaultValue || false);
|
||||
|
||||
if (!content.primary_button && !content.secondary_button) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
className: "action-buttons"
|
||||
className: `action-buttons ${content.dual_action_buttons ? "dual-action-buttons" : ""}`
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_MSLocalized__WEBPACK_IMPORTED_MODULE_1__.Localized, {
|
||||
text: (_content$primary_butt = content.primary_button) === null || _content$primary_butt === void 0 ? void 0 : _content$primary_butt.label
|
||||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
|
||||
|
@ -695,6 +701,7 @@ const ProtonScreenActionButtons = props => {
|
|||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", {
|
||||
type: "checkbox",
|
||||
id: "action-checkbox",
|
||||
checked: isChecked,
|
||||
onChange: () => {
|
||||
setIsChecked(!isChecked);
|
||||
}
|
||||
|
@ -1367,7 +1374,7 @@ function LanguageSwitcher(props) {
|
|||
}, /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", {
|
||||
value: "decline_waiting",
|
||||
type: "button",
|
||||
className: "secondary text-link",
|
||||
className: "secondary text-link arrow-icon",
|
||||
onClick: handleAction
|
||||
})))), /*#__PURE__*/react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", {
|
||||
style: {
|
||||
|
|
|
@ -415,6 +415,19 @@ body[lwt-newtab-brighttext] {
|
|||
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .checkbox-container label {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons.dual-action-buttons button {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons.dual-action-buttons .secondary-cta {
|
||||
display: block;
|
||||
position: relative;
|
||||
bottom: unset;
|
||||
margin-block: 5px 0;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons.dual-action-buttons .secondary-cta .secondary {
|
||||
margin-inline: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
|
@ -436,6 +449,8 @@ body[lwt-newtab-brighttext] {
|
|||
background: url("chrome://browser/skin/forward.svg") no-repeat right 8px center;
|
||||
background-size: 12px;
|
||||
padding-inline-end: 24px;
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
}
|
||||
.onboardingContainer .screen[pos=split] .section-main .main-content .action-buttons .secondary-cta .arrow-icon:dir(rtl) {
|
||||
background-image: url("chrome://browser/skin/back.svg");
|
||||
|
|
|
@ -321,6 +321,25 @@ body {
|
|||
}
|
||||
}
|
||||
|
||||
&.dual-action-buttons {
|
||||
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
}
|
||||
|
||||
.secondary-cta {
|
||||
display: block;
|
||||
position: relative;
|
||||
bottom: unset;
|
||||
margin-block: 5px 0;
|
||||
|
||||
.secondary {
|
||||
margin-inline: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.secondary-cta {
|
||||
position: absolute;
|
||||
bottom: -30px;
|
||||
|
@ -346,6 +365,8 @@ body {
|
|||
background: url('chrome://browser/skin/forward.svg') no-repeat right 8px center;
|
||||
background-size: 12px;
|
||||
padding-inline-end: 24px;
|
||||
-moz-context-properties: fill;
|
||||
fill: currentColor;
|
||||
|
||||
&:dir(rtl) {
|
||||
background-image: url('chrome://browser/skin/back.svg');
|
||||
|
|
|
@ -200,7 +200,7 @@ export function LanguageSwitcher(props) {
|
|||
<button
|
||||
value="decline_waiting"
|
||||
type="button"
|
||||
className="secondary text-link"
|
||||
className="secondary text-link arrow-icon"
|
||||
onClick={handleAction}
|
||||
/>
|
||||
</Localized>
|
||||
|
|
|
@ -54,11 +54,20 @@ export const MultiStageProtonScreen = props => {
|
|||
|
||||
export const ProtonScreenActionButtons = props => {
|
||||
const { content } = props;
|
||||
const defaultValue = content.checkbox?.defaultValue;
|
||||
|
||||
const [isChecked, setIsChecked] = useState(false);
|
||||
const [isChecked, setIsChecked] = useState(defaultValue || false);
|
||||
|
||||
if (!content.primary_button && !content.secondary_button) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="action-buttons">
|
||||
<div
|
||||
className={`action-buttons ${
|
||||
content.dual_action_buttons ? "dual-action-buttons" : ""
|
||||
}`}
|
||||
>
|
||||
<Localized text={content.primary_button?.label}>
|
||||
<button
|
||||
className="primary"
|
||||
|
@ -76,6 +85,7 @@ export const ProtonScreenActionButtons = props => {
|
|||
<input
|
||||
type="checkbox"
|
||||
id="action-checkbox"
|
||||
checked={isChecked}
|
||||
onChange={() => {
|
||||
setIsChecked(!isChecked);
|
||||
}}
|
||||
|
|
|
@ -277,6 +277,12 @@ const QueryCache = {
|
|||
FRECENT_SITES_UPDATE_INTERVAL,
|
||||
ShellService
|
||||
),
|
||||
isDefaultBrowser: new CachedTargetingGetter(
|
||||
"isDefaultBrowser",
|
||||
null,
|
||||
FRECENT_SITES_UPDATE_INTERVAL,
|
||||
ShellService
|
||||
),
|
||||
currentThemes: new CachedTargetingGetter(
|
||||
"getAddonsByTypes",
|
||||
["theme"],
|
||||
|
@ -506,10 +512,7 @@ const TargetingGetters = {
|
|||
});
|
||||
},
|
||||
get isDefaultBrowser() {
|
||||
try {
|
||||
return ShellService.isDefaultBrowser();
|
||||
} catch (e) {}
|
||||
return null;
|
||||
return QueryCache.getters.isDefaultBrowser.get().catch(() => null);
|
||||
},
|
||||
get devToolsOpenedCount() {
|
||||
return lazy.devtoolsSelfXSSCount;
|
||||
|
@ -774,7 +777,7 @@ const TargetingGetters = {
|
|||
}
|
||||
return QueryCache.getters.currentThemes.get().then(themes => {
|
||||
let themeId = themes.find(theme => theme.isActive)?.id;
|
||||
return (
|
||||
return !!(
|
||||
themeId && lazy.BuiltInThemes.isColorwayFromCurrentCollection(themeId)
|
||||
);
|
||||
});
|
||||
|
|
|
@ -96,6 +96,29 @@ const ONBOARDING_MESSAGES = () => [
|
|||
type: "PIN_FIREFOX_TO_TASKBAR",
|
||||
},
|
||||
},
|
||||
checkbox: {
|
||||
label: {
|
||||
string_id: "mr2022-onboarding-existing-pin-checkbox-label",
|
||||
},
|
||||
defaultValue: true,
|
||||
action: {
|
||||
type: "MULTI_ACTION",
|
||||
navigate: true,
|
||||
data: {
|
||||
actions: [
|
||||
{
|
||||
type: "PIN_FIREFOX_TO_TASKBAR",
|
||||
data: {
|
||||
privatePin: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
type: "PIN_FIREFOX_TO_TASKBAR",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: {
|
||||
string_id: "mr2022-onboarding-secondary-skip-button-label",
|
||||
|
@ -411,6 +434,60 @@ const ONBOARDING_MESSAGES = () => [
|
|||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "UPGRADE_PRIVACY_SEGMENTATION",
|
||||
content: {
|
||||
position: "split",
|
||||
progress_bar: "true",
|
||||
dual_action_buttons: true,
|
||||
background:
|
||||
"url('chrome://activity-stream/content/data/content/assets/mr-pintaskbar.svg') var(--mr-secondary-position) no-repeat, var(--in-content-page-background) radial-gradient(83.12% 83.12% at 80.59% 16.88%, rgba(103, 51, 205, 0.75) 0%, rgba(0, 108, 207, 0.75) 54.51%, rgba(128, 199, 247, 0.75) 100%)",
|
||||
logo: {},
|
||||
title: {
|
||||
string_id: "mr2022-onboarding-privacy-segmentation-title",
|
||||
},
|
||||
subtitle: {
|
||||
string_id: "mr2022-onboarding-privacy-segmentation-subtitle",
|
||||
},
|
||||
cta_paragraph: {
|
||||
text: {
|
||||
string_id: "mr2022-onboarding-privacy-segmentation-text-cta",
|
||||
},
|
||||
},
|
||||
primary_button: {
|
||||
label: {
|
||||
string_id:
|
||||
"mr2022-onboarding-privacy-segmentation-button-primary-label",
|
||||
},
|
||||
action: {
|
||||
type: "SET_PREF",
|
||||
data: {
|
||||
pref: {
|
||||
name: "browser.privacySegmentation.enabled",
|
||||
value: true,
|
||||
},
|
||||
},
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
secondary_button: {
|
||||
label: {
|
||||
string_id:
|
||||
"mr2022-onboarding-privacy-segmentation-button-secondary-label",
|
||||
},
|
||||
action: {
|
||||
type: "SET_PREF",
|
||||
data: {
|
||||
pref: {
|
||||
name: "browser.privacySegmentation.enabled",
|
||||
value: false,
|
||||
},
|
||||
},
|
||||
navigate: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "UPGRADE_GRATITUDE",
|
||||
content: {
|
||||
|
@ -895,6 +972,9 @@ const OnboardingMessageProvider = {
|
|||
let removeDefault = !needDefault;
|
||||
// If user doesn't need pin, update screen to set "default" or "get started" configuration
|
||||
if (!needPin && pinScreen) {
|
||||
// don't need to show the checkbox
|
||||
delete pinScreen.content.checkbox;
|
||||
|
||||
removeDefault = true;
|
||||
let primary = pinScreen.content.primary_button;
|
||||
if (needDefault) {
|
||||
|
@ -919,6 +999,22 @@ const OnboardingMessageProvider = {
|
|||
}
|
||||
}
|
||||
|
||||
// If a user has Firefox private pinned remove pin private window screen
|
||||
// We also remove standalone pin private window screen if a user doesn't have
|
||||
// Firefox pinned in which case the option is shown as checkbox with UPGRADE_PIN_FIREFOX screen
|
||||
if (!needPrivatePin || needPin) {
|
||||
removeScreens(screen =>
|
||||
screen.id?.startsWith("UPGRADE_PIN_PRIVATE_WINDOW")
|
||||
);
|
||||
}
|
||||
|
||||
//If privatePin, remove checkbox from pinscreen
|
||||
if (!needPrivatePin) {
|
||||
delete content.screens?.find(
|
||||
screen => screen.id === "UPGRADE_PIN_FIREFOX"
|
||||
)?.content?.checkbox;
|
||||
}
|
||||
|
||||
if (removeDefault) {
|
||||
removeScreens(screen => screen.id?.startsWith("UPGRADE_SET_DEFAULT"));
|
||||
}
|
||||
|
@ -938,15 +1034,6 @@ const OnboardingMessageProvider = {
|
|||
};
|
||||
}
|
||||
|
||||
// If a user has Firefox private pinned remove pin private window screen
|
||||
// We also remove standalone pin private window screen if a user doesn't have
|
||||
// Firefox pinned in which case the option is shown as checkbox with UPGRADE_PIN_FIREFOX screen
|
||||
if (!needPrivatePin || needPin) {
|
||||
removeScreens(screen =>
|
||||
screen.id?.startsWith("UPGRADE_PIN_PRIVATE_WINDOW")
|
||||
);
|
||||
}
|
||||
|
||||
// Remove colorways screen if there is no active colorways collection
|
||||
const hasActiveColorways = !!lazy.BuiltInThemes.findActiveColorwayCollection?.();
|
||||
if (!hasActiveColorways) {
|
||||
|
|
|
@ -32,6 +32,8 @@ add_setup(async () => {
|
|||
.stub(OnboardingMessageProvider, "_doesAppNeedDefault")
|
||||
.resolves(false);
|
||||
|
||||
sandbox.stub(SpecialMessageActions, "pinFirefoxToTaskbar").resolves();
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
await popPrefs();
|
||||
sandbox.restore();
|
||||
|
@ -240,3 +242,109 @@ add_task(
|
|||
await popPrefs();
|
||||
}
|
||||
);
|
||||
|
||||
/*
|
||||
*Test checkbox if needPrivatePin is true
|
||||
*/
|
||||
add_task(async function test_aboutwelcome_upgrade_mr_private_pin() {
|
||||
OnboardingMessageProvider._doesAppNeedPin.resolves(true);
|
||||
let browser = await openMRUpgradeWelcome(["UPGRADE_PIN_FIREFOX"]);
|
||||
|
||||
await test_upgrade_screen_content(
|
||||
browser,
|
||||
//Expected selectors:
|
||||
["main.UPGRADE_PIN_FIREFOX", "input#action-checkbox"],
|
||||
//Unexpected selectors:
|
||||
["main.UPGRADE_COLORWAY"]
|
||||
);
|
||||
await clickVisibleButton(browser, ".action-buttons button.primary");
|
||||
await BrowserTestUtils.waitForDialogClose(
|
||||
browser.top.document.getElementById("window-modal-dialog")
|
||||
);
|
||||
|
||||
const pinStub = SpecialMessageActions.pinFirefoxToTaskbar;
|
||||
Assert.equal(
|
||||
pinStub.callCount,
|
||||
2,
|
||||
"pinFirefoxToTaskbar should have been called twice"
|
||||
);
|
||||
Assert.ok(
|
||||
// eslint-disable-next-line eqeqeq
|
||||
pinStub.firstCall.lastArg != pinStub.secondCall.lastArg,
|
||||
"pinFirefoxToTaskbar should have been called once for private, once not"
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
*Test checkbox shouldn't be shown in get started screen
|
||||
*/
|
||||
|
||||
add_task(async function test_aboutwelcome_upgrade_mr_private_pin_get_started() {
|
||||
OnboardingMessageProvider._doesAppNeedPin.resolves(false);
|
||||
|
||||
let browser = await openMRUpgradeWelcome(["UPGRADE_GET_STARTED"]);
|
||||
|
||||
await test_upgrade_screen_content(
|
||||
browser,
|
||||
//Expected selectors
|
||||
["main.UPGRADE_GET_STARTED"],
|
||||
//Unexpected selectors:
|
||||
["input#action-checkbox"]
|
||||
);
|
||||
|
||||
await clickVisibleButton(browser, ".action-buttons button.secondary");
|
||||
|
||||
await BrowserTestUtils.waitForDialogClose(
|
||||
browser.top.document.getElementById("window-modal-dialog")
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
*Test checkbox shouldn't be shown if needPrivatePin is false
|
||||
*/
|
||||
add_task(async function test_aboutwelcome_upgrade_mr_private_pin_not_needed() {
|
||||
OnboardingMessageProvider._doesAppNeedPin
|
||||
.resolves(true)
|
||||
.withArgs(true)
|
||||
.resolves(false);
|
||||
|
||||
let browser = await openMRUpgradeWelcome(["UPGRADE_PIN_FIREFOX"]);
|
||||
|
||||
await test_upgrade_screen_content(
|
||||
browser,
|
||||
//Expected selectors
|
||||
["main.UPGRADE_PIN_FIREFOX"],
|
||||
//Unexpected selectors:
|
||||
["input#action-checkbox"]
|
||||
);
|
||||
|
||||
await clickVisibleButton(browser, ".action-buttons button.secondary");
|
||||
await BrowserTestUtils.waitForDialogClose(
|
||||
browser.top.document.getElementById("window-modal-dialog")
|
||||
);
|
||||
});
|
||||
|
||||
/*
|
||||
* Make sure we don't get an extraneous checkbox here.
|
||||
*/
|
||||
add_task(
|
||||
async function test_aboutwelcome_upgrade_mr_pin_not_needed_default_needed() {
|
||||
OnboardingMessageProvider._doesAppNeedPin.resolves(false);
|
||||
OnboardingMessageProvider._doesAppNeedDefault.resolves(false);
|
||||
|
||||
let browser = await openMRUpgradeWelcome(["UPGRADE_GET_STARTED"]);
|
||||
|
||||
await test_upgrade_screen_content(
|
||||
browser,
|
||||
//Expected selectors
|
||||
["main.UPGRADE_GET_STARTED"],
|
||||
//Unexpected selectors:
|
||||
["input#action-checkbox"]
|
||||
);
|
||||
|
||||
await clickVisibleButton(browser, ".action-buttons button.secondary");
|
||||
await BrowserTestUtils.waitForDialogClose(
|
||||
browser.top.document.getElementById("window-modal-dialog")
|
||||
);
|
||||
}
|
||||
);
|
||||
|
|
|
@ -355,7 +355,7 @@ add_task(async function checksearchEngines() {
|
|||
|
||||
add_task(async function checkisDefaultBrowser() {
|
||||
const expected = ShellService.isDefaultBrowser();
|
||||
const result = ASRouterTargeting.Environment.isDefaultBrowser;
|
||||
const result = await ASRouterTargeting.Environment.isDefaultBrowser;
|
||||
is(typeof result, "boolean", "isDefaultBrowser should be a boolean value");
|
||||
is(
|
||||
result,
|
||||
|
|
|
@ -103,6 +103,25 @@ describe("MultiStageAboutWelcomeProton module", () => {
|
|||
assert.equal(wrapper.find(".welcome-text h1").text(), "test title");
|
||||
assert.equal(wrapper.find("main").prop("pos"), "center");
|
||||
});
|
||||
|
||||
it("should render action buttons container with dual-action-buttons class", () => {
|
||||
const SCREEN_PROPS = {
|
||||
content: {
|
||||
position: "split",
|
||||
title: "test title",
|
||||
dual_action_buttons: true,
|
||||
primary_button: {
|
||||
label: "test primary button",
|
||||
},
|
||||
secondary_button: {
|
||||
label: "test secondary button",
|
||||
},
|
||||
},
|
||||
};
|
||||
const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />);
|
||||
assert.ok(wrapper.exists());
|
||||
assert.ok(wrapper.find(".dual-action-buttons").text());
|
||||
});
|
||||
});
|
||||
|
||||
describe("AboutWelcomeDefaults for proton", () => {
|
||||
|
@ -335,5 +354,17 @@ describe("MultiStageAboutWelcomeProton module", () => {
|
|||
{ id: "world" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("should not render action buttons if a primary and secondary button does not exist", async () => {
|
||||
const SCREEN_PROPS = {
|
||||
content: {
|
||||
title: "test title",
|
||||
subtitle: "test subtitle",
|
||||
},
|
||||
};
|
||||
const wrapper = mount(<MultiStageProtonScreen {...SCREEN_PROPS} />);
|
||||
assert.ok(wrapper.exists());
|
||||
assert.equal(wrapper.find(".action-buttons").exists(), false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -437,16 +437,6 @@ var PlacesOrganizer = {
|
|||
? [view.selectedNode]
|
||||
: view.selectedNodes;
|
||||
this._fillDetailsPane(selectedNodes);
|
||||
window
|
||||
.promiseDocumentFlushed(() => {})
|
||||
.then(() => {
|
||||
if (view.selectedNode && ContentArea.currentView.view) {
|
||||
let row = ContentArea.currentView.view.treeIndexForNode(
|
||||
view.selectedNode
|
||||
);
|
||||
ContentTree.view.ensureRowIsVisible(row);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -115,7 +115,6 @@ skip-if = os == "linux" && (tsan || asan) # Bug 1714384
|
|||
[browser_library_open_all.js]
|
||||
[browser_library_open_bookmark.js]
|
||||
[browser_library_panel_leak.js]
|
||||
[browser_library_row_is_visible.js]
|
||||
[browser_library_search.js]
|
||||
[browser_library_tree_leak.js]
|
||||
[browser_library_views_liveupdate.js]
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
|
||||
/**
|
||||
* Checks whether or not a row is visible when a user clicks on
|
||||
* row that's close to the bottom of the Library window, as the
|
||||
* DetailsPane can grow after the user has clicked on a bookmark.
|
||||
*
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
let library;
|
||||
let PlacesOrganizer;
|
||||
|
||||
function assertSelectedRowIsVisible(selectedRow, msg) {
|
||||
let firstRow = library.ContentTree.view.getFirstVisibleRow();
|
||||
let lastRow = library.ContentTree.view.getLastVisibleRow();
|
||||
Assert.ok(firstRow <= selectedRow && lastRow >= selectedRow, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a bunch of bookmarks so that the Library view needs to be
|
||||
* scrolled in order to see all the bookmarks.
|
||||
*/
|
||||
add_setup(async function() {
|
||||
await PlacesUtils.bookmarks.eraseEverything();
|
||||
|
||||
library = await promiseLibrary("UnfiledBookmarks");
|
||||
|
||||
let baseUrl = "https://www.example.com/";
|
||||
|
||||
// Enough bookmarks that should go beyond the initial screen
|
||||
let nBookmarks = library.ContentTree.view.getLastVisibleRow() + 5;
|
||||
|
||||
let bookmarks = new Array(nBookmarks);
|
||||
|
||||
// Hack to make it so that when the list of bookmarks is
|
||||
// first loaded, a bookmark folder is first selected so that
|
||||
// the details pane is small
|
||||
bookmarks[0] = {
|
||||
title: "Test Folder",
|
||||
type: PlacesUtils.bookmarks.TYPE_FOLDER,
|
||||
};
|
||||
|
||||
for (let index = 1; index < nBookmarks; ++index) {
|
||||
bookmarks[index] = {
|
||||
title: `Example Bookmark ${index + 10}`,
|
||||
url: baseUrl + index + 10,
|
||||
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||
};
|
||||
}
|
||||
|
||||
await PlacesUtils.bookmarks.insertTree({
|
||||
guid: PlacesUtils.bookmarks.unfiledGuid,
|
||||
children: bookmarks,
|
||||
});
|
||||
|
||||
await promiseLibraryClosed(library);
|
||||
|
||||
registerCleanupFunction(async () => {
|
||||
await PlacesUtils.bookmarks.eraseEverything();
|
||||
await promiseLibraryClosed(library);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Click on a bookmark that should be hidden when the details
|
||||
* panel is expanded, and so the library panel should scroll.
|
||||
*/
|
||||
add_task(async function test_click_bookmark() {
|
||||
library = await promiseLibrary("UnfiledBookmarks");
|
||||
|
||||
let selectedRow = library.ContentTree.view.getLastVisibleRow();
|
||||
|
||||
let node = library.ContentTree.view.view.nodeForTreeIndex(selectedRow);
|
||||
library.ContentTree.view.selectNode(node);
|
||||
synthesizeClickOnSelectedTreeCell(library.ContentTree.view);
|
||||
|
||||
// TODO Bug 1769312: Have to wait till the row is scrolled to
|
||||
// which unfortunately is not easy to know at the current moment.
|
||||
// Should be replaced with an event.
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
|
||||
assertSelectedRowIsVisible(selectedRow, "Selected row is visible");
|
||||
|
||||
await promiseLibraryClosed(library);
|
||||
});
|
||||
|
||||
/**
|
||||
* Sort a bookmarks list by one of the columns and check if
|
||||
* clicking on a bookmark will show the bookmark.
|
||||
*/
|
||||
add_task(async function test_click_bookmark_on_sort() {
|
||||
library = await promiseLibrary("UnfiledBookmarks");
|
||||
|
||||
let selectedRow = library.ContentTree.view.getLastVisibleRow();
|
||||
|
||||
// Re-sort by name
|
||||
library.ViewMenu.setSortColumn(0, "descending");
|
||||
|
||||
let node = library.ContentTree.view.view.nodeForTreeIndex(selectedRow);
|
||||
library.ContentTree.view.selectNode(node);
|
||||
synthesizeClickOnSelectedTreeCell(library.ContentTree.view);
|
||||
|
||||
// TODO Bug 1769312: Have to wait till the row is scrolled to
|
||||
// which unfortunately is not easy to know at the current moment.
|
||||
// Should be replaced with an event.
|
||||
/* eslint-disable mozilla/no-arbitrary-setTimeout */
|
||||
await new Promise(resolve => {
|
||||
setTimeout(resolve, 500);
|
||||
});
|
||||
|
||||
assertSelectedRowIsVisible(selectedRow, "Selected row is visible after sort");
|
||||
});
|
|
@ -10,9 +10,22 @@ add_task(async function() {
|
|||
set: [["dom.text-recognition.enabled", true]],
|
||||
});
|
||||
|
||||
clearTelemetry();
|
||||
|
||||
await BrowserTestUtils.withNewTab(URL_IMG, async function(browser) {
|
||||
setClipboardText("");
|
||||
is(getTextFromClipboard(), "", "The copied text is empty.");
|
||||
ok(
|
||||
!getTelemetryScalars()["browser.ui.interaction.content_context"],
|
||||
"No telemetry has been recorded yet."
|
||||
);
|
||||
is(
|
||||
Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_API_PERFORMANCE")
|
||||
.snapshot().sum,
|
||||
0,
|
||||
"No histogram timing was recorded."
|
||||
);
|
||||
|
||||
info("Right click image to show context menu.");
|
||||
let popupShownPromise = BrowserTestUtils.waitForEvent(
|
||||
|
@ -43,6 +56,17 @@ add_task(async function() {
|
|||
document.querySelector(".textRecognitionDialogFrame")
|
||||
);
|
||||
|
||||
{
|
||||
info("Check the scalar telemetry.");
|
||||
const scalars = await BrowserTestUtils.waitForCondition(() =>
|
||||
getTelemetryScalars()
|
||||
);
|
||||
const contentContext = scalars["browser.ui.interaction.content_context"];
|
||||
ok(contentContext, "Opening the context menu was recorded.");
|
||||
|
||||
is(contentContext["context-imagetext"], 1, "Telemetry has been recorded");
|
||||
}
|
||||
|
||||
info("Waiting for text results.");
|
||||
const resultsHeader = contentDocument.querySelector(
|
||||
"#text-recognition-header-results"
|
||||
|
@ -51,29 +75,57 @@ add_task(async function() {
|
|||
return resultsHeader.style.display !== "none";
|
||||
});
|
||||
|
||||
info("Checking the text results.");
|
||||
const text = contentDocument.querySelector(".textRecognitionText");
|
||||
is(text.children.length, 2, "Two piece of text were found");
|
||||
const [p1, p2] = text.children;
|
||||
is(p1.tagName, "P", "The children are paragraph tags.");
|
||||
is(p2.tagName, "P", "The children are paragraph tags.");
|
||||
is(p1.innerText, "Mozilla\n", "The first piece of text matches.");
|
||||
is(p2.innerText, "Firefox\n", "The second piece of text matches.");
|
||||
{
|
||||
info("Check the text results.");
|
||||
const text = contentDocument.querySelector(".textRecognitionText");
|
||||
is(text.children.length, 2, "Two piece of text were found");
|
||||
const [p1, p2] = text.children;
|
||||
is(p1.tagName, "P", "The children are paragraph tags.");
|
||||
is(p2.tagName, "P", "The children are paragraph tags.");
|
||||
is(p1.innerText, "Mozilla\n", "The first piece of text matches.");
|
||||
is(p2.innerText, "Firefox\n", "The second piece of text matches.");
|
||||
}
|
||||
|
||||
ok(
|
||||
Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_API_PERFORMANCE")
|
||||
.snapshot().sum > 0,
|
||||
"Text recognition API performance was recorded."
|
||||
);
|
||||
|
||||
info("Close the dialog box.");
|
||||
const close = contentDocument.querySelector("#text-recognition-close");
|
||||
close.click();
|
||||
|
||||
const expectedResultText = "Mozilla\nFirefox\n";
|
||||
is(getTextFromClipboard(), expectedResultText, "The copied text matches.");
|
||||
|
||||
is(
|
||||
getTextFromClipboard(),
|
||||
"Mozilla\nFirefox\n",
|
||||
"The copied text matches."
|
||||
Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_TEXT_LENGTH")
|
||||
.snapshot().sum,
|
||||
expectedResultText.length,
|
||||
"The length of the text was recorded."
|
||||
);
|
||||
|
||||
info("Waiting for the dialog frame to close.");
|
||||
await BrowserTestUtils.waitForCondition(
|
||||
() => !document.querySelector(".textRecognitionDialogFrame")
|
||||
);
|
||||
|
||||
info("Check for interaction telemetry.");
|
||||
const timing = await BrowserTestUtils.waitForCondition(() => {
|
||||
const { sum } = Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_INTERACTION_TIMING")
|
||||
.snapshot();
|
||||
if (sum > 0) {
|
||||
return sum;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
ok(timing > 0, "Interaction timing was measured.");
|
||||
|
||||
setClipboardText("");
|
||||
clearTelemetry();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,6 +10,8 @@ add_task(async function() {
|
|||
set: [["dom.text-recognition.enabled", true]],
|
||||
});
|
||||
|
||||
clearTelemetry();
|
||||
|
||||
await BrowserTestUtils.withNewTab(url, async function(browser) {
|
||||
setClipboardText("");
|
||||
is(getTextFromClipboard(), "", "The copied text is empty.");
|
||||
|
@ -51,9 +53,35 @@ add_task(async function() {
|
|||
return noResultsHeader.style.display !== "none";
|
||||
});
|
||||
|
||||
{
|
||||
info("Check the scalar telemetry.");
|
||||
const scalars = await BrowserTestUtils.waitForCondition(() =>
|
||||
getTelemetryScalars()
|
||||
);
|
||||
const contentContext = scalars["browser.ui.interaction.content_context"];
|
||||
ok(contentContext, "Opening the context menu was recorded.");
|
||||
|
||||
is(contentContext["context-imagetext"], 1, "Telemetry has been recorded");
|
||||
}
|
||||
|
||||
const text = contentDocument.querySelector(".textRecognitionText");
|
||||
is(text.children.length, 0, "No results are listed.");
|
||||
|
||||
ok(
|
||||
Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_API_PERFORMANCE")
|
||||
.snapshot().sum > 0,
|
||||
"Histogram timing was recorded even though there were no results."
|
||||
);
|
||||
|
||||
is(
|
||||
Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_INTERACTION_TIMING")
|
||||
.snapshot().sum,
|
||||
0,
|
||||
"No interaction timing has been measured yet."
|
||||
);
|
||||
|
||||
info("Close the dialog box.");
|
||||
const close = contentDocument.querySelector("#text-recognition-close");
|
||||
close.click();
|
||||
|
@ -65,4 +93,6 @@ add_task(async function() {
|
|||
|
||||
is(getTextFromClipboard(), "", "The copied text is still empty.");
|
||||
});
|
||||
|
||||
clearTelemetry();
|
||||
});
|
||||
|
|
|
@ -26,3 +26,30 @@ function getTextFromClipboard() {
|
|||
transferable.getTransferData("text/unicode", results);
|
||||
return results.value.QueryInterface(Ci.nsISupportsString)?.data ?? "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns events specifically for text recognition.
|
||||
*/
|
||||
function getTelemetryScalars() {
|
||||
const snapshot = Services.telemetry.getSnapshotForKeyedScalars(
|
||||
"main",
|
||||
true /* clear events */
|
||||
);
|
||||
|
||||
if (!snapshot.parent) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return snapshot.parent;
|
||||
}
|
||||
|
||||
function clearTelemetry() {
|
||||
Services.telemetry.clearScalars();
|
||||
Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_API_PERFORMANCE")
|
||||
.clear();
|
||||
Services.telemetry
|
||||
.getHistogramById("TEXT_RECOGNITION_INTERACTION_TIMING")
|
||||
.clear();
|
||||
Services.telemetry.getHistogramById("TEXT_RECOGNITION_TEXT_LENGTH").clear();
|
||||
}
|
||||
|
|
|
@ -46,6 +46,11 @@ class TextRecognitionModal {
|
|||
if (results.length === 0) {
|
||||
// Update the UI to indicate that there were no results.
|
||||
this.showHeaderByID("text-recognition-header-no-results");
|
||||
// It's still worth recording telemetry times, as the API was still invoked.
|
||||
TelemetryStopwatch.finish(
|
||||
"TEXT_RECOGNITION_API_PERFORMANCE",
|
||||
resultsPromise
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -53,16 +58,71 @@ class TextRecognitionModal {
|
|||
// the results to the UI.
|
||||
this.runClusteringAndUpdateUI(results, direction);
|
||||
this.showHeaderByID("text-recognition-header-results");
|
||||
TelemetryStopwatch.finish(
|
||||
"TEXT_RECOGNITION_API_PERFORMANCE",
|
||||
resultsPromise
|
||||
);
|
||||
|
||||
TextRecognitionModal.recordInteractionTime();
|
||||
},
|
||||
error => {
|
||||
// There was an error in the text recognition call. Treat this as the same
|
||||
// as if there were no results, but report the error to the console.
|
||||
console.error(error);
|
||||
// as if there were no results, but report the error to the console and telemetry.
|
||||
this.showHeaderByID("text-recognition-header-no-results");
|
||||
|
||||
console.error(
|
||||
"There was an error recognizing the text from an image.",
|
||||
error
|
||||
);
|
||||
Services.telemetry.scalarAdd(
|
||||
"browser.ui.interaction.textrecognition_error",
|
||||
1
|
||||
);
|
||||
TelemetryStopwatch.cancel(
|
||||
"TEXT_RECOGNITION_API_PERFORMANCE",
|
||||
resultsPromise
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* After the results are shown, measure how long a user interacts with the modal.
|
||||
*/
|
||||
static recordInteractionTime() {
|
||||
TelemetryStopwatch.start(
|
||||
"TEXT_RECOGNITION_INTERACTION_TIMING",
|
||||
// Pass the instance of the window in case multiple tabs are doing text recognition
|
||||
// and there is a race condition.
|
||||
window
|
||||
);
|
||||
|
||||
const finish = () => {
|
||||
TelemetryStopwatch.finish("TEXT_RECOGNITION_INTERACTION_TIMING", window);
|
||||
window.removeEventListener("blur", finish);
|
||||
window.removeEventListener("unload", finish);
|
||||
};
|
||||
|
||||
// The user's focus went away, record this as the total interaction, even if they
|
||||
// go back and interact with it more. This can be triggered by doing actions like
|
||||
// clicking the URL bar, or by switching tabs.
|
||||
window.addEventListener("blur", finish);
|
||||
|
||||
// The modal is closed.
|
||||
window.addEventListener("unload", finish);
|
||||
}
|
||||
|
||||
/**
|
||||
* After the results are shown, measure how long a user interacts with the modal.
|
||||
* @param {number} length
|
||||
*/
|
||||
static recordTextLengthTelemetry(length) {
|
||||
const histogram = Services.telemetry.getHistogramById(
|
||||
"TEXT_RECOGNITION_TEXT_LENGTH"
|
||||
);
|
||||
histogram.add(length);
|
||||
}
|
||||
|
||||
setupCloseHandler() {
|
||||
document
|
||||
.querySelector("#text-recognition-close")
|
||||
|
@ -166,6 +226,7 @@ class TextRecognitionModal {
|
|||
this.textEl.style.display = "block";
|
||||
|
||||
TextRecognitionModal.copy(text);
|
||||
TextRecognitionModal.recordTextLengthTelemetry(text.length);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ async function checkOpensOnFocus(win = window) {
|
|||
});
|
||||
}
|
||||
|
||||
add_task(async function setUp() {
|
||||
add_setup(async function() {
|
||||
// Add some history for the empty panel.
|
||||
await PlacesTestUtils.addVisits([
|
||||
{
|
||||
|
|
|
@ -69,7 +69,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "25ffad05ab62ef2a0f749182e2382572cb980d78"
|
||||
"revision": "e4efbb1ae8c9460d91697cb19ca399391516c6a1"
|
||||
},
|
||||
"ast": {
|
||||
"pin": false,
|
||||
|
@ -357,7 +357,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "9e11d30d9d1e59de209db1601d33afeb5f64f16c"
|
||||
"revision": "10e0d35aa0c0fe14bdfca7003998b021d3112067"
|
||||
},
|
||||
"de": {
|
||||
"pin": false,
|
||||
|
@ -375,7 +375,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "7008190a3c810b10d03174708111652787885eb5"
|
||||
"revision": "081cb3579df43cc693b9ec190ee0080834b077b9"
|
||||
},
|
||||
"dsb": {
|
||||
"pin": false,
|
||||
|
@ -483,7 +483,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "1c0769caf37db56f48b38a60527e9ddaae558207"
|
||||
"revision": "5792a213755f0522cfa517a248ac4c749abcb906"
|
||||
},
|
||||
"es-CL": {
|
||||
"pin": false,
|
||||
|
@ -501,7 +501,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "2027b3e3a0503374d8a42b3505d448fef6d96a34"
|
||||
"revision": "717f385cee05e39f757d9eb2df86c26210d0a35f"
|
||||
},
|
||||
"es-ES": {
|
||||
"pin": false,
|
||||
|
@ -663,7 +663,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "8e24d6455abf245be8c9f7653d8a70e2bb9392b4"
|
||||
"revision": "43d90376f8c879819a9690e79a46207045fda723"
|
||||
},
|
||||
"ga-IE": {
|
||||
"pin": false,
|
||||
|
@ -771,7 +771,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "54f3fd42e75f97931f4b8bc4381269e145123ba3"
|
||||
"revision": "9bfd43614204af81d29411dd2db524d0c79eaab2"
|
||||
},
|
||||
"hi-IN": {
|
||||
"pin": false,
|
||||
|
@ -1317,7 +1317,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "2510a6117777124d4b65572ae4b01cbc03c4ddfe"
|
||||
"revision": "5ae8284dce3f664674a05e48b39f565f7cc1ecd4"
|
||||
},
|
||||
"nn-NO": {
|
||||
"pin": false,
|
||||
|
@ -1353,7 +1353,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "00c0c22d756eb512e952534155971c79b6b930bf"
|
||||
"revision": "859a2e7f699c47e49a33e6fb7e1ac2ed72aa2290"
|
||||
},
|
||||
"pa-IN": {
|
||||
"pin": false,
|
||||
|
@ -1569,7 +1569,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "788aa6ec2d7c12368f80ab818919103beeaf513a"
|
||||
"revision": "fadee9d67986216412cc618106e2caa4029778c6"
|
||||
},
|
||||
"sk": {
|
||||
"pin": false,
|
||||
|
@ -1587,7 +1587,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "2d30d7600d5883569e4f7ad680ceac5310e185df"
|
||||
"revision": "13f02831c35a98b6dd6c013cf65b3b9facd2e638"
|
||||
},
|
||||
"sl": {
|
||||
"pin": false,
|
||||
|
@ -1749,7 +1749,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "ce90e151998747f4d3b5acec66cd6d6b42ee949e"
|
||||
"revision": "684694f3c6fb8be0c709a8c5203f3a6792d352f5"
|
||||
},
|
||||
"th": {
|
||||
"pin": false,
|
||||
|
@ -1839,7 +1839,7 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "0994db68103ff00e69b4b4477c5324076c37fc07"
|
||||
"revision": "9ab9d08a4e6c8a432f1d7ff699840d685fd48020"
|
||||
},
|
||||
"ur": {
|
||||
"pin": false,
|
||||
|
@ -1965,6 +1965,6 @@
|
|||
"win64-aarch64-devedition",
|
||||
"win64-devedition"
|
||||
],
|
||||
"revision": "2824c9de66684379c0cafa1250646fdd744a52fd"
|
||||
"revision": "e46566eabf0f5d4915c1088cc94b0e2254b538c3"
|
||||
}
|
||||
}
|
|
@ -52,12 +52,19 @@ with only_when(target_is_osx):
|
|||
"--enable-macos-target",
|
||||
env="MACOSX_DEPLOYMENT_TARGET",
|
||||
nargs=1,
|
||||
default=depends(target)(lambda t: "11.0" if t.cpu == "aarch64" else "10.12"),
|
||||
default=depends(target, developer_options)
|
||||
# We continue to target 10.12 on Intel, but can target 11.0 for
|
||||
# aarch64 since the earliest hardware was released alongside 11.0.
|
||||
# For local builds, we want to target 10.12 regardless of the
|
||||
# underlying platform to catch any errors or warnings that wouldn't
|
||||
# show up when targeting 11.0, since these would later show up on
|
||||
# CI for Intel builds.
|
||||
(lambda t, d: "11.0" if (t.cpu == "aarch64" and not d) else "10.12"),
|
||||
help="Set the minimum MacOS version needed at runtime{|}",
|
||||
)
|
||||
|
||||
@depends_if("--enable-macos-target")
|
||||
def macos_target(value):
|
||||
@depends_if("--enable-macos-target", developer_options)
|
||||
def macos_target(value, _):
|
||||
return value[0]
|
||||
|
||||
|
||||
|
|
|
@ -143,18 +143,6 @@ with only_when(depends(target)(lambda t: t.kernel == "Darwin")):
|
|||
# catch ObjC method parameter type not matching super class method
|
||||
check_and_add_warning("-Wsuper-class-method-mismatch")
|
||||
|
||||
# catches expressions used as a null pointer constant
|
||||
# XXX: at the time of writing, the version of clang used on the OS X test
|
||||
# machines has a bug that causes it to reject some valid files if both
|
||||
# -Wnon-literal-null-conversion and -Wsometimes-uninitialized are
|
||||
# specified. We work around this by instead using
|
||||
# -Werror=non-literal-null-conversion, but we only do that when
|
||||
# --enable-warnings-as-errors is specified so that no unexpected fatal
|
||||
# warnings are produced.
|
||||
check_and_add_warning(
|
||||
"-Werror=non-literal-null-conversion", when="--enable-warnings-as-errors"
|
||||
)
|
||||
|
||||
# catches string literals used in boolean expressions
|
||||
check_and_add_warning("-Wstring-conversion")
|
||||
|
||||
|
@ -172,25 +160,12 @@ check_and_add_warning("-Wno-error=deprecated-declarations")
|
|||
# false positives depending on optimization
|
||||
check_and_add_warning("-Wno-error=array-bounds")
|
||||
|
||||
# can't get rid of those PGO warnings
|
||||
check_and_add_warning("-Wno-error=coverage-mismatch")
|
||||
|
||||
# -Wbackend-plugin warnings from Android PGO profile-use builds:
|
||||
# error: /builds/worker/workspace/build/src/mozglue/misc/AutoProfilerLabel.cpp:
|
||||
# Function control flow change detected (hash mismatch)
|
||||
# _ZN7mozilla17AutoProfilerLabelD2Ev [-Werror,-Wbackend-plugin]
|
||||
check_and_add_warning("-Wno-error=backend-plugin")
|
||||
|
||||
# false positives depending on optimizations
|
||||
check_and_add_warning("-Wno-error=free-nonheap-object")
|
||||
|
||||
# Would be a pain to fix all occurrences, for very little gain
|
||||
check_and_add_warning("-Wno-multistatement-macros")
|
||||
|
||||
# Disable the -Werror for return-std-move because of a false positive
|
||||
# on nsTAutoStringN: https://bugs.llvm.org/show_bug.cgi?id=37249
|
||||
check_and_add_warning("-Wno-error=return-std-move")
|
||||
|
||||
# Disable the -Werror for -Wclass-memaccess as we have a long
|
||||
# tail of issues to fix
|
||||
check_and_add_warning("-Wno-error=class-memaccess")
|
||||
|
@ -199,10 +174,6 @@ check_and_add_warning("-Wno-error=class-memaccess")
|
|||
# https://bugs.llvm.org/show_bug.cgi?id=38593
|
||||
check_and_add_warning("-Wno-error=atomic-alignment")
|
||||
|
||||
# New warning with gcc 9. Not useful
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=1515356
|
||||
check_and_add_warning("-Wno-error=deprecated-copy")
|
||||
|
||||
# New warning with clang 15. Catches uses of deprecated builtins in abseil-cpp.
|
||||
# https://bugzilla.mozilla.org/show_bug.cgi?id=1779528
|
||||
check_and_add_warning("-Wno-error=deprecated-builtins")
|
||||
|
@ -308,9 +279,6 @@ with only_when(target_is_windows):
|
|||
# build, but we're not sure why.
|
||||
check_and_add_warning("-Wno-enum-compare")
|
||||
|
||||
# We hit this all over the place with the gtest INSTANTIATE_TEST_CASE_P macro
|
||||
check_and_add_warning("-Wno-gnu-zero-variadic-macro-arguments")
|
||||
|
||||
# Make it an error to be missing function declarations for C code.
|
||||
check_and_add_warning("-Werror=implicit-function-declaration", c_compiler)
|
||||
|
||||
|
@ -332,6 +300,9 @@ check_and_add_warning(
|
|||
),
|
||||
)
|
||||
|
||||
# Warn if APIs are used without available() checks on macOS.
|
||||
check_and_add_warning("-Werror=unguarded-availability-new", when=target_is_osx)
|
||||
|
||||
# Please keep these last in this file
|
||||
add_old_configure_assignment("_WARNINGS_CFLAGS", warnings_flags.cflags)
|
||||
add_old_configure_assignment("_WARNINGS_CXXFLAGS", warnings_flags.cxxflags)
|
||||
|
|
10
configure
vendored
Executable file
10
configure
vendored
Executable file
|
@ -0,0 +1,10 @@
|
|||
#!/bin/sh
|
||||
# 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/.
|
||||
|
||||
SRCDIR=$(dirname $0)
|
||||
TOPSRCDIR="$SRCDIR"
|
||||
PYTHON3="${PYTHON3:-python3}"
|
||||
|
||||
exec "$PYTHON3" "$TOPSRCDIR/configure.py" "$@"
|
19
configure.in
19
configure.in
|
@ -1,19 +0,0 @@
|
|||
#!/bin/sh
|
||||
# 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/.
|
||||
#
|
||||
# Because adding a configure file in the tree is going to conflict with
|
||||
# existing configure files in people's (and automation) work trees, and
|
||||
# because some automation jobs are still running autoconf and configure
|
||||
# "manually", this file is actually an m4 file that is processed by
|
||||
# autoconf, but doesn't call any autoconf macros. The `divert` line
|
||||
# below ensures the script that follows is output by autoconf.
|
||||
: "divert(0)dnl"
|
||||
#!/bin/sh
|
||||
|
||||
SRCDIR=$(dirname $0)
|
||||
TOPSRCDIR="$SRCDIR"
|
||||
PYTHON3="${PYTHON3:-python3}"
|
||||
|
||||
exec "$PYTHON3" "$TOPSRCDIR/configure.py" "$@"
|
|
@ -40,12 +40,11 @@ add_task(async function testWebExtensionToolboxReload() {
|
|||
document
|
||||
);
|
||||
|
||||
const { devtoolsTab, devtoolsWindow } = await openAboutDevtoolsToolbox(
|
||||
document,
|
||||
tab,
|
||||
window,
|
||||
ADDON_NAME
|
||||
);
|
||||
const {
|
||||
devtoolsDocument,
|
||||
devtoolsTab,
|
||||
devtoolsWindow,
|
||||
} = await openAboutDevtoolsToolbox(document, tab, window, ADDON_NAME);
|
||||
const toolbox = getToolbox(devtoolsWindow);
|
||||
|
||||
const webconsole = await toolbox.selectTool("webconsole");
|
||||
|
@ -64,13 +63,35 @@ add_task(async function testWebExtensionToolboxReload() {
|
|||
synthesizeKeyShortcut(L10N.getStr("toolbox.reload.key"), toolbox.win);
|
||||
|
||||
info("Wait until a new background log message is logged");
|
||||
const secondMessage = await waitFor(() => {
|
||||
const newMessage = findMessagesByType(
|
||||
hud,
|
||||
"background script executed",
|
||||
".console-api"
|
||||
);
|
||||
if (newMessage && newMessage !== initialMessage) {
|
||||
return newMessage;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
await waitForLoadedPanelsReload();
|
||||
|
||||
info("Reload via the debug target info bar button");
|
||||
clickReload(devtoolsDocument);
|
||||
|
||||
info("Wait until yet another background log message is logged");
|
||||
await waitFor(() => {
|
||||
const newMessage = findMessagesByType(
|
||||
hud,
|
||||
"background script executed",
|
||||
".console-api"
|
||||
);
|
||||
return newMessage && newMessage !== initialMessage;
|
||||
return (
|
||||
newMessage &&
|
||||
newMessage !== initialMessage &&
|
||||
newMessage !== secondMessage
|
||||
);
|
||||
});
|
||||
|
||||
await waitForLoadedPanelsReload();
|
||||
|
@ -79,3 +100,7 @@ add_task(async function testWebExtensionToolboxReload() {
|
|||
await removeTemporaryExtension(ADDON_NAME, document);
|
||||
await removeTab(tab);
|
||||
});
|
||||
|
||||
function clickReload(devtoolsDocument) {
|
||||
devtoolsDocument.querySelector(".qa-reload-button").click();
|
||||
}
|
||||
|
|
|
@ -289,13 +289,20 @@ class DebugTargetInfo extends PureComponent {
|
|||
const { debugTargetData } = this.props;
|
||||
const { targetType } = debugTargetData;
|
||||
|
||||
if (targetType !== DEBUG_TARGET_TYPES.TAB) {
|
||||
if (
|
||||
targetType !== DEBUG_TARGET_TYPES.TAB &&
|
||||
targetType !== DEBUG_TARGET_TYPES.EXTENSION
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const items = [];
|
||||
|
||||
if (this.props.toolbox.target.getTrait("navigation")) {
|
||||
// There is little value in exposing back/forward for WebExtensions
|
||||
if (
|
||||
this.props.toolbox.target.getTrait("navigation") &&
|
||||
targetType === DEBUG_TARGET_TYPES.TAB
|
||||
) {
|
||||
items.push(
|
||||
this.renderNavigationButton({
|
||||
className: "qa-back-button",
|
||||
|
|
|
@ -91,6 +91,7 @@ skip-if = (verify && !debug && os == 'win')
|
|||
[browser_rules_computed-lists_02.js]
|
||||
[browser_rules_computed-lists_03.js]
|
||||
[browser_rules_completion-popup-hidden-after-navigation.js]
|
||||
[browser_rules_container-queries.js]
|
||||
[browser_rules_content_01.js]
|
||||
[browser_rules_content_02.js]
|
||||
[browser_rules_variables-in-pseudo-element_01.js]
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the rule-view content is correct when the page defines container queries.
|
||||
const TEST_URI = `
|
||||
<style type="text/css">
|
||||
body {
|
||||
container: mycontainer / inline-size;
|
||||
}
|
||||
|
||||
@container (width > 0px) {
|
||||
h1, [test-hint="nocontainername"]{
|
||||
outline-color: chartreuse;
|
||||
}
|
||||
}
|
||||
|
||||
@container unknowncontainer (min-width: 2vw) {
|
||||
h1, [test-hint="unknowncontainer"] {
|
||||
border-color: salmon;
|
||||
}
|
||||
}
|
||||
|
||||
@container mycontainer (1px < width < 10000px) {
|
||||
h1, [test-hint="container"] {
|
||||
color: tomato;
|
||||
}
|
||||
}
|
||||
|
||||
@layer mylayer {
|
||||
@container mycontainer (min-width: 3em) {
|
||||
@media screen {
|
||||
@container mycontainer (min-width: 4rem) {
|
||||
h1, [test-hint="nested"] {
|
||||
background: gold;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<h1>Hello @container!</h1>
|
||||
`;
|
||||
|
||||
add_task(async function() {
|
||||
await pushPref("layout.css.container-queries.enabled", true);
|
||||
|
||||
await addTab(
|
||||
"https://example.com/document-builder.sjs?html=" +
|
||||
encodeURIComponent(TEST_URI)
|
||||
);
|
||||
const { inspector, view } = await openRuleView();
|
||||
|
||||
await selectNode("h1", inspector);
|
||||
|
||||
const expectedRules = [
|
||||
{ selector: "element", ancestorRulesData: null },
|
||||
{
|
||||
selector: `h1, [test-hint="container"]`,
|
||||
ancestorRulesData: ["@container mycontainer (1px < width < 10000px)"],
|
||||
},
|
||||
{
|
||||
selector: `h1, [test-hint="unknowncontainer"]`,
|
||||
ancestorRulesData: ["@container unknowncontainer (min-width: 2vw)"],
|
||||
},
|
||||
{
|
||||
selector: `h1, [test-hint="nocontainername"]`,
|
||||
ancestorRulesData: ["@container (width > 0px)"],
|
||||
},
|
||||
{
|
||||
selector: `h1, [test-hint="nested"]`,
|
||||
ancestorRulesData: [
|
||||
`@layer mylayer`,
|
||||
`@container mycontainer (min-width: 3em)`,
|
||||
`@media screen`,
|
||||
`@container mycontainer (min-width: 4rem)`,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const rulesInView = Array.from(view.element.children);
|
||||
is(
|
||||
rulesInView.length,
|
||||
expectedRules.length,
|
||||
"All expected rules are displayed"
|
||||
);
|
||||
|
||||
for (let i = 0; i < expectedRules.length; i++) {
|
||||
const expectedRule = expectedRules[i];
|
||||
info(`Checking rule #${i}: ${expectedRule.selector}`);
|
||||
|
||||
const selector = rulesInView[i].querySelector(".ruleview-selectorcontainer")
|
||||
.innerText;
|
||||
is(selector, expectedRule.selector, `Expected selector for ${selector}`);
|
||||
|
||||
if (expectedRule.ancestorRulesData == null) {
|
||||
is(
|
||||
getRuleViewAncestorRulesDataElementByIndex(view, i),
|
||||
null,
|
||||
`No ancestor rules data displayed for ${selector}`
|
||||
);
|
||||
} else {
|
||||
is(
|
||||
getRuleViewAncestorRulesDataTextByIndex(view, i),
|
||||
expectedRule.ancestorRulesData.join("\n"),
|
||||
`Expected ancestor rules data displayed for ${selector}`
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -137,14 +137,22 @@ RuleEditor.prototype = {
|
|||
|
||||
if (this.rule.domRule.ancestorData.length) {
|
||||
const parts = this.rule.domRule.ancestorData.map(ancestorData => {
|
||||
if (ancestorData.type == "container") {
|
||||
const containerQueryParts = [
|
||||
"@container",
|
||||
ancestorData.containerName,
|
||||
ancestorData.containerQuery,
|
||||
].filter(p => !!p);
|
||||
return containerQueryParts.join(" ");
|
||||
}
|
||||
if (ancestorData.type == "layer") {
|
||||
return `@layer${ancestorData.value ? " " + ancestorData.value : ""}`;
|
||||
}
|
||||
if (ancestorData.type == "media") {
|
||||
return `@media ${ancestorData.value}`;
|
||||
}
|
||||
// We shouldn't get here as `type` can only be set to "layer" or "media", but just
|
||||
// in case, let's return an empty string.
|
||||
// We shouldn't get here as `type` can only be set to "container", "layer" or "media",
|
||||
// but just in case, let's return an empty string.
|
||||
console.warn("Unknown ancestor data type:", ancestorData.type);
|
||||
return ``;
|
||||
});
|
||||
|
|
|
@ -396,6 +396,16 @@ pieChart.loading=Loading
|
|||
# no data available, even after loading it.
|
||||
pieChart.unavailable=Empty
|
||||
|
||||
# LOCALIZATION NOTE (pieChart.ariaLabel): This is the text used for the aria-label attribute
|
||||
# for SVG pie charts (e.g., in the performance analysis view).
|
||||
pieChart.ariaLabel=Pie chart representing the size of each type of request in proportion to each other
|
||||
|
||||
# LOCALIZATION NOTE (pieChart.sliceAriaLabel): This is the text used for the aria-label attribute
|
||||
# for SVG pie charts slices (e.g., in the performance analysis view).
|
||||
# %1$S is the slice label (e.g. "html")
|
||||
# %2$S is the percentage (e.g. "33.23%").
|
||||
pieChart.sliceAriaLabel=%1$S: %2$S
|
||||
|
||||
# LOCALIZATION NOTE (tableChart.loading): This is the label displayed
|
||||
# for table charts (e.g., in the performance analysis view) when there is
|
||||
# no data available yet.
|
||||
|
@ -458,6 +468,11 @@ charts.totalCached=Cached responses: %S
|
|||
# in the performance analysis view for total requests.
|
||||
charts.totalCount=Total requests: %S
|
||||
|
||||
# LOCALIZATION NOTE (charts.requestsNumber): This is the label for the header column in
|
||||
# the performance analysis view for the number of requests. The label is not visible on screen,
|
||||
# but is set in the DOM for accessibility sake.
|
||||
charts.requestsNumber=Number of requests
|
||||
|
||||
# LOCALIZATION NOTE (charts.size): This is the label displayed
|
||||
# in the header column in the performance analysis view for size of the request.
|
||||
charts.size=Size
|
||||
|
|
|
@ -65,60 +65,52 @@
|
|||
}
|
||||
|
||||
.statistics-panel .table-chart-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-width: 240px;
|
||||
margin-inline-start: 1vw;
|
||||
margin-inline-end: 3vw;
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=html] {
|
||||
fill: var(--theme-highlight-bluegrey);
|
||||
background: var(--theme-highlight-bluegrey);
|
||||
tr[data-statistic-name] td:first-of-type {
|
||||
border-inline-start: 15px solid var(--stat-color);
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=css] {
|
||||
fill: var(--theme-highlight-blue);
|
||||
background: var(--theme-highlight-blue);
|
||||
path[data-statistic-name] {
|
||||
fill: var(--stat-color);
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=js] {
|
||||
fill: var(--theme-highlight-lightorange);
|
||||
background: var(--theme-highlight-lightorange);
|
||||
[data-statistic-name=html] {
|
||||
--stat-color: var(--theme-highlight-bluegrey);
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=xhr] {
|
||||
fill: var(--theme-highlight-orange);
|
||||
background: var(--theme-highlight-orange);
|
||||
[data-statistic-name=css] {
|
||||
--stat-color: var(--theme-highlight-blue);
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=fonts] {
|
||||
fill: var(--theme-highlight-purple);
|
||||
background: var(--theme-highlight-purple);
|
||||
[data-statistic-name=js] {
|
||||
--stat-color: var(--theme-highlight-lightorange);
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=images] {
|
||||
fill: var(--theme-highlight-pink);
|
||||
background: var(--theme-highlight-pink);
|
||||
[data-statistic-name=xhr] {
|
||||
--stat-color: var(--theme-highlight-orange);
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=media] {
|
||||
fill: var(--theme-highlight-green);
|
||||
background: var(--theme-highlight-green);
|
||||
[data-statistic-name=fonts] {
|
||||
--stat-color: var(--theme-highlight-purple);
|
||||
}
|
||||
|
||||
.chart-colored-blob[name=flash] {
|
||||
fill: var(--theme-highlight-red);
|
||||
background: var(--theme-highlight-red);
|
||||
[data-statistic-name=images] {
|
||||
--stat-color: var(--theme-highlight-pink);
|
||||
}
|
||||
|
||||
.table-chart-row {
|
||||
display: flex;
|
||||
[data-statistic-name=media] {
|
||||
--stat-color: var(--theme-highlight-green);
|
||||
}
|
||||
|
||||
.table-chart-row-label[name=cached] {
|
||||
display: none;
|
||||
/*
|
||||
* Align cell text to the center by default.
|
||||
*/
|
||||
.table-chart-row-label {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.table-chart-row-label[name=count] {
|
||||
|
@ -169,3 +161,11 @@
|
|||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.offscreen{
|
||||
position: absolute!important;
|
||||
font-size: 0px;
|
||||
overflow: hidden;
|
||||
clip: rect(1px,1px,1px,1px);
|
||||
clip-path: polygon(0 0,0 0,0 0,0 0);
|
||||
}
|
||||
|
|
|
@ -173,8 +173,7 @@ class StatisticsPanel extends Component {
|
|||
diameter: NETWORK_ANALYSIS_PIE_CHART_DIAMETER,
|
||||
title,
|
||||
header: {
|
||||
cached: "",
|
||||
count: "",
|
||||
count: L10N.getStr("charts.requestsNumber"),
|
||||
label: L10N.getStr("charts.type"),
|
||||
size: L10N.getStr("charts.size"),
|
||||
transferredSize: L10N.getStr("charts.transferred"),
|
||||
|
|
|
@ -264,7 +264,6 @@ skip-if = true #Bug 1667115
|
|||
[browser_net_statistics-01.js]
|
||||
skip-if = true # Bug 1373558
|
||||
[browser_net_statistics-02.js]
|
||||
fail-if = a11y_checks # bug 1687789 interactive char is not accessible
|
||||
[browser_net_status-bar-transferred-size.js]
|
||||
[browser_net_status-bar.js]
|
||||
skip-if = win10_2004 # Bug 1723573
|
||||
|
@ -282,7 +281,7 @@ skip-if = true # TODO: fix the test
|
|||
[browser_net_timing-division.js]
|
||||
[browser_net_tracking-resources.js]
|
||||
[browser_net_truncate-post-data.js]
|
||||
skip-if =
|
||||
skip-if =
|
||||
os == 'win' && os_version == '6.1' && !debug # Bug 1547150
|
||||
socketprocess_networking # Bug 1772211
|
||||
[browser_net_truncate.js]
|
||||
|
|
|
@ -41,7 +41,8 @@ add_task(async function() {
|
|||
});
|
||||
|
||||
const { node } = pie;
|
||||
const slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
|
||||
const slicesContainer = node.querySelectorAll(".pie-chart-slice-container");
|
||||
const slices = node.querySelectorAll(".pie-chart-slice");
|
||||
const labels = node.querySelectorAll(".pie-chart-label");
|
||||
|
||||
ok(
|
||||
|
@ -49,6 +50,17 @@ add_task(async function() {
|
|||
node.classList.contains("generic-chart-container"),
|
||||
"A pie chart container was created successfully."
|
||||
);
|
||||
is(
|
||||
node.getAttribute("aria-label"),
|
||||
"Pie chart representing the size of each type of request in proportion to each other",
|
||||
"pie chart container has expected aria-label"
|
||||
);
|
||||
|
||||
is(
|
||||
slicesContainer.length,
|
||||
3,
|
||||
"There should be 3 pie chart slices container created."
|
||||
);
|
||||
|
||||
is(slices.length, 3, "There should be 3 pie chart slices created.");
|
||||
ok(
|
||||
|
@ -76,6 +88,22 @@ add_task(async function() {
|
|||
"The third slice has the correct data."
|
||||
);
|
||||
|
||||
is(
|
||||
slicesContainer[0].getAttribute("aria-label"),
|
||||
"baz: 50%",
|
||||
"First slice container has expected aria-label"
|
||||
);
|
||||
is(
|
||||
slicesContainer[1].getAttribute("aria-label"),
|
||||
"bar: 33.33%",
|
||||
"Second slice container has expected aria-label"
|
||||
);
|
||||
is(
|
||||
slicesContainer[2].getAttribute("aria-label"),
|
||||
"foo: 16.67%",
|
||||
"Third slice container has expected aria-label"
|
||||
);
|
||||
|
||||
ok(
|
||||
slices[0].hasAttribute("largest"),
|
||||
"The first slice should be the largest one."
|
||||
|
@ -86,17 +114,17 @@ add_task(async function() {
|
|||
);
|
||||
|
||||
is(
|
||||
slices[0].getAttribute("name"),
|
||||
slices[0].getAttribute("data-statistic-name"),
|
||||
"baz",
|
||||
"The first slice's name is correct."
|
||||
);
|
||||
is(
|
||||
slices[1].getAttribute("name"),
|
||||
slices[1].getAttribute("data-statistic-name"),
|
||||
"bar",
|
||||
"The first slice's name is correct."
|
||||
);
|
||||
is(
|
||||
slices[2].getAttribute("name"),
|
||||
slices[2].getAttribute("data-statistic-name"),
|
||||
"foo",
|
||||
"The first slice's name is correct."
|
||||
);
|
||||
|
@ -105,6 +133,21 @@ add_task(async function() {
|
|||
is(labels[0].textContent, "baz", "The first label's text is correct.");
|
||||
is(labels[1].textContent, "bar", "The first label's text is correct.");
|
||||
is(labels[2].textContent, "foo", "The first label's text is correct.");
|
||||
is(
|
||||
labels[0].getAttribute("aria-hidden"),
|
||||
"true",
|
||||
"The first label has aria-hidden."
|
||||
);
|
||||
is(
|
||||
labels[1].getAttribute("aria-hidden"),
|
||||
"true",
|
||||
"The first label has aria-hidden."
|
||||
);
|
||||
is(
|
||||
labels[2].getAttribute("aria-hidden"),
|
||||
"true",
|
||||
"The first label has aria-hidden."
|
||||
);
|
||||
|
||||
await teardown(monitor);
|
||||
});
|
||||
|
|
|
@ -30,7 +30,7 @@ add_task(async function() {
|
|||
});
|
||||
|
||||
const { node } = pie;
|
||||
const slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
|
||||
const slices = node.querySelectorAll(".pie-chart-slice");
|
||||
const labels = node.querySelectorAll(".pie-chart-label");
|
||||
|
||||
ok(
|
||||
|
@ -58,7 +58,7 @@ add_task(async function() {
|
|||
"The first slice should also be the smallest one."
|
||||
);
|
||||
is(
|
||||
slices[0].getAttribute("name"),
|
||||
slices[0].getAttribute("data-statistic-name"),
|
||||
L10N.getStr("pieChart.loading"),
|
||||
"The first slice's name is correct."
|
||||
);
|
||||
|
|
|
@ -78,97 +78,85 @@ add_task(async function() {
|
|||
);
|
||||
|
||||
is(
|
||||
rows[0].querySelectorAll("span")[0].getAttribute("name"),
|
||||
rows[0].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"),
|
||||
"label1",
|
||||
"The first column of the header exists."
|
||||
);
|
||||
is(
|
||||
rows[0].querySelectorAll("span")[1].getAttribute("name"),
|
||||
rows[0].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"),
|
||||
"label2",
|
||||
"The second column of the header exists."
|
||||
);
|
||||
is(
|
||||
rows[0].querySelectorAll("span")[0].textContent,
|
||||
rows[0].querySelectorAll(".table-chart-row-label")[0].textContent,
|
||||
"label1header",
|
||||
"The first column of the header displays the correct text."
|
||||
);
|
||||
is(
|
||||
rows[0].querySelectorAll("span")[1].textContent,
|
||||
rows[0].querySelectorAll(".table-chart-row-label")[1].textContent,
|
||||
"label2header",
|
||||
"The second column of the header displays the correct text."
|
||||
);
|
||||
|
||||
ok(
|
||||
rows[1].querySelector(".table-chart-row-box.chart-colored-blob"),
|
||||
"A colored blob exists for the first row."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[0].getAttribute("name"),
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"),
|
||||
"label1",
|
||||
"The first column of the first row exists."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[1].getAttribute("name"),
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"),
|
||||
"label2",
|
||||
"The second column of the first row exists."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[0].textContent,
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[0].textContent,
|
||||
"1",
|
||||
"The first column of the first row displays the correct text."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[1].textContent,
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[1].textContent,
|
||||
"11.1foo",
|
||||
"The second column of the first row displays the correct text."
|
||||
);
|
||||
|
||||
ok(
|
||||
rows[2].querySelector(".table-chart-row-box.chart-colored-blob"),
|
||||
"A colored blob exists for the second row."
|
||||
);
|
||||
is(
|
||||
rows[2].querySelectorAll("span")[0].getAttribute("name"),
|
||||
rows[2].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"),
|
||||
"label1",
|
||||
"The first column of the second row exists."
|
||||
);
|
||||
is(
|
||||
rows[2].querySelectorAll("span")[1].getAttribute("name"),
|
||||
rows[2].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"),
|
||||
"label2",
|
||||
"The second column of the second row exists."
|
||||
);
|
||||
is(
|
||||
rows[2].querySelectorAll("span")[0].textContent,
|
||||
rows[2].querySelectorAll(".table-chart-row-label")[0].textContent,
|
||||
"2",
|
||||
"The first column of the second row displays the correct text."
|
||||
);
|
||||
is(
|
||||
rows[2].querySelectorAll("span")[1].textContent,
|
||||
rows[2].querySelectorAll(".table-chart-row-label")[1].textContent,
|
||||
"12.2bar",
|
||||
"The second column of the first row displays the correct text."
|
||||
);
|
||||
|
||||
ok(
|
||||
rows[3].querySelector(".table-chart-row-box.chart-colored-blob"),
|
||||
"A colored blob exists for the third row."
|
||||
);
|
||||
is(
|
||||
rows[3].querySelectorAll("span")[0].getAttribute("name"),
|
||||
rows[3].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"),
|
||||
"label1",
|
||||
"The first column of the third row exists."
|
||||
);
|
||||
is(
|
||||
rows[3].querySelectorAll("span")[1].getAttribute("name"),
|
||||
rows[3].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"),
|
||||
"label2",
|
||||
"The second column of the third row exists."
|
||||
);
|
||||
is(
|
||||
rows[3].querySelectorAll("span")[0].textContent,
|
||||
rows[3].querySelectorAll(".table-chart-row-label")[0].textContent,
|
||||
"3",
|
||||
"The first column of the third row displays the correct text."
|
||||
);
|
||||
is(
|
||||
rows[3].querySelectorAll("span")[1].textContent,
|
||||
rows[3].querySelectorAll(".table-chart-row-label")[1].textContent,
|
||||
"13.3baz",
|
||||
"The second column of the third row displays the correct text."
|
||||
);
|
||||
|
|
|
@ -62,27 +62,23 @@ add_task(async function() {
|
|||
"There should be 1 table chart row and a 1 header created."
|
||||
);
|
||||
|
||||
ok(
|
||||
rows[1].querySelector(".table-chart-row-box.chart-colored-blob"),
|
||||
"A colored blob exists for the first row."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[0].getAttribute("name"),
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"),
|
||||
"size",
|
||||
"The first column of the first row exists."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[1].getAttribute("name"),
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"),
|
||||
"label",
|
||||
"The second column of the first row exists."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[0].textContent,
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[0].textContent,
|
||||
"",
|
||||
"The first column of the first row displays the correct text."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[1].textContent,
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[1].textContent,
|
||||
L10N.getStr("tableChart.loading"),
|
||||
"The second column of the first row displays the correct text."
|
||||
);
|
||||
|
|
|
@ -29,7 +29,7 @@ add_task(async function() {
|
|||
});
|
||||
|
||||
const { node } = pie;
|
||||
const slices = node.querySelectorAll(".pie-chart-slice.chart-colored-blob");
|
||||
const slices = node.querySelectorAll(".pie-chart-slice");
|
||||
const labels = node.querySelectorAll(".pie-chart-label");
|
||||
|
||||
is(slices.length, 1, "There should be 1 pie chart slice created.");
|
||||
|
@ -48,7 +48,7 @@ add_task(async function() {
|
|||
"The slice should also be the smallest one."
|
||||
);
|
||||
is(
|
||||
slices[0].getAttribute("name"),
|
||||
slices[0].getAttribute("data-statistic-name"),
|
||||
L10N.getStr("pieChart.unavailable"),
|
||||
"The slice's name is correct."
|
||||
);
|
||||
|
|
|
@ -42,27 +42,23 @@ add_task(async function() {
|
|||
|
||||
is(rows.length, 2, "There should be 1 table chart row and 1 header created.");
|
||||
|
||||
ok(
|
||||
rows[1].querySelector(".table-chart-row-box.chart-colored-blob"),
|
||||
"A colored blob exists for the first row."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[0].getAttribute("name"),
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[0].getAttribute("name"),
|
||||
"size",
|
||||
"The first column of the first row exists."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[1].getAttribute("name"),
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[1].getAttribute("name"),
|
||||
"label",
|
||||
"The second column of the first row exists."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[0].textContent,
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[0].textContent,
|
||||
"",
|
||||
"The first column of the first row displays the correct text."
|
||||
);
|
||||
is(
|
||||
rows[1].querySelectorAll("span")[1].textContent,
|
||||
rows[1].querySelectorAll(".table-chart-row-label")[1].textContent,
|
||||
L10N.getStr("tableChart.unavailable"),
|
||||
"The second column of the first row displays the correct text."
|
||||
);
|
||||
|
|
|
@ -57,7 +57,7 @@ add_task(async function() {
|
|||
|
||||
EventUtils.sendMouseEvent(
|
||||
{ type: "click" },
|
||||
document.querySelector(".pie-chart-slice")
|
||||
document.querySelector(".pie-chart-slice-container")
|
||||
);
|
||||
|
||||
ok(
|
||||
|
|
|
@ -69,20 +69,9 @@ define(function(require, exports, module) {
|
|||
);
|
||||
}
|
||||
|
||||
function createGripMapEntry(key, value) {
|
||||
return {
|
||||
type: "mapEntry",
|
||||
preview: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Exports from this module
|
||||
module.exports = {
|
||||
rep: wrapRender(GripEntry),
|
||||
createGripMapEntry,
|
||||
supportsObject,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -14,7 +14,9 @@ const {
|
|||
|
||||
const { shouldLoadItemIndexedProperties } = Utils.loadProperties;
|
||||
|
||||
const GripEntryRep = require("devtools/client/shared/components/reps/reps/grip-entry");
|
||||
const {
|
||||
createGripMapEntry,
|
||||
} = require("devtools/client/shared/components/test/node/components/reps/test-helpers");
|
||||
const accessorStubs = require("devtools/client/shared/components/test/node/stubs/reps/accessor");
|
||||
const gripMapStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-map");
|
||||
const gripArrayStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-array");
|
||||
|
@ -189,7 +191,7 @@ describe("shouldLoadItemIndexedProperties", () => {
|
|||
});
|
||||
|
||||
it("returns false for a MapEntry node", () => {
|
||||
const node = GripEntryRep.createGripMapEntry("key", "value");
|
||||
const node = createGripMapEntry("key", "value");
|
||||
expect(shouldLoadItemIndexedProperties(node)).toBeFalsy();
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,9 @@ const {
|
|||
|
||||
const { shouldLoadItemNonIndexedProperties } = Utils.loadProperties;
|
||||
|
||||
const GripEntryRep = require("devtools/client/shared/components/reps/reps/grip-entry");
|
||||
const {
|
||||
createGripMapEntry,
|
||||
} = require("devtools/client/shared/components/test/node/components/reps/test-helpers");
|
||||
const accessorStubs = require("devtools/client/shared/components/test/node/stubs/reps/accessor");
|
||||
const gripMapStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-map");
|
||||
const gripArrayStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-array");
|
||||
|
@ -152,7 +154,7 @@ describe("shouldLoadItemNonIndexedProperties", () => {
|
|||
});
|
||||
|
||||
it("returns false for a MapEntry node", () => {
|
||||
const node = GripEntryRep.createGripMapEntry("key", "value");
|
||||
const node = createGripMapEntry("key", "value");
|
||||
expect(shouldLoadItemNonIndexedProperties(node)).toBeFalsy();
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,9 @@ const {
|
|||
|
||||
const { shouldLoadItemPrototype } = Utils.loadProperties;
|
||||
|
||||
const GripEntryRep = require("devtools/client/shared/components/reps/reps/grip-entry");
|
||||
const {
|
||||
createGripMapEntry,
|
||||
} = require("devtools/client/shared/components/test/node/components/reps/test-helpers");
|
||||
const accessorStubs = require("devtools/client/shared/components/test/node/stubs/reps/accessor");
|
||||
const gripMapStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-map");
|
||||
const gripArrayStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-array");
|
||||
|
@ -148,7 +150,7 @@ describe("shouldLoadItemPrototype", () => {
|
|||
});
|
||||
|
||||
it("returns false for a MapEntry node", () => {
|
||||
const node = GripEntryRep.createGripMapEntry("key", "value");
|
||||
const node = createGripMapEntry("key", "value");
|
||||
expect(shouldLoadItemPrototype(node)).toBeFalsy();
|
||||
});
|
||||
|
||||
|
|
|
@ -14,7 +14,9 @@ const {
|
|||
|
||||
const { shouldLoadItemSymbols } = Utils.loadProperties;
|
||||
|
||||
const GripEntryRep = require("devtools/client/shared/components/reps/reps/grip-entry");
|
||||
const {
|
||||
createGripMapEntry,
|
||||
} = require("devtools/client/shared/components/test/node/components/reps/test-helpers");
|
||||
const accessorStubs = require("devtools/client/shared/components/test/node/stubs/reps/accessor");
|
||||
const gripMapStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-map");
|
||||
const gripArrayStubs = require("devtools/client/shared/components/test/node/stubs/reps/grip-array");
|
||||
|
@ -148,7 +150,7 @@ describe("shouldLoadItemSymbols", () => {
|
|||
});
|
||||
|
||||
it("returns false for a MapEntry node", () => {
|
||||
const node = GripEntryRep.createGripMapEntry("key", "value");
|
||||
const node = createGripMapEntry("key", "value");
|
||||
expect(shouldLoadItemSymbols(node)).toBeFalsy();
|
||||
});
|
||||
|
||||
|
|
|
@ -13,11 +13,11 @@ const {
|
|||
} = require("devtools/client/shared/components/reps/reps/rep");
|
||||
|
||||
const { GripEntry } = REPS;
|
||||
const { createGripMapEntry } = GripEntry;
|
||||
const {
|
||||
MODE,
|
||||
} = require("devtools/client/shared/components/reps/reps/constants");
|
||||
const {
|
||||
createGripMapEntry,
|
||||
getGripLengthBubbleText,
|
||||
} = require("devtools/client/shared/components/test/node/components/reps/test-helpers");
|
||||
|
||||
|
|
|
@ -97,7 +97,18 @@ function getMapLengthBubbleText(object, props) {
|
|||
});
|
||||
}
|
||||
|
||||
function createGripMapEntry(key, value) {
|
||||
return {
|
||||
type: "mapEntry",
|
||||
preview: {
|
||||
key,
|
||||
value,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createGripMapEntry,
|
||||
expectActorAttribute,
|
||||
getSelectableInInspectorGrips,
|
||||
getGripLengthBubbleText,
|
||||
|
|
113
devtools/client/shared/widgets/Chart.js
vendored
113
devtools/client/shared/widgets/Chart.js
vendored
|
@ -197,9 +197,6 @@ function createPieChart(
|
|||
radius = radius || (width + height) / 4;
|
||||
let isPlaceholder = false;
|
||||
|
||||
// Filter out very small sizes, as they'll just render invisible slices.
|
||||
data = data ? data.filter(e => e.size > EPSILON) : null;
|
||||
|
||||
// If there's no data available, display an empty placeholder.
|
||||
if (!data) {
|
||||
data = loadingPieChartData();
|
||||
|
@ -215,13 +212,18 @@ function createPieChart(
|
|||
"class",
|
||||
"generic-chart-container pie-chart-container"
|
||||
);
|
||||
container.setAttribute("pack", "center");
|
||||
container.setAttribute("flex", "1");
|
||||
|
||||
container.setAttribute("width", width);
|
||||
container.setAttribute("height", height);
|
||||
container.setAttribute("viewBox", "0 0 " + width + " " + height);
|
||||
container.setAttribute("slices", data.length);
|
||||
container.setAttribute("placeholder", isPlaceholder);
|
||||
container.setAttribute("role", "group");
|
||||
container.setAttribute("aria-label", L10N.getStr("pieChart.ariaLabel"));
|
||||
|
||||
const slicesGroup = document.createElementNS(SVG_NS, "g");
|
||||
slicesGroup.setAttribute("role", "list");
|
||||
container.append(slicesGroup);
|
||||
|
||||
const proxy = new PieChart(container);
|
||||
|
||||
|
@ -240,10 +242,38 @@ function createPieChart(
|
|||
for (let i = data.length - 1; i >= 0; i--) {
|
||||
const sliceInfo = data[i];
|
||||
const sliceAngle = angles[i];
|
||||
if (!sliceInfo.size || sliceAngle < EPSILON) {
|
||||
continue;
|
||||
|
||||
const sliceNode = document.createElementNS(SVG_NS, "g");
|
||||
sliceNode.setAttribute("role", "listitem");
|
||||
slicesGroup.append(sliceNode);
|
||||
|
||||
const interactiveNodeId = `${sliceInfo.label}-slice`;
|
||||
const textNodeId = `${sliceInfo.label}-slice-label`;
|
||||
|
||||
// The only way to make this keyboard accessible is to have a link
|
||||
const interactiveNode = document.createElementNS(SVG_NS, "a");
|
||||
interactiveNode.setAttribute("id", interactiveNodeId);
|
||||
interactiveNode.setAttribute("xlink:href", `#${interactiveNodeId}`);
|
||||
interactiveNode.setAttribute("tabindex", `0`);
|
||||
interactiveNode.setAttribute("role", `button`);
|
||||
interactiveNode.classList.add("pie-chart-slice-container");
|
||||
if (!isPlaceholder) {
|
||||
interactiveNode.setAttribute(
|
||||
"aria-label",
|
||||
L10N.getFormatStr(
|
||||
"pieChart.sliceAriaLabel",
|
||||
sliceInfo.label,
|
||||
new Intl.NumberFormat(undefined, {
|
||||
style: "unit",
|
||||
unit: "percent",
|
||||
maximumFractionDigits: 2,
|
||||
}).format((sliceInfo.size / total) * 100)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
sliceNode.append(interactiveNode);
|
||||
|
||||
endAngle = startAngle - sliceAngle;
|
||||
midAngle = (startAngle + endAngle) / 2;
|
||||
|
||||
|
@ -254,8 +284,8 @@ function createPieChart(
|
|||
const largeArcFlag = Math.abs(startAngle - endAngle) > PI ? 1 : 0;
|
||||
|
||||
const pathNode = document.createElementNS(SVG_NS, "path");
|
||||
pathNode.setAttribute("class", "pie-chart-slice chart-colored-blob");
|
||||
pathNode.setAttribute("name", sliceInfo.label);
|
||||
pathNode.classList.add("pie-chart-slice");
|
||||
pathNode.setAttribute("data-statistic-name", sliceInfo.label);
|
||||
pathNode.setAttribute(
|
||||
"d",
|
||||
" M " +
|
||||
|
@ -293,19 +323,30 @@ function createPieChart(
|
|||
pathNode.setAttribute("style", data.length > 1 ? hoverTransform : "");
|
||||
|
||||
proxy.slices.set(sliceInfo, pathNode);
|
||||
delegate(proxy, ["click", "mouseover", "mouseout"], pathNode, sliceInfo);
|
||||
container.appendChild(pathNode);
|
||||
delegate(
|
||||
proxy,
|
||||
["click", "mouseover", "mouseout", "focus"],
|
||||
interactiveNode,
|
||||
sliceInfo
|
||||
);
|
||||
interactiveNode.appendChild(pathNode);
|
||||
|
||||
if (sliceInfo.label && sliceAngle > NAMED_SLICE_MIN_ANGLE) {
|
||||
const textX = centerX + textDistance * Math.sin(midAngle);
|
||||
const textY = centerY - textDistance * Math.cos(midAngle);
|
||||
const textX = centerX + textDistance * Math.sin(midAngle);
|
||||
const textY = centerY - textDistance * Math.cos(midAngle);
|
||||
|
||||
// Don't add the label if the slice isn't large enough so it doesn't look cramped.
|
||||
if (sliceAngle >= NAMED_SLICE_MIN_ANGLE) {
|
||||
const label = document.createElementNS(SVG_NS, "text");
|
||||
label.appendChild(document.createTextNode(sliceInfo.label));
|
||||
label.setAttribute("id", textNodeId);
|
||||
// A label is already set on `interactiveNode`, so hide this from the accessibility tree
|
||||
// to avoid duplicating text.
|
||||
label.setAttribute("aria-hidden", "true");
|
||||
label.setAttribute("class", "pie-chart-label");
|
||||
label.setAttribute("style", data.length > 1 ? hoverTransform : "");
|
||||
label.setAttribute("x", data.length > 1 ? textX : centerX);
|
||||
label.setAttribute("y", data.length > 1 ? textY : centerY);
|
||||
container.appendChild(label);
|
||||
interactiveNode.append(label);
|
||||
}
|
||||
|
||||
startAngle = endAngle;
|
||||
|
@ -376,11 +417,7 @@ function createTableChart(document, { title, data, strings, totals, header }) {
|
|||
|
||||
const container = document.createElement("div");
|
||||
container.className = "generic-chart-container table-chart-container";
|
||||
container.setAttribute("pack", "center");
|
||||
container.setAttribute("flex", "1");
|
||||
container.setAttribute("rows", data.length);
|
||||
container.setAttribute("placeholder", isPlaceholder);
|
||||
container.setAttribute("style", "-moz-box-orient: vertical");
|
||||
|
||||
const proxy = new TableChart(container);
|
||||
|
||||
|
@ -389,43 +426,46 @@ function createTableChart(document, { title, data, strings, totals, header }) {
|
|||
titleNode.textContent = title;
|
||||
container.appendChild(titleNode);
|
||||
|
||||
const tableNode = document.createElement("div");
|
||||
const tableNode = document.createElement("table");
|
||||
tableNode.className = "plain table-chart-grid";
|
||||
tableNode.setAttribute("style", "-moz-box-orient: vertical");
|
||||
container.appendChild(tableNode);
|
||||
|
||||
const headerNode = document.createElement("div");
|
||||
const headerNode = document.createElement("thead");
|
||||
headerNode.className = "table-chart-row";
|
||||
|
||||
const headerBoxNode = document.createElement("div");
|
||||
const bodyNode = document.createElement("tbody");
|
||||
|
||||
const headerBoxNode = document.createElement("tr");
|
||||
headerBoxNode.className = "table-chart-row-box";
|
||||
headerNode.appendChild(headerBoxNode);
|
||||
|
||||
for (const [key, value] of Object.entries(header)) {
|
||||
const headerLabelNode = document.createElement("span");
|
||||
const headerLabelNode = document.createElement("th");
|
||||
headerLabelNode.className = "plain table-chart-row-label";
|
||||
headerLabelNode.setAttribute("name", key);
|
||||
headerLabelNode.textContent = value;
|
||||
|
||||
headerNode.appendChild(headerLabelNode);
|
||||
if (key == "count") {
|
||||
headerLabelNode.classList.add("offscreen");
|
||||
}
|
||||
headerBoxNode.appendChild(headerLabelNode);
|
||||
}
|
||||
|
||||
tableNode.appendChild(headerNode);
|
||||
tableNode.append(headerNode, bodyNode);
|
||||
|
||||
for (const rowInfo of data) {
|
||||
const rowNode = document.createElement("div");
|
||||
const rowNode = document.createElement("tr");
|
||||
rowNode.className = "table-chart-row";
|
||||
rowNode.setAttribute("align", "center");
|
||||
|
||||
const boxNode = document.createElement("div");
|
||||
boxNode.className = "table-chart-row-box chart-colored-blob";
|
||||
boxNode.setAttribute("name", rowInfo.label);
|
||||
rowNode.appendChild(boxNode);
|
||||
rowNode.setAttribute("data-statistic-name", rowInfo.label);
|
||||
|
||||
for (const [key, value] of Object.entries(rowInfo)) {
|
||||
// Don't render the "cached" column. We only have it in here so it can be displayed
|
||||
// in the `totals` section.
|
||||
if (key == "cached") {
|
||||
continue;
|
||||
}
|
||||
const index = data.indexOf(rowInfo);
|
||||
const stringified = strings[key] ? strings[key](value, index) : value;
|
||||
const labelNode = document.createElement("span");
|
||||
const labelNode = document.createElement("td");
|
||||
labelNode.className = "plain table-chart-row-label";
|
||||
labelNode.setAttribute("name", key);
|
||||
labelNode.textContent = stringified;
|
||||
|
@ -434,12 +474,11 @@ function createTableChart(document, { title, data, strings, totals, header }) {
|
|||
|
||||
proxy.rows.set(rowInfo, rowNode);
|
||||
delegate(proxy, ["click", "mouseover", "mouseout"], rowNode, rowInfo);
|
||||
tableNode.appendChild(rowNode);
|
||||
bodyNode.appendChild(rowNode);
|
||||
}
|
||||
|
||||
const totalsNode = document.createElement("div");
|
||||
totalsNode.className = "table-chart-totals";
|
||||
totalsNode.setAttribute("style", "-moz-box-orient: vertical");
|
||||
|
||||
for (const [key, value] of Object.entries(totals)) {
|
||||
const total = data.reduce((acc, e) => acc + e[key], 0);
|
||||
|
|
|
@ -87,16 +87,16 @@
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table-chart-grid:hover > .table-chart-row {
|
||||
.table-chart-grid:hover .table-chart-row {
|
||||
transition: opacity 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.table-chart-grid:not(:hover) > .table-chart-row {
|
||||
.table-chart-grid:not(:hover) .table-chart-row {
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.generic-chart-container:hover > .table-chart-grid:hover > .table-chart-row:not(:hover),
|
||||
.generic-chart-container:hover ~ .table-chart-container > .table-chart-grid > .table-chart-row:not([focused]) {
|
||||
.generic-chart-container:hover > .table-chart-grid:hover .table-chart-row:not(:hover),
|
||||
.generic-chart-container:hover ~ .table-chart-container > .table-chart-grid .table-chart-row:not([focused]) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
|
|
|
@ -333,6 +333,16 @@ const StyleRuleActor = protocol.ActorClassWithSpec(styleRuleSpec, {
|
|||
type: "layer",
|
||||
value: ancestorRule.rawRule.name,
|
||||
});
|
||||
} else if (
|
||||
ChromeUtils.getClassName(ancestorRule.rawRule) === "CSSContainerRule"
|
||||
) {
|
||||
form.ancestorData.push({
|
||||
type: "container",
|
||||
// Send containerName and containerQuery separately (instead of conditionText)
|
||||
// so the client has more flexibility to display the information.
|
||||
containerName: ancestorRule.rawRule.containerName,
|
||||
containerQuery: ancestorRule.rawRule.containerQuery,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -253,7 +253,17 @@ nsIFrame* nsIContent::GetPrimaryFrame(mozilla::FlushType aType) {
|
|||
doc->FlushPendingNotifications(aType);
|
||||
}
|
||||
|
||||
return GetPrimaryFrame();
|
||||
auto* frame = GetPrimaryFrame();
|
||||
if (!frame) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (aType == mozilla::FlushType::Layout) {
|
||||
frame->PresShell()->EnsureReflowIfFrameHasHiddenContent(frame);
|
||||
frame = GetPrimaryFrame();
|
||||
}
|
||||
|
||||
return frame;
|
||||
}
|
||||
|
||||
namespace mozilla::dom {
|
||||
|
|
|
@ -60,7 +60,9 @@ DOMHighResTimeStamp IdleDeadline::TimeRemaining() {
|
|||
return 0.0;
|
||||
}
|
||||
|
||||
return std::max(mDeadline - performance->Now(), 0.0);
|
||||
// The web API doesn't expect deadlines > 50ms, but conversion from the
|
||||
// internal API may lead to some rounding errors.
|
||||
return std::min(std::max(mDeadline - performance->Now(), 0.0), 50.0);
|
||||
}
|
||||
|
||||
// If there's no window, we're in a system scope, and can just use
|
||||
|
|
|
@ -22142,38 +22142,42 @@ class CGIterableMethodGenerator(CGGeneric):
|
|||
),
|
||||
)
|
||||
return
|
||||
|
||||
if descriptor.interface.isIterable():
|
||||
CGGeneric.__init__(
|
||||
self,
|
||||
fill(
|
||||
"""
|
||||
typedef ${iterClass} itrType;
|
||||
RefPtr<itrType> result(new itrType(self,
|
||||
itrType::IteratorType::${itrMethod},
|
||||
&${ifaceName}Iterator_Binding::Wrap));
|
||||
""",
|
||||
iterClass=iteratorNativeType(descriptor),
|
||||
ifaceName=descriptor.interface.identifier.name,
|
||||
itrMethod=methodName.title(),
|
||||
),
|
||||
)
|
||||
binding = descriptor.interface.identifier.name + "Iterator_Binding"
|
||||
init = ""
|
||||
else:
|
||||
assert descriptor.interface.isAsyncIterable()
|
||||
CGGeneric.__init__(
|
||||
self,
|
||||
fill(
|
||||
"""
|
||||
|
||||
binding = descriptor.interface.identifier.name + "AsyncIterator_Binding"
|
||||
init = fill(
|
||||
"""
|
||||
{
|
||||
ErrorResult initError;
|
||||
self->InitAsyncIterator(result.get(), initError);
|
||||
if (initError.MaybeSetPendingException(cx, "Asynchronous iterator initialization steps for ${ifaceName} failed")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
""",
|
||||
ifaceName=descriptor.interface.identifier.name,
|
||||
)
|
||||
CGGeneric.__init__(
|
||||
self,
|
||||
fill(
|
||||
"""
|
||||
typedef ${iterClass} itrType;
|
||||
RefPtr<itrType> result(new itrType(self,
|
||||
itrType::IteratorType::${itrMethod},
|
||||
&${ifaceName}AsyncIterator_Binding::Wrap));
|
||||
self->InitAsyncIterator(result.get());
|
||||
&${binding}::Wrap));
|
||||
$*{init}
|
||||
""",
|
||||
iterClass=iteratorNativeType(descriptor),
|
||||
ifaceName=descriptor.interface.identifier.name,
|
||||
itrMethod=methodName.title(),
|
||||
),
|
||||
)
|
||||
iterClass=iteratorNativeType(descriptor),
|
||||
itrMethod=methodName.title(),
|
||||
binding=binding,
|
||||
init=init,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def getObservableArrayBackingObject(descriptor, attr, errorReturn="return false;\n"):
|
||||
|
|
|
@ -54,7 +54,8 @@ nsPIDOMWindowInner* TestInterfaceAsyncIterableDouble::GetParentObject() const {
|
|||
return mParent;
|
||||
}
|
||||
|
||||
void TestInterfaceAsyncIterableDouble::InitAsyncIterator(Iterator* aIterator) {
|
||||
void TestInterfaceAsyncIterableDouble::InitAsyncIterator(Iterator* aIterator,
|
||||
ErrorResult& aError) {
|
||||
UniquePtr<IteratorData> data(new IteratorData(0));
|
||||
aIterator->SetData((void*)data.release());
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ class TestInterfaceAsyncIterableDouble final : public nsISupports,
|
|||
const GlobalObject& aGlobal, ErrorResult& rv);
|
||||
|
||||
using Iterator = AsyncIterableIterator<TestInterfaceAsyncIterableDouble>;
|
||||
void InitAsyncIterator(Iterator* aIterator);
|
||||
void InitAsyncIterator(Iterator* aIterator, ErrorResult& aError);
|
||||
void DestroyAsyncIterator(Iterator* aIterator);
|
||||
already_AddRefed<Promise> GetNextPromise(JSContext* aCx, Iterator* aIterator,
|
||||
ErrorResult& aRv);
|
||||
|
|
|
@ -61,7 +61,7 @@ nsPIDOMWindowInner* TestInterfaceAsyncIterableDoubleUnion::GetParentObject()
|
|||
}
|
||||
|
||||
void TestInterfaceAsyncIterableDoubleUnion::InitAsyncIterator(
|
||||
Iterator* aIterator) {
|
||||
Iterator* aIterator, ErrorResult& aError) {
|
||||
UniquePtr<IteratorData> data(new IteratorData(0));
|
||||
aIterator->SetData((void*)data.release());
|
||||
}
|
||||
|
|
|
@ -39,7 +39,7 @@ class TestInterfaceAsyncIterableDoubleUnion final : public nsISupports,
|
|||
const GlobalObject& aGlobal, ErrorResult& rv);
|
||||
|
||||
using Iterator = AsyncIterableIterator<TestInterfaceAsyncIterableDoubleUnion>;
|
||||
void InitAsyncIterator(Iterator* aIterator);
|
||||
void InitAsyncIterator(Iterator* aIterator, ErrorResult& aError);
|
||||
void DestroyAsyncIterator(Iterator* aIterator);
|
||||
already_AddRefed<Promise> GetNextPromise(JSContext* aCx, Iterator* aIterator,
|
||||
ErrorResult& aRv);
|
||||
|
|
|
@ -23,13 +23,14 @@ NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TestInterfaceAsyncIterableSingle)
|
|||
NS_INTERFACE_MAP_END
|
||||
|
||||
TestInterfaceAsyncIterableSingle::TestInterfaceAsyncIterableSingle(
|
||||
nsPIDOMWindowInner* aParent)
|
||||
: mParent(aParent) {}
|
||||
nsPIDOMWindowInner* aParent, bool aFailToInit)
|
||||
: mParent(aParent), mFailToInit(aFailToInit) {}
|
||||
|
||||
// static
|
||||
already_AddRefed<TestInterfaceAsyncIterableSingle>
|
||||
TestInterfaceAsyncIterableSingle::Constructor(const GlobalObject& aGlobal,
|
||||
ErrorResult& aRv) {
|
||||
TestInterfaceAsyncIterableSingle::Constructor(
|
||||
const GlobalObject& aGlobal,
|
||||
const TestInterfaceAsyncIterableSingleOptions& aOptions, ErrorResult& aRv) {
|
||||
nsCOMPtr<nsPIDOMWindowInner> window =
|
||||
do_QueryInterface(aGlobal.GetAsSupports());
|
||||
if (!window) {
|
||||
|
@ -38,7 +39,7 @@ TestInterfaceAsyncIterableSingle::Constructor(const GlobalObject& aGlobal,
|
|||
}
|
||||
|
||||
RefPtr<TestInterfaceAsyncIterableSingle> r =
|
||||
new TestInterfaceAsyncIterableSingle(window);
|
||||
new TestInterfaceAsyncIterableSingle(window, aOptions.mFailToInit);
|
||||
return r.forget();
|
||||
}
|
||||
|
||||
|
@ -51,7 +52,13 @@ nsPIDOMWindowInner* TestInterfaceAsyncIterableSingle::GetParentObject() const {
|
|||
return mParent;
|
||||
}
|
||||
|
||||
void TestInterfaceAsyncIterableSingle::InitAsyncIterator(Iterator* aIterator) {
|
||||
void TestInterfaceAsyncIterableSingle::InitAsyncIterator(Iterator* aIterator,
|
||||
ErrorResult& aError) {
|
||||
if (mFailToInit) {
|
||||
aError.ThrowTypeError("Caller asked us to fail");
|
||||
return;
|
||||
}
|
||||
|
||||
UniquePtr<IteratorData> data(new IteratorData(0));
|
||||
aIterator->SetData((void*)data.release());
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ class ErrorResult;
|
|||
namespace dom {
|
||||
|
||||
class GlobalObject;
|
||||
struct TestInterfaceAsyncIterableSingleOptions;
|
||||
|
||||
// Implementation of test binding for webidl iterable interfaces, using
|
||||
// primitives for value type
|
||||
|
@ -30,15 +31,17 @@ class TestInterfaceAsyncIterableSingle final : public nsISupports,
|
|||
NS_DECL_CYCLE_COLLECTING_ISUPPORTS
|
||||
NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS(TestInterfaceAsyncIterableSingle)
|
||||
|
||||
explicit TestInterfaceAsyncIterableSingle(nsPIDOMWindowInner* aParent);
|
||||
TestInterfaceAsyncIterableSingle(nsPIDOMWindowInner* aParent,
|
||||
bool aFailToInit);
|
||||
nsPIDOMWindowInner* GetParentObject() const;
|
||||
virtual JSObject* WrapObject(JSContext* aCx,
|
||||
JS::Handle<JSObject*> aGivenProto) override;
|
||||
static already_AddRefed<TestInterfaceAsyncIterableSingle> Constructor(
|
||||
const GlobalObject& aGlobal, ErrorResult& rv);
|
||||
const GlobalObject& aGlobal,
|
||||
const TestInterfaceAsyncIterableSingleOptions& aOptions, ErrorResult& rv);
|
||||
|
||||
using Iterator = AsyncIterableIterator<TestInterfaceAsyncIterableSingle>;
|
||||
void InitAsyncIterator(Iterator* aIterator);
|
||||
void InitAsyncIterator(Iterator* aIterator, ErrorResult& aError);
|
||||
void DestroyAsyncIterator(Iterator* aIterator);
|
||||
already_AddRefed<Promise> GetNextPromise(JSContext* aCx, Iterator* aIterator,
|
||||
ErrorResult& aRv);
|
||||
|
@ -59,6 +62,7 @@ class TestInterfaceAsyncIterableSingle final : public nsISupports,
|
|||
void ResolvePromise(IteratorData* aData);
|
||||
|
||||
nsCOMPtr<nsPIDOMWindowInner> mParent;
|
||||
bool mFailToInit;
|
||||
};
|
||||
|
||||
} // namespace dom
|
||||
|
|
|
@ -18,7 +18,20 @@ async function test_data_single() {
|
|||
info(`AsyncIterableSingle: Testing simple iterable creation and functionality`);
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
let itr = new TestInterfaceAsyncIterableSingle();
|
||||
let itr = new TestInterfaceAsyncIterableSingle({ failToInit: true });
|
||||
let initFailed = false;
|
||||
try {
|
||||
itr.values();
|
||||
} catch (e) {
|
||||
initFailed = true;
|
||||
}
|
||||
ok(initFailed,
|
||||
"AsyncIterableSingle: A failure in asynchronous iterator initialization " +
|
||||
"steps should propagate to the caller of the asynchronous iterator's " +
|
||||
"constructor.");
|
||||
|
||||
// eslint-disable-next-line no-undef
|
||||
itr = new TestInterfaceAsyncIterableSingle();
|
||||
is(itr.values, itr[Symbol.asyncIterator],
|
||||
`AsyncIterableSingle: Should be using @@asyncIterator for 'values'`);
|
||||
|
||||
|
|
|
@ -1088,6 +1088,9 @@ bool DrawTargetWebgl::SharedContext::SupportsPattern(const Pattern& aPattern) {
|
|||
return true;
|
||||
case PatternType::SURFACE: {
|
||||
auto surfacePattern = static_cast<const SurfacePattern&>(aPattern);
|
||||
if (surfacePattern.mExtendMode != ExtendMode::CLAMP) {
|
||||
return false;
|
||||
}
|
||||
if (surfacePattern.mSurface) {
|
||||
IntSize size = surfacePattern.mSurface->GetSize();
|
||||
// The maximum size a surface can be before triggering a fallback to
|
||||
|
|
|
@ -59,7 +59,7 @@ bool Clipboard::IsTestingPrefEnabledOrHasReadPermission(
|
|||
}
|
||||
|
||||
// @return true iff the event was dispatched successfully.
|
||||
static bool MaybeCreateAndDispatchMozClipboardReadTextPasteEvent(
|
||||
static bool MaybeCreateAndDispatchMozClipboardReadPasteEvent(
|
||||
nsPIDOMWindowInner& aOwner) {
|
||||
RefPtr<Document> document = aOwner.GetDoc();
|
||||
|
||||
|
@ -70,187 +70,28 @@ static bool MaybeCreateAndDispatchMozClipboardReadTextPasteEvent(
|
|||
return false;
|
||||
}
|
||||
|
||||
// Conceptionally, `ClipboardReadTextPasteChild` is the target of the event.
|
||||
// Conceptionally, `ClipboardReadPasteChild` is the target of the event.
|
||||
// It ensures to receive the event by declaring the event in
|
||||
// <BrowserGlue.jsm>.
|
||||
return !NS_WARN_IF(NS_FAILED(nsContentUtils::DispatchChromeEvent(
|
||||
document, ToSupports(document), u"MozClipboardReadTextPaste"_ns,
|
||||
document, ToSupports(document), u"MozClipboardReadPaste"_ns,
|
||||
CanBubble::eNo, Cancelable::eNo)));
|
||||
}
|
||||
|
||||
already_AddRefed<nsIRunnable> Clipboard::ReadTextRequest::Answer() {
|
||||
return NS_NewRunnableFunction(
|
||||
"Clipboard::ReadText", [p = std::move(mPromise)]() {
|
||||
nsresult rv;
|
||||
nsCOMPtr<nsIClipboard> clipboardService(
|
||||
do_GetService("@mozilla.org/widget/clipboard;1", &rv));
|
||||
if (NS_FAILED(rv)) {
|
||||
p->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
void Clipboard::ReadRequest::Answer() {
|
||||
RefPtr<Promise> p(std::move(mPromise));
|
||||
RefPtr<nsPIDOMWindowInner> owner(std::move(mOwner));
|
||||
|
||||
nsCOMPtr<nsITransferable> trans =
|
||||
do_CreateInstance("@mozilla.org/widget/transferable;1");
|
||||
if (NS_WARN_IF(!trans)) {
|
||||
p->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
trans->Init(nullptr);
|
||||
trans->AddDataFlavor(kUnicodeMime);
|
||||
clipboardService->AsyncGetData(trans, nsIClipboard::kGlobalClipboard)
|
||||
->Then(
|
||||
GetMainThreadSerialEventTarget(), __func__,
|
||||
/* resolve */
|
||||
[trans, p]() {
|
||||
nsCOMPtr<nsISupports> data;
|
||||
nsresult rv = trans->GetTransferData(kUnicodeMime,
|
||||
getter_AddRefs(data));
|
||||
|
||||
nsAutoString str;
|
||||
if (!NS_WARN_IF(NS_FAILED(rv))) {
|
||||
nsCOMPtr<nsISupportsString> supportsstr =
|
||||
do_QueryInterface(data);
|
||||
MOZ_ASSERT(supportsstr);
|
||||
if (supportsstr) {
|
||||
supportsstr->GetData(str);
|
||||
}
|
||||
}
|
||||
|
||||
p->MaybeResolve(str);
|
||||
},
|
||||
/* reject */
|
||||
[p](nsresult rv) { p->MaybeReject(rv); });
|
||||
});
|
||||
}
|
||||
|
||||
static bool IsReadTextExposedToContent() {
|
||||
return StaticPrefs::dom_events_asyncClipboard_readText_DoNotUseDirectly();
|
||||
}
|
||||
|
||||
already_AddRefed<nsIRunnable>
|
||||
Clipboard::CheckReadTextPermissionAndHandleRequest(
|
||||
Promise& aPromise, nsIPrincipal& aSubjectPrincipal) {
|
||||
if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) {
|
||||
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
|
||||
("%s: testing pref enabled or has read permission", __FUNCTION__));
|
||||
return ReadTextRequest{aPromise}.Answer();
|
||||
nsresult rv;
|
||||
nsCOMPtr<nsIClipboard> clipboardService(
|
||||
do_GetService("@mozilla.org/widget/clipboard;1", &rv));
|
||||
if (NS_FAILED(rv)) {
|
||||
p->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
if (aSubjectPrincipal.GetIsAddonOrExpandedAddonPrincipal()) {
|
||||
// TODO: enable showing the "Paste" button in this case; see bug 1773681.
|
||||
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
|
||||
("%s: Addon without read permssion.", __FUNCTION__));
|
||||
aPromise.MaybeRejectWithUndefined();
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return HandleReadTextRequestWhichRequiresPasteButton(aPromise);
|
||||
}
|
||||
|
||||
already_AddRefed<nsIRunnable>
|
||||
Clipboard::HandleReadTextRequestWhichRequiresPasteButton(Promise& aPromise) {
|
||||
RefPtr<nsIRunnable> runnable;
|
||||
|
||||
nsPIDOMWindowInner* owner = GetOwner();
|
||||
WindowContext* windowContext = owner ? owner->GetWindowContext() : nullptr;
|
||||
if (!windowContext) {
|
||||
MOZ_ASSERT_UNREACHABLE("There should be a WindowContext.");
|
||||
aPromise.MaybeRejectWithUndefined();
|
||||
return runnable.forget();
|
||||
}
|
||||
|
||||
// If no transient user activation, reject the promise and return.
|
||||
if (!windowContext->HasValidTransientUserGestureActivation()) {
|
||||
aPromise.MaybeRejectWithNotAllowedError(
|
||||
"`navigator.clipboard.readText()` was blocked due to lack of "
|
||||
"user activation.");
|
||||
return runnable.forget();
|
||||
}
|
||||
|
||||
// TODO: when a user activation stems from a contextmenu event
|
||||
// (https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event),
|
||||
// forbid pasting (bug 1767941).
|
||||
|
||||
switch (mTransientUserPasteState.RefreshAndGet(*windowContext)) {
|
||||
case TransientUserPasteState::Value::Initial: {
|
||||
MOZ_ASSERT(mReadTextRequests.IsEmpty());
|
||||
|
||||
if (MaybeCreateAndDispatchMozClipboardReadTextPasteEvent(*owner)) {
|
||||
mTransientUserPasteState.OnStartWaitingForUserReactionToPasteMenuPopup(
|
||||
windowContext->GetUserGestureStart());
|
||||
mReadTextRequests.AppendElement(MakeUnique<ReadTextRequest>(aPromise));
|
||||
} else {
|
||||
// This shouldn't happen but let's handle this case.
|
||||
aPromise.MaybeRejectWithUndefined();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TransientUserPasteState::Value::
|
||||
WaitingForUserReactionToPasteMenuPopup: {
|
||||
MOZ_ASSERT(!mReadTextRequests.IsEmpty());
|
||||
|
||||
mReadTextRequests.AppendElement(MakeUnique<ReadTextRequest>(aPromise));
|
||||
break;
|
||||
}
|
||||
case TransientUserPasteState::Value::TransientlyForbiddenByUser: {
|
||||
aPromise.MaybeRejectWithNotAllowedError(
|
||||
"`navigator.clipboard.readText()` was blocked due to the user "
|
||||
"dismissing the 'Paste' button.");
|
||||
break;
|
||||
}
|
||||
case TransientUserPasteState::Value::TransientlyAllowedByUser: {
|
||||
runnable = ReadTextRequest{aPromise}.Answer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return runnable.forget();
|
||||
}
|
||||
|
||||
already_AddRefed<Promise> Clipboard::ReadHelper(
|
||||
nsIPrincipal& aSubjectPrincipal, ClipboardReadType aClipboardReadType,
|
||||
ErrorResult& aRv) {
|
||||
// Create a new promise
|
||||
RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv);
|
||||
if (aRv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
switch (aClipboardReadType) {
|
||||
case eReadText: {
|
||||
RefPtr<nsIRunnable> runnable =
|
||||
CheckReadTextPermissionAndHandleRequest(*p, aSubjectPrincipal);
|
||||
|
||||
if (runnable) {
|
||||
GetParentObject()->Dispatch(TaskCategory::Other, runnable.forget());
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
case eRead: {
|
||||
// We want to disable security check for automated tests that have the
|
||||
// pref
|
||||
// dom.events.testing.asyncClipboard set to true
|
||||
const bool isTestingPrefEnabledOrHasReadPermission =
|
||||
IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal);
|
||||
|
||||
if (!isTestingPrefEnabledOrHasReadPermission) {
|
||||
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
|
||||
("Clipboard, ReadHelper, "
|
||||
"Don't have permissions for reading\n"));
|
||||
p->MaybeRejectWithUndefined();
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
nsresult rv;
|
||||
nsCOMPtr<nsIClipboard> clipboardService(
|
||||
do_GetService("@mozilla.org/widget/clipboard;1", &rv));
|
||||
if (NS_FAILED(rv)) {
|
||||
p->MaybeReject(rv);
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
switch (mType) {
|
||||
case ReadRequestType::eRead: {
|
||||
clipboardService
|
||||
->AsyncHasDataMatchingFlavors(
|
||||
// Mandatory data types defined in
|
||||
|
@ -262,9 +103,8 @@ already_AddRefed<Promise> Clipboard::ReadHelper(
|
|||
->Then(
|
||||
GetMainThreadSerialEventTarget(), __func__,
|
||||
/* resolve */
|
||||
[self = RefPtr{this}, p](nsTArray<nsCString> formats) {
|
||||
nsCOMPtr<nsIGlobalObject> global =
|
||||
do_QueryInterface(self->GetOwner());
|
||||
[owner, p](nsTArray<nsCString> formats) {
|
||||
nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(owner);
|
||||
if (NS_WARN_IF(!global)) {
|
||||
p->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
|
@ -303,8 +143,146 @@ already_AddRefed<Promise> Clipboard::ReadHelper(
|
|||
[p](nsresult rv) { p->MaybeReject(rv); });
|
||||
break;
|
||||
}
|
||||
case ReadRequestType::eReadText: {
|
||||
nsCOMPtr<nsITransferable> trans =
|
||||
do_CreateInstance("@mozilla.org/widget/transferable;1");
|
||||
if (NS_WARN_IF(!trans)) {
|
||||
p->MaybeReject(NS_ERROR_UNEXPECTED);
|
||||
return;
|
||||
}
|
||||
|
||||
trans->Init(nullptr);
|
||||
trans->AddDataFlavor(kUnicodeMime);
|
||||
clipboardService->AsyncGetData(trans, nsIClipboard::kGlobalClipboard)
|
||||
->Then(
|
||||
GetMainThreadSerialEventTarget(), __func__,
|
||||
/* resolve */
|
||||
[trans, p]() {
|
||||
nsCOMPtr<nsISupports> data;
|
||||
nsresult rv =
|
||||
trans->GetTransferData(kUnicodeMime, getter_AddRefs(data));
|
||||
|
||||
nsAutoString str;
|
||||
if (!NS_WARN_IF(NS_FAILED(rv))) {
|
||||
nsCOMPtr<nsISupportsString> supportsstr =
|
||||
do_QueryInterface(data);
|
||||
MOZ_ASSERT(supportsstr);
|
||||
if (supportsstr) {
|
||||
supportsstr->GetData(str);
|
||||
}
|
||||
}
|
||||
|
||||
p->MaybeResolve(str);
|
||||
},
|
||||
/* reject */
|
||||
[p](nsresult rv) { p->MaybeReject(rv); });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
MOZ_ASSERT_UNREACHABLE("Unknown read type");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool IsReadTextExposedToContent() {
|
||||
return StaticPrefs::dom_events_asyncClipboard_readText_DoNotUseDirectly();
|
||||
}
|
||||
|
||||
void Clipboard::CheckReadPermissionAndHandleRequest(
|
||||
Promise& aPromise, nsIPrincipal& aSubjectPrincipal, ReadRequestType aType) {
|
||||
if (IsTestingPrefEnabledOrHasReadPermission(aSubjectPrincipal)) {
|
||||
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
|
||||
("%s: testing pref enabled or has read permission", __FUNCTION__));
|
||||
nsPIDOMWindowInner* owner = GetOwner();
|
||||
if (!owner) {
|
||||
aPromise.MaybeRejectWithUndefined();
|
||||
return;
|
||||
}
|
||||
|
||||
ReadRequest{aPromise, aType, *owner}.Answer();
|
||||
return;
|
||||
}
|
||||
|
||||
if (aSubjectPrincipal.GetIsAddonOrExpandedAddonPrincipal()) {
|
||||
// TODO: enable showing the "Paste" button in this case; see bug 1773681.
|
||||
MOZ_LOG(GetClipboardLog(), LogLevel::Debug,
|
||||
("%s: Addon without read permssion.", __FUNCTION__));
|
||||
aPromise.MaybeRejectWithUndefined();
|
||||
return;
|
||||
}
|
||||
|
||||
HandleReadRequestWhichRequiresPasteButton(aPromise, aType);
|
||||
}
|
||||
|
||||
void Clipboard::HandleReadRequestWhichRequiresPasteButton(
|
||||
Promise& aPromise, ReadRequestType aType) {
|
||||
nsPIDOMWindowInner* owner = GetOwner();
|
||||
WindowContext* windowContext = owner ? owner->GetWindowContext() : nullptr;
|
||||
if (!windowContext) {
|
||||
MOZ_ASSERT_UNREACHABLE("There should be a WindowContext.");
|
||||
aPromise.MaybeRejectWithUndefined();
|
||||
return;
|
||||
}
|
||||
|
||||
// If no transient user activation, reject the promise and return.
|
||||
if (!windowContext->HasValidTransientUserGestureActivation()) {
|
||||
aPromise.MaybeRejectWithNotAllowedError(
|
||||
"Clipboard read request was blocked due to lack of "
|
||||
"user activation.");
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: when a user activation stems from a contextmenu event
|
||||
// (https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event),
|
||||
// forbid pasting (bug 1767941).
|
||||
|
||||
switch (mTransientUserPasteState.RefreshAndGet(*windowContext)) {
|
||||
case TransientUserPasteState::Value::Initial: {
|
||||
MOZ_ASSERT(mReadRequests.IsEmpty());
|
||||
|
||||
if (MaybeCreateAndDispatchMozClipboardReadPasteEvent(*owner)) {
|
||||
mTransientUserPasteState.OnStartWaitingForUserReactionToPasteMenuPopup(
|
||||
windowContext->GetUserGestureStart());
|
||||
mReadRequests.AppendElement(
|
||||
MakeUnique<ReadRequest>(aPromise, aType, *owner));
|
||||
} else {
|
||||
// This shouldn't happen but let's handle this case.
|
||||
aPromise.MaybeRejectWithUndefined();
|
||||
}
|
||||
break;
|
||||
}
|
||||
case TransientUserPasteState::Value::
|
||||
WaitingForUserReactionToPasteMenuPopup: {
|
||||
MOZ_ASSERT(!mReadRequests.IsEmpty());
|
||||
|
||||
mReadRequests.AppendElement(
|
||||
MakeUnique<ReadRequest>(aPromise, aType, *owner));
|
||||
break;
|
||||
}
|
||||
case TransientUserPasteState::Value::TransientlyForbiddenByUser: {
|
||||
aPromise.MaybeRejectWithNotAllowedError(
|
||||
"`Clipboard read request was blocked due to the user "
|
||||
"dismissing the 'Paste' button.");
|
||||
break;
|
||||
}
|
||||
case TransientUserPasteState::Value::TransientlyAllowedByUser: {
|
||||
ReadRequest{aPromise, aType, *owner}.Answer();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
already_AddRefed<Promise> Clipboard::ReadHelper(nsIPrincipal& aSubjectPrincipal,
|
||||
ReadRequestType aType,
|
||||
ErrorResult& aRv) {
|
||||
// Create a new promise
|
||||
RefPtr<Promise> p = dom::Promise::Create(GetOwnerGlobal(), aRv);
|
||||
if (aRv.Failed()) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CheckReadPermissionAndHandleRequest(*p, aSubjectPrincipal, aType);
|
||||
return p.forget();
|
||||
}
|
||||
|
||||
|
@ -360,12 +338,12 @@ void Clipboard::TransientUserPasteState::OnUserReactedToPasteMenuPopup(
|
|||
|
||||
already_AddRefed<Promise> Clipboard::Read(nsIPrincipal& aSubjectPrincipal,
|
||||
ErrorResult& aRv) {
|
||||
return ReadHelper(aSubjectPrincipal, eRead, aRv);
|
||||
return ReadHelper(aSubjectPrincipal, ReadRequestType::eRead, aRv);
|
||||
}
|
||||
|
||||
already_AddRefed<Promise> Clipboard::ReadText(nsIPrincipal& aSubjectPrincipal,
|
||||
ErrorResult& aRv) {
|
||||
return ReadHelper(aSubjectPrincipal, eReadText, aRv);
|
||||
return ReadHelper(aSubjectPrincipal, ReadRequestType::eReadText, aRv);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
@ -714,7 +692,7 @@ already_AddRefed<Promise> Clipboard::WriteText(const nsAString& aData,
|
|||
return Write(std::move(sequence), aSubjectPrincipal, aRv);
|
||||
}
|
||||
|
||||
void Clipboard::ReadTextRequest::MaybeRejectWithNotAllowedError(
|
||||
void Clipboard::ReadRequest::MaybeRejectWithNotAllowedError(
|
||||
const nsACString& aMessage) {
|
||||
mPromise->MaybeRejectWithNotAllowedError(aMessage);
|
||||
}
|
||||
|
@ -724,18 +702,18 @@ void Clipboard::OnUserReactedToPasteMenuPopup(const bool aAllowed) {
|
|||
|
||||
mTransientUserPasteState.OnUserReactedToPasteMenuPopup(aAllowed);
|
||||
|
||||
MOZ_ASSERT(!mReadTextRequests.IsEmpty());
|
||||
MOZ_ASSERT(!mReadRequests.IsEmpty());
|
||||
|
||||
for (UniquePtr<ReadTextRequest>& request : mReadTextRequests) {
|
||||
for (UniquePtr<ReadRequest>& request : mReadRequests) {
|
||||
if (aAllowed) {
|
||||
GetParentObject()->Dispatch(TaskCategory::Other, request->Answer());
|
||||
request->Answer();
|
||||
} else {
|
||||
request->MaybeRejectWithNotAllowedError(
|
||||
"The user dismissed the 'Paste' button."_ns);
|
||||
}
|
||||
}
|
||||
|
||||
mReadTextRequests.Clear();
|
||||
mReadRequests.Clear();
|
||||
}
|
||||
|
||||
JSObject* Clipboard::WrapObject(JSContext* aCx,
|
||||
|
|
|
@ -17,11 +17,6 @@
|
|||
|
||||
namespace mozilla::dom {
|
||||
|
||||
enum ClipboardReadType {
|
||||
eRead,
|
||||
eReadText,
|
||||
};
|
||||
|
||||
class Promise;
|
||||
class ClipboardItem;
|
||||
|
||||
|
@ -62,6 +57,11 @@ class Clipboard : public DOMEventTargetHelper {
|
|||
JS::Handle<JSObject*> aGivenProto) override;
|
||||
|
||||
private:
|
||||
enum class ReadRequestType {
|
||||
eRead,
|
||||
eReadText,
|
||||
};
|
||||
|
||||
// Checks if dom.events.testing.asyncClipboard pref is enabled.
|
||||
// The aforementioned pref allows automated tests to bypass the security
|
||||
// checks when writing to
|
||||
|
@ -71,36 +71,38 @@ class Clipboard : public DOMEventTargetHelper {
|
|||
static bool IsTestingPrefEnabledOrHasReadPermission(
|
||||
nsIPrincipal& aSubjectPrincipal);
|
||||
|
||||
// @return the remaining work to fill aPromise.
|
||||
already_AddRefed<nsIRunnable> CheckReadTextPermissionAndHandleRequest(
|
||||
Promise& aPromise, nsIPrincipal& aSubjectPrincipal);
|
||||
void CheckReadPermissionAndHandleRequest(Promise& aPromise,
|
||||
nsIPrincipal& aSubjectPrincipal,
|
||||
ReadRequestType aType);
|
||||
|
||||
// @return the remaining work to fill aPromise.
|
||||
already_AddRefed<nsIRunnable> HandleReadTextRequestWhichRequiresPasteButton(
|
||||
Promise& aPromise);
|
||||
void HandleReadRequestWhichRequiresPasteButton(Promise& aPromise,
|
||||
ReadRequestType aType);
|
||||
|
||||
already_AddRefed<Promise> ReadHelper(nsIPrincipal& aSubjectPrincipal,
|
||||
ClipboardReadType aClipboardReadType,
|
||||
ErrorResult& aRv);
|
||||
ReadRequestType aType, ErrorResult& aRv);
|
||||
|
||||
~Clipboard();
|
||||
|
||||
class ReadTextRequest final {
|
||||
class ReadRequest final {
|
||||
public:
|
||||
explicit ReadTextRequest(Promise& aPromise) : mPromise{&aPromise} {}
|
||||
ReadRequest(Promise& aPromise, ReadRequestType aType,
|
||||
nsPIDOMWindowInner& aOwner)
|
||||
: mType(aType), mPromise(&aPromise), mOwner(&aOwner) {}
|
||||
|
||||
// Clears the request too.
|
||||
already_AddRefed<nsIRunnable> Answer();
|
||||
void Answer();
|
||||
|
||||
void MaybeRejectWithNotAllowedError(const nsACString& aMessage);
|
||||
|
||||
private:
|
||||
ReadRequestType mType;
|
||||
// Not cycle-collected, because it's nulled when the request is answered or
|
||||
// destructed.
|
||||
RefPtr<Promise> mPromise;
|
||||
RefPtr<nsPIDOMWindowInner> mOwner;
|
||||
};
|
||||
|
||||
AutoTArray<UniquePtr<ReadTextRequest>, 1> mReadTextRequests;
|
||||
AutoTArray<UniquePtr<ReadRequest>, 1> mReadRequests;
|
||||
|
||||
class TransientUserPasteState final {
|
||||
public:
|
||||
|
|
|
@ -7,6 +7,9 @@ support-files =
|
|||
[browser_navigator_clipboard_readText.js]
|
||||
support-files =
|
||||
simple_navigator_clipboard_readText.html
|
||||
[browser_navigator_clipboard_read.js]
|
||||
support-files =
|
||||
simple_navigator_clipboard_read.html
|
||||
[browser_navigator_clipboard_touch.js]
|
||||
support-files =
|
||||
simple_navigator_clipboard_readText.html
|
||||
|
|
200
dom/events/test/clipboard/browser_navigator_clipboard_read.js
Normal file
200
dom/events/test/clipboard/browser_navigator_clipboard_read.js
Normal file
|
@ -0,0 +1,200 @@
|
|||
/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
||||
/* vim: set ts=8 sts=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";
|
||||
|
||||
/* import-globals-from head.js */
|
||||
|
||||
const kBaseUrlForContent = getRootDirectory(gTestPath).replace(
|
||||
"chrome://mochitests/content",
|
||||
"https://example.com"
|
||||
);
|
||||
|
||||
const kContentFileName = "simple_navigator_clipboard_read.html";
|
||||
|
||||
const kContentFileUrl = kBaseUrlForContent + kContentFileName;
|
||||
|
||||
const kApzTestNativeEventUtilsUrl =
|
||||
"chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js";
|
||||
|
||||
Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this);
|
||||
|
||||
// @param aBrowser browser object of the content tab.
|
||||
// @param aMultipleReadTextCalls if false, exactly one call is made, two
|
||||
// otherwise.
|
||||
function promiseClickContentToTriggerClipboardRead(
|
||||
aBrowser,
|
||||
aMultipleReadTextCalls
|
||||
) {
|
||||
return promiseClickContentElement(
|
||||
aBrowser,
|
||||
aMultipleReadTextCalls ? "invokeReadTwiceId" : "invokeReadOnceId"
|
||||
);
|
||||
}
|
||||
|
||||
// @param aBrowser browser object of the content tab.
|
||||
function promiseMutatedReadResultFromContentElement(aBrowser) {
|
||||
return promiseMutatedTextContentFromContentElement(aBrowser, "readResultId");
|
||||
}
|
||||
|
||||
add_task(async function init() {
|
||||
await SpecialPowers.pushPrefEnv({
|
||||
set: [
|
||||
["dom.events.asyncClipboard.clipboardItem", true],
|
||||
["test.events.async.enabled", true],
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_paste_button_position() {
|
||||
// Ensure there's text on the clipboard.
|
||||
await promiseWritingRandomTextToClipboard();
|
||||
|
||||
await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
|
||||
const pasteButtonIsShown = promisePasteButtonIsShown();
|
||||
const coordsOfClickInContentRelativeToScreenInDevicePixels = await promiseClickContentToTriggerClipboardRead(
|
||||
browser,
|
||||
false
|
||||
);
|
||||
info(
|
||||
"coordsOfClickInContentRelativeToScreenInDevicePixels: " +
|
||||
coordsOfClickInContentRelativeToScreenInDevicePixels.x +
|
||||
", " +
|
||||
coordsOfClickInContentRelativeToScreenInDevicePixels.y
|
||||
);
|
||||
|
||||
const pasteButtonCoordsRelativeToScreenInDevicePixels = await pasteButtonIsShown;
|
||||
info(
|
||||
"pasteButtonCoordsRelativeToScreenInDevicePixels: " +
|
||||
pasteButtonCoordsRelativeToScreenInDevicePixels.x +
|
||||
", " +
|
||||
pasteButtonCoordsRelativeToScreenInDevicePixels.y
|
||||
);
|
||||
|
||||
const mouseCoordsRelativeToScreenInDevicePixels = getMouseCoordsRelativeToScreenInDevicePixels();
|
||||
info(
|
||||
"mouseCoordsRelativeToScreenInDevicePixels: " +
|
||||
mouseCoordsRelativeToScreenInDevicePixels.x +
|
||||
", " +
|
||||
mouseCoordsRelativeToScreenInDevicePixels.y
|
||||
);
|
||||
|
||||
// Asserting not overlapping is important; otherwise, when the
|
||||
// "Paste" button is shown via a `mousedown` event, the following
|
||||
// `mouseup` event could accept the "Paste" button unnoticed by the
|
||||
// user.
|
||||
ok(
|
||||
isCloselyLeftOnTopOf(
|
||||
mouseCoordsRelativeToScreenInDevicePixels,
|
||||
pasteButtonCoordsRelativeToScreenInDevicePixels
|
||||
),
|
||||
"'Paste' button is closely left on top of the mouse pointer."
|
||||
);
|
||||
ok(
|
||||
isCloselyLeftOnTopOf(
|
||||
coordsOfClickInContentRelativeToScreenInDevicePixels,
|
||||
pasteButtonCoordsRelativeToScreenInDevicePixels
|
||||
),
|
||||
"Coords of click in content are closely left on top of the 'Paste' button."
|
||||
);
|
||||
|
||||
// To avoid disturbing subsequent tests.
|
||||
const pasteButtonIsHidden = promisePasteButtonIsHidden();
|
||||
await promiseClickPasteButton();
|
||||
await pasteButtonIsHidden;
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_accepting_paste_button() {
|
||||
// Randomized text to avoid overlappings with other tests.
|
||||
const clipboardText = await promiseWritingRandomTextToClipboard();
|
||||
|
||||
await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
|
||||
const pasteButtonIsShown = promisePasteButtonIsShown();
|
||||
await promiseClickContentToTriggerClipboardRead(browser, false);
|
||||
await pasteButtonIsShown;
|
||||
const pasteButtonIsHidden = promisePasteButtonIsHidden();
|
||||
const mutatedReadResultFromContentElement = promiseMutatedReadResultFromContentElement(
|
||||
browser
|
||||
);
|
||||
await promiseClickPasteButton();
|
||||
await pasteButtonIsHidden;
|
||||
await mutatedReadResultFromContentElement.then(value => {
|
||||
is(
|
||||
value,
|
||||
"Resolved: " + clipboardText,
|
||||
"Text returned from `navigator.clipboard.read()` is as expected."
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_task(async function test_dismissing_paste_button() {
|
||||
await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
|
||||
const pasteButtonIsShown = promisePasteButtonIsShown();
|
||||
await promiseClickContentToTriggerClipboardRead(browser, false);
|
||||
await pasteButtonIsShown;
|
||||
const pasteButtonIsHidden = promisePasteButtonIsHidden();
|
||||
const mutatedReadResultFromContentElement = promiseMutatedReadResultFromContentElement(
|
||||
browser
|
||||
);
|
||||
await promiseDismissPasteButton();
|
||||
await pasteButtonIsHidden;
|
||||
await mutatedReadResultFromContentElement.then(value => {
|
||||
is(
|
||||
value,
|
||||
"Rejected: The user dismissed the 'Paste' button.",
|
||||
"`navigator.clipboard.read()` rejected after dismissing the 'Paste' button"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_task(
|
||||
async function test_multiple_read_invocations_for_same_user_activation() {
|
||||
// Randomized text to avoid overlappings with other tests.
|
||||
const clipboardText = await promiseWritingRandomTextToClipboard();
|
||||
|
||||
await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
|
||||
const pasteButtonIsShown = promisePasteButtonIsShown();
|
||||
await promiseClickContentToTriggerClipboardRead(browser, true);
|
||||
await pasteButtonIsShown;
|
||||
const mutatedReadResultFromContentElement = promiseMutatedReadResultFromContentElement(
|
||||
browser
|
||||
);
|
||||
const pasteButtonIsHidden = promisePasteButtonIsHidden();
|
||||
await promiseClickPasteButton();
|
||||
await mutatedReadResultFromContentElement.then(value => {
|
||||
is(
|
||||
value,
|
||||
"Resolved 1: " + clipboardText + "; Resolved 2: " + clipboardText,
|
||||
"Two calls of `navigator.clipboard.read()` both resolved with the expected text."
|
||||
);
|
||||
});
|
||||
|
||||
// To avoid disturbing subsequent tests.
|
||||
await pasteButtonIsHidden;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
add_task(async function test_new_user_activation_shows_paste_button_again() {
|
||||
await BrowserTestUtils.withNewTab(kContentFileUrl, async function(browser) {
|
||||
// Ensure there's text on the clipboard.
|
||||
await promiseWritingRandomTextToClipboard();
|
||||
|
||||
for (let i = 0; i < 2; ++i) {
|
||||
const pasteButtonIsShown = promisePasteButtonIsShown();
|
||||
// A click initiates a new user activation.
|
||||
await promiseClickContentToTriggerClipboardRead(browser, false);
|
||||
await pasteButtonIsShown;
|
||||
|
||||
const pasteButtonIsHidden = promisePasteButtonIsHidden();
|
||||
await promiseClickPasteButton();
|
||||
await pasteButtonIsHidden;
|
||||
}
|
||||
});
|
||||
});
|
|
@ -22,8 +22,6 @@ const kApzTestNativeEventUtilsUrl =
|
|||
|
||||
Services.scriptloader.loadSubScript(kApzTestNativeEventUtilsUrl, this);
|
||||
|
||||
const chromeDoc = window.document;
|
||||
|
||||
// @param aBrowser browser object of the content tab.
|
||||
// @param aMultipleReadTextCalls if false, exactly one call is made, two
|
||||
// otherwise.
|
||||
|
@ -31,75 +29,18 @@ function promiseClickContentToTriggerClipboardReadText(
|
|||
aBrowser,
|
||||
aMultipleReadTextCalls
|
||||
) {
|
||||
const contentButtonId = aMultipleReadTextCalls
|
||||
? "invokeReadTextTwiceId"
|
||||
: "invokeReadTextOnceId";
|
||||
|
||||
// `aBrowser.contentDocument` is null, therefore use `SpecialPowers.spawn`.
|
||||
return SpecialPowers.spawn(
|
||||
return promiseClickContentElement(
|
||||
aBrowser,
|
||||
[contentButtonId],
|
||||
async _contentButtonId => {
|
||||
const contentButton = content.document.getElementById(_contentButtonId);
|
||||
let promise = new Promise(resolve => {
|
||||
contentButton.addEventListener(
|
||||
"click",
|
||||
function(e) {
|
||||
resolve({ x: e.screenX, y: e.screenY });
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(contentButton, {}, content.window);
|
||||
|
||||
return promise;
|
||||
}
|
||||
aMultipleReadTextCalls ? "invokeReadTextTwiceId" : "invokeReadTextOnceId"
|
||||
);
|
||||
}
|
||||
|
||||
// @param aBrowser browser object of the content tab.
|
||||
function promiseMutatedReadTextResultFromContentElement(aBrowser) {
|
||||
return SpecialPowers.spawn(aBrowser, [], async () => {
|
||||
const readTextResultElement = content.document.getElementById(
|
||||
"readTextResultId"
|
||||
);
|
||||
|
||||
const promiseReadTextResult = new Promise(resolve => {
|
||||
const mutationObserver = new content.MutationObserver(
|
||||
(aMutationRecord, aMutationObserver) => {
|
||||
info("Observed mutation.");
|
||||
aMutationObserver.disconnect();
|
||||
resolve(readTextResultElement.textContent);
|
||||
}
|
||||
);
|
||||
|
||||
mutationObserver.observe(readTextResultElement, {
|
||||
childList: true,
|
||||
});
|
||||
});
|
||||
|
||||
return await promiseReadTextResult;
|
||||
});
|
||||
}
|
||||
|
||||
function promiseWritingRandomTextToClipboard() {
|
||||
const clipboardText = "X" + Math.random();
|
||||
return navigator.clipboard.writeText(clipboardText).then(() => {
|
||||
return clipboardText;
|
||||
});
|
||||
}
|
||||
|
||||
function promiseDismissPasteButton() {
|
||||
// nsXULPopupManager rollup is handled in widget code, so we have to
|
||||
// synthesize native mouse events.
|
||||
return EventUtils.promiseNativeMouseEvent({
|
||||
type: "click",
|
||||
target: chromeDoc.body,
|
||||
// Relies on the assumption that the center of chrome document doesn't
|
||||
// overlay with the paste button showed for clipboard readText request.
|
||||
atCenter: true,
|
||||
});
|
||||
return promiseMutatedTextContentFromContentElement(
|
||||
aBrowser,
|
||||
"readTextResultId"
|
||||
);
|
||||
}
|
||||
|
||||
add_task(async function init() {
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
|
||||
"use strict";
|
||||
|
||||
const kPasteMenuPopupId = "clipboardReadTextPasteMenuPopup";
|
||||
const kPasteMenuItemId = "clipboardReadTextPasteMenuItem";
|
||||
const kPasteMenuPopupId = "clipboardReadPasteMenuPopup";
|
||||
const kPasteMenuItemId = "clipboardReadPasteMenuItem";
|
||||
|
||||
function promiseWritingRandomTextToClipboard() {
|
||||
const clipboardText = "X" + Math.random();
|
||||
|
@ -89,3 +89,71 @@ function isCloselyLeftOnTopOf(aCoordsP1, aCoordsP2, aDelta = 10) {
|
|||
Math.abs(aCoordsP2.y - aCoordsP1.y) < aDelta
|
||||
);
|
||||
}
|
||||
|
||||
function promiseDismissPasteButton() {
|
||||
// nsXULPopupManager rollup is handled in widget code, so we have to
|
||||
// synthesize native mouse events.
|
||||
return EventUtils.promiseNativeMouseEvent({
|
||||
type: "click",
|
||||
target: document.body,
|
||||
// Relies on the assumption that the center of chrome document doesn't
|
||||
// overlay with the paste button showed for clipboard readText request.
|
||||
atCenter: true,
|
||||
});
|
||||
}
|
||||
|
||||
// @param aBrowser browser object of the content tab.
|
||||
// @param aContentElementId the ID of the element to be clicked.
|
||||
function promiseClickContentElement(aBrowser, aContentElementId) {
|
||||
return SpecialPowers.spawn(
|
||||
aBrowser,
|
||||
[aContentElementId],
|
||||
async _contentElementId => {
|
||||
const contentElement = content.document.getElementById(_contentElementId);
|
||||
let promise = new Promise(resolve => {
|
||||
contentElement.addEventListener(
|
||||
"click",
|
||||
function(e) {
|
||||
resolve({ x: e.screenX, y: e.screenY });
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
});
|
||||
|
||||
EventUtils.synthesizeMouseAtCenter(contentElement, {}, content.window);
|
||||
|
||||
return promise;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// @param aBrowser browser object of the content tab.
|
||||
// @param aContentElementId the ID of the element to observe.
|
||||
function promiseMutatedTextContentFromContentElement(
|
||||
aBrowser,
|
||||
aContentElementId
|
||||
) {
|
||||
return SpecialPowers.spawn(
|
||||
aBrowser,
|
||||
[aContentElementId],
|
||||
async _contentElementId => {
|
||||
const contentElement = content.document.getElementById(_contentElementId);
|
||||
|
||||
const promiseTextContentResult = new Promise(resolve => {
|
||||
const mutationObserver = new content.MutationObserver(
|
||||
(aMutationRecord, aMutationObserver) => {
|
||||
info("Observed mutation.");
|
||||
aMutationObserver.disconnect();
|
||||
resolve(contentElement.textContent);
|
||||
}
|
||||
);
|
||||
|
||||
mutationObserver.observe(contentElement, {
|
||||
childList: true,
|
||||
});
|
||||
});
|
||||
|
||||
return await promiseTextContentResult;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<!-- Required by the .js part of the test. In a more ideal world, the script
|
||||
could be loaded in the .js part; however, currently, that causes other
|
||||
problems, which would require other changes in test framework code. -->
|
||||
<script src="/tests/SimpleTest/SimpleTest.js"></script>
|
||||
<script src="/tests/SimpleTest/paint_listener.js"></script>
|
||||
<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js"></script>
|
||||
<script src="/tests/gfx/layers/apz/test/mochitest/apz_test_utils.js"></script>
|
||||
|
||||
<script>
|
||||
function onLoad() {
|
||||
const readResult = document.getElementById("readResultId");
|
||||
|
||||
async function getClipboardText() {
|
||||
let items = await navigator.clipboard.read();
|
||||
if (items.length != 1) {
|
||||
throw Error(`incorrect number of clipboard item (${items.length})`);
|
||||
return;
|
||||
}
|
||||
|
||||
let item = items[0];
|
||||
for (let type of item.types) {
|
||||
if (type == "text/plain") {
|
||||
let blob = await item.getType(type);
|
||||
return await blob.text();
|
||||
}
|
||||
}
|
||||
|
||||
throw Error("no text/plain type");
|
||||
}
|
||||
|
||||
const b1 = document.getElementById("invokeReadOnceId");
|
||||
b1.addEventListener("click", async () => {
|
||||
getClipboardText().then(text => {
|
||||
readResult.textContent = `Resolved: ${text}`;
|
||||
}, (e) => { readResult.textContent = `Rejected: ${e.message}`});
|
||||
});
|
||||
|
||||
const b2 = document.getElementById("invokeReadTwiceId");
|
||||
b2.addEventListener("click", async () => {
|
||||
const t1 = getClipboardText();
|
||||
const t2 = getClipboardText();
|
||||
|
||||
const r1 = await t1.then(text => {
|
||||
return `Resolved 1: ${text}`;
|
||||
}, (e) => { return `Rejected 1: ${e.message}`;});
|
||||
|
||||
const r2 = await t2.then(text => {
|
||||
return "Resolved 2: " + text;
|
||||
}, (e) => { return `Rejected 2: ${e.message}`;});
|
||||
|
||||
readResult.textContent = r1 + "; " + r2;
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body onload="onLoad()">
|
||||
<button id="invokeReadOnceId">1</button>
|
||||
<button id="invokeReadTwiceId">2</button>
|
||||
<div id="readResultId"/>
|
||||
</body>
|
||||
</html>
|
|
@ -28,7 +28,6 @@ namespace {
|
|||
|
||||
using namespace mozilla;
|
||||
using namespace mozilla::dom;
|
||||
using std::vector;
|
||||
class DarwinGamepadService;
|
||||
|
||||
DarwinGamepadService* gService = nullptr;
|
||||
|
@ -80,6 +79,11 @@ const unsigned kBackUsage = 0x224;
|
|||
// 50ms is arbitrarily chosen.
|
||||
const uint32_t kDarwinGamepadPollInterval = 50;
|
||||
|
||||
struct GamepadInputReportContext {
|
||||
DarwinGamepadService* service;
|
||||
size_t gamepadSlot;
|
||||
};
|
||||
|
||||
class Gamepad {
|
||||
private:
|
||||
IOHIDDeviceRef mDevice;
|
||||
|
@ -120,7 +124,8 @@ class Gamepad {
|
|||
|
||||
GamepadHandle mHandle;
|
||||
RefPtr<GamepadRemapper> mRemapper;
|
||||
std::vector<uint8_t> mInputReport;
|
||||
nsTArray<uint8_t> mInputReport;
|
||||
UniquePtr<GamepadInputReportContext> mInputReportContext;
|
||||
};
|
||||
|
||||
void Gamepad::init(IOHIDDeviceRef aDevice, bool aDefaultRemapper) {
|
||||
|
@ -179,7 +184,7 @@ void Gamepad::init(IOHIDDeviceRef aDevice, bool aDefaultRemapper) {
|
|||
class DarwinGamepadService {
|
||||
private:
|
||||
IOHIDManagerRef mManager;
|
||||
vector<Gamepad> mGamepads;
|
||||
nsTArray<Gamepad> mGamepads;
|
||||
|
||||
nsCOMPtr<nsIThread> mMonitorThread;
|
||||
nsCOMPtr<nsIThread> mBackgroundThread;
|
||||
|
@ -272,14 +277,14 @@ void DarwinGamepadService::DeviceAdded(IOHIDDeviceRef device) {
|
|||
}
|
||||
|
||||
size_t slot = size_t(-1);
|
||||
for (size_t i = 0; i < mGamepads.size(); i++) {
|
||||
for (size_t i = 0; i < mGamepads.Length(); i++) {
|
||||
if (mGamepads[i] == device) return;
|
||||
if (slot == size_t(-1) && mGamepads[i].empty()) slot = i;
|
||||
}
|
||||
|
||||
if (slot == size_t(-1)) {
|
||||
slot = mGamepads.size();
|
||||
mGamepads.push_back(Gamepad());
|
||||
slot = mGamepads.Length();
|
||||
mGamepads.AppendElement(Gamepad());
|
||||
}
|
||||
|
||||
// Gather some identifying information
|
||||
|
@ -322,13 +327,15 @@ void DarwinGamepadService::DeviceAdded(IOHIDDeviceRef device) {
|
|||
}
|
||||
|
||||
mGamepads[slot].mHandle = handle;
|
||||
mGamepads[slot].mInputReport.resize(remapper->GetMaxInputReportLength());
|
||||
mGamepads[slot].mInputReport.SetLength(remapper->GetMaxInputReportLength());
|
||||
mGamepads[slot].mInputReportContext = UniquePtr<GamepadInputReportContext>(
|
||||
new GamepadInputReportContext{this, slot});
|
||||
mGamepads[slot].mRemapper = remapper.forget();
|
||||
|
||||
IOHIDDeviceRegisterInputReportCallback(
|
||||
device, mGamepads[slot].mInputReport.data(),
|
||||
mGamepads[slot].mInputReport.size(), ReportChangedCallback,
|
||||
&mGamepads[slot]);
|
||||
device, mGamepads[slot].mInputReport.Elements(),
|
||||
mGamepads[slot].mInputReport.Length(), ReportChangedCallback,
|
||||
mGamepads[slot].mInputReportContext.get());
|
||||
}
|
||||
|
||||
void DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device) {
|
||||
|
@ -337,13 +344,16 @@ void DarwinGamepadService::DeviceRemoved(IOHIDDeviceRef device) {
|
|||
if (!service) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0; i < mGamepads.size(); i++) {
|
||||
if (mGamepads[i] == device) {
|
||||
for (Gamepad& gamepad : mGamepads) {
|
||||
if (gamepad == device) {
|
||||
IOHIDDeviceRegisterInputReportCallback(
|
||||
device, mGamepads[i].mInputReport.data(), 0, NULL, &mGamepads[i]);
|
||||
device, gamepad.mInputReport.Elements(), 0, NULL,
|
||||
gamepad.mInputReportContext.get());
|
||||
|
||||
service->RemoveGamepad(mGamepads[i].mHandle);
|
||||
mGamepads[i].clear();
|
||||
gamepad.mInputReportContext.reset();
|
||||
|
||||
service->RemoveGamepad(gamepad.mHandle);
|
||||
gamepad.clear();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -354,7 +364,10 @@ void DarwinGamepadService::ReportChangedCallback(
|
|||
void* context, IOReturn result, void* sender, IOHIDReportType report_type,
|
||||
uint32_t report_id, uint8_t* report, CFIndex report_length) {
|
||||
if (context && report_type == kIOHIDReportTypeInput && report_length) {
|
||||
reinterpret_cast<Gamepad*>(context)->ReportChanged(report, report_length);
|
||||
auto reportContext = static_cast<GamepadInputReportContext*>(context);
|
||||
DarwinGamepadService* service = reportContext->service;
|
||||
service->mGamepads[reportContext->gamepadSlot].ReportChanged(report,
|
||||
report_length);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -388,8 +401,7 @@ void DarwinGamepadService::InputValueChanged(IOHIDValueRef value) {
|
|||
IOHIDElementRef element = IOHIDValueGetElement(value);
|
||||
IOHIDDeviceRef device = IOHIDElementGetDevice(element);
|
||||
|
||||
for (unsigned i = 0; i < mGamepads.size(); i++) {
|
||||
Gamepad& gamepad = mGamepads[i];
|
||||
for (Gamepad& gamepad : mGamepads) {
|
||||
if (gamepad == device) {
|
||||
// Axis elements represent axes and d-pads.
|
||||
if (Axis* axis = gamepad.lookupAxis(element)) {
|
||||
|
|
|
@ -224,8 +224,7 @@ bool RemoteDecoderManagerChild::Supports(
|
|||
}
|
||||
if (!supported) {
|
||||
// We haven't received the correct information yet from either the GPU or
|
||||
// the RDD process nor the Utility process; assume it is supported to
|
||||
// prevent false negative.
|
||||
// the RDD process nor the Utility process.
|
||||
if (aLocation == RemoteDecodeIn::UtilityProcess) {
|
||||
LaunchUtilityProcessIfNeeded();
|
||||
}
|
||||
|
@ -234,7 +233,20 @@ bool RemoteDecoderManagerChild::Supports(
|
|||
// TODO: This can be removed once bug 1684991 is fixed.
|
||||
LaunchRDDProcessIfNeeded();
|
||||
}
|
||||
return true;
|
||||
|
||||
// Assume the format is supported to prevent false negative, if the remote
|
||||
// process supports that specific track type.
|
||||
const bool isVideo = aParams.mConfig.IsVideo();
|
||||
const bool isAudio = aParams.mConfig.IsAudio();
|
||||
const auto trackSupport = GetTrackSupport(aLocation);
|
||||
if (isVideo) {
|
||||
return trackSupport.contains(TrackSupport::Video);
|
||||
}
|
||||
if (isAudio) {
|
||||
return trackSupport.contains(TrackSupport::Audio);
|
||||
}
|
||||
MOZ_ASSERT_UNREACHABLE("Not audio and video?!");
|
||||
return false;
|
||||
}
|
||||
|
||||
// We can ignore the SupportDecoderParams argument for now as creation of the
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
<!-- 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/. -->
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"><html><head><meta charset="utf-8"><title></title></head><body></body></html>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><meta charset="utf-8"><title></title></head>
|
||||
<body></body>
|
||||
</html>
|
||||
|
|
|
@ -96,11 +96,15 @@ interface TestInterfaceIterableDoubleUnion {
|
|||
iterable<DOMString, (DOMString or long)>;
|
||||
};
|
||||
|
||||
dictionary TestInterfaceAsyncIterableSingleOptions {
|
||||
boolean failToInit = false;
|
||||
};
|
||||
|
||||
[Pref="dom.expose_test_interfaces",
|
||||
Exposed=Window]
|
||||
interface TestInterfaceAsyncIterableSingle {
|
||||
[Throws]
|
||||
constructor();
|
||||
constructor(optional TestInterfaceAsyncIterableSingleOptions options = {});
|
||||
|
||||
async iterable<long>;
|
||||
};
|
||||
|
|
|
@ -729,6 +729,10 @@ bool MacOSFontEntry::SupportsOpenTypeFeature(Script aScript, uint32_t aFeatureTa
|
|||
if (!cgFont) {
|
||||
return mHasAATSmallCaps;
|
||||
}
|
||||
|
||||
CrashReporter::AutoAnnotateCrashReport autoFontName(CrashReporter::Annotation::FontName,
|
||||
FamilyName());
|
||||
|
||||
AutoCFRelease<CTFontRef> ctFont = CTFontCreateWithGraphicsFont(cgFont, 0.0, nullptr, nullptr);
|
||||
if (ctFont) {
|
||||
AutoCFRelease<CFArrayRef> features = CTFontCopyFeatures(ctFont);
|
||||
|
@ -1717,6 +1721,9 @@ gfxFontEntry* gfxMacPlatformFontList::LookupLocalFont(nsPresContext* aPresContex
|
|||
|
||||
nsAutoreleasePool localPool;
|
||||
|
||||
CrashReporter::AutoAnnotateCrashReport autoFontName(CrashReporter::Annotation::FontName,
|
||||
aFontName);
|
||||
|
||||
NSString* faceName = GetNSStringForString(NS_ConvertUTF8toUTF16(aFontName));
|
||||
|
||||
// lookup face based on postscript or full name
|
||||
|
@ -1782,6 +1789,9 @@ gfxFontEntry* gfxMacPlatformFontList::MakePlatformFont(const nsACString& aFontNa
|
|||
return nullptr;
|
||||
}
|
||||
|
||||
CrashReporter::AutoAnnotateCrashReport autoFontName(CrashReporter::Annotation::FontName,
|
||||
aFontName);
|
||||
|
||||
AutoCFRelease<CGDataProviderRef> provider =
|
||||
::CGDataProviderCreateWithData(nullptr, aFontData, aLength, &ReleaseData);
|
||||
AutoCFRelease<CGFontRef> fontRef = ::CGFontCreateWithDataProvider(provider);
|
||||
|
@ -2029,8 +2039,10 @@ void gfxMacPlatformFontList::GetFacesInitDataForFamily(const fontlist::Family* a
|
|||
bool aLoadCmaps) const {
|
||||
nsAutoreleasePool localPool;
|
||||
|
||||
NS_ConvertUTF8toUTF16 name(aFamily->Key().AsString(SharedFontList()));
|
||||
NSString* family = GetNSStringForString(name);
|
||||
auto name = aFamily->Key().AsString(SharedFontList());
|
||||
NSString* family = GetNSStringForString(NS_ConvertUTF8toUTF16(name));
|
||||
|
||||
CrashReporter::AutoAnnotateCrashReport autoFontName(CrashReporter::Annotation::FontName, name);
|
||||
|
||||
// returns an array of [psname, style name, weight, traits] elements, goofy api
|
||||
NSArray* fontfaces = [sFontManager availableMembersOfFontFamily:family];
|
||||
|
|
|
@ -2759,7 +2759,7 @@ void gfxPlatform::InitWebRenderConfig() {
|
|||
"FEATURE_FAILURE_WR_NO_GFX_INFO"_ns);
|
||||
useHwVideoZeroCopy = false;
|
||||
} else {
|
||||
if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
|
||||
if (status != nsIGfxInfo::FEATURE_ALLOW_ALWAYS) {
|
||||
FeatureState& feature =
|
||||
gfxConfig::GetFeature(Feature::HW_DECODED_VIDEO_ZERO_COPY);
|
||||
feature.DisableByDefault(FeatureStatus::Blocked,
|
||||
|
@ -2796,7 +2796,7 @@ void gfxPlatform::InitWebRenderConfig() {
|
|||
"FEATURE_FAILURE_WR_NO_GFX_INFO"_ns);
|
||||
reuseDecoderDevice = false;
|
||||
} else {
|
||||
if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
|
||||
if (status != nsIGfxInfo::FEATURE_ALLOW_ALWAYS) {
|
||||
FeatureState& feature =
|
||||
gfxConfig::GetFeature(Feature::REUSE_DECODER_DEVICE);
|
||||
feature.DisableByDefault(FeatureStatus::Blocked,
|
||||
|
|
13
js/src/configure
vendored
Executable file
13
js/src/configure
vendored
Executable file
|
@ -0,0 +1,13 @@
|
|||
#!/bin/sh
|
||||
# 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/.
|
||||
|
||||
SRCDIR=$(dirname $0)
|
||||
TOPSRCDIR="$SRCDIR"/../..
|
||||
PYTHON3="${PYTHON3:-python3}"
|
||||
export OLD_CONFIGURE="$SRCDIR"/old-configure
|
||||
|
||||
set -- "$@" --enable-project=js
|
||||
|
||||
exec "$PYTHON3" "$TOPSRCDIR/configure.py" "$@"
|
|
@ -1,22 +0,0 @@
|
|||
#!/bin/sh
|
||||
# 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/.
|
||||
#
|
||||
# Because adding a configure file in the tree is going to conflict with
|
||||
# existing configure files in people's (and automation) work trees, and
|
||||
# because some automation jobs are still running autoconf and configure
|
||||
# "manually", this file is actually an m4 file that is processed by
|
||||
# autoconf, but doesn't call any autoconf macros. The `divert` line
|
||||
# below ensures the script that follows is output by autoconf.
|
||||
: "divert(0)dnl"
|
||||
#!/bin/sh
|
||||
|
||||
SRCDIR=$(dirname $0)
|
||||
TOPSRCDIR="$SRCDIR"/../..
|
||||
PYTHON3="${PYTHON3:-python3}"
|
||||
export OLD_CONFIGURE="$SRCDIR"/old-configure
|
||||
|
||||
set -- "$@" --enable-project=js
|
||||
|
||||
exec "$PYTHON3" "$TOPSRCDIR/configure.py" "$@"
|
|
@ -8,7 +8,6 @@ import enum
|
|||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
@ -375,17 +374,9 @@ def copy_cargo_toml():
|
|||
def generate_configure():
|
||||
"""Generate configure files to avoid build dependency on autoconf-2.13"""
|
||||
|
||||
src_configure_in_file = topsrc_dir / "js" / "src" / "configure.in"
|
||||
src_old_configure_in_file = topsrc_dir / "js" / "src" / "old-configure.in"
|
||||
dest_configure_file = target_dir / "js" / "src" / "configure"
|
||||
dest_old_configure_file = target_dir / "js" / "src" / "old-configure"
|
||||
|
||||
shutil.copy2(
|
||||
str(src_configure_in_file), str(dest_configure_file), follow_symlinks=False
|
||||
)
|
||||
st = dest_configure_file.stat()
|
||||
dest_configure_file.chmod(st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||
|
||||
js_src_dir = topsrc_dir / "js" / "src"
|
||||
|
||||
env = os.environ.copy()
|
||||
|
|
|
@ -5642,8 +5642,9 @@ void PresShell::SynthesizeMouseMove(bool aFromScroll) {
|
|||
static nsView* FindFloatingViewContaining(nsPresContext* aRootPresContext,
|
||||
nsIWidget* aRootWidget,
|
||||
const LayoutDeviceIntPoint& aPt) {
|
||||
nsIFrame* popupFrame =
|
||||
nsLayoutUtils::GetPopupFrameForPoint(aRootPresContext, aRootWidget, aPt);
|
||||
nsIFrame* popupFrame = nsLayoutUtils::GetPopupFrameForPoint(
|
||||
aRootPresContext, aRootWidget, aPt,
|
||||
nsLayoutUtils::GetPopupFrameForPointFlags::OnlyReturnFramesWithWidgets);
|
||||
return popupFrame ? popupFrame->GetView() : nullptr;
|
||||
}
|
||||
|
||||
|
@ -9409,6 +9410,8 @@ void PresShell::WillDoReflow() {
|
|||
}
|
||||
|
||||
void PresShell::DidDoReflow(bool aInterruptible) {
|
||||
mHiddenContentInForcedLayout.Clear();
|
||||
|
||||
HandlePostedReflowCallbacks(aInterruptible);
|
||||
if (mIsDestroying) {
|
||||
return;
|
||||
|
@ -11821,3 +11824,41 @@ void PresShell::PingPerTickTelemetry(FlushType aFlushType) {
|
|||
bool PresShell::GetZoomableByAPZ() const {
|
||||
return mZoomConstraintsClient && mZoomConstraintsClient->GetAllowZoom();
|
||||
}
|
||||
|
||||
void PresShell::EnsureReflowIfFrameHasHiddenContent(nsIFrame* aFrame) {
|
||||
if (!aFrame || !aFrame->IsSubtreeDirty() ||
|
||||
!StaticPrefs::layout_css_content_visibility_enabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Flushing notifications below might trigger more layouts, which might,
|
||||
// in turn, trigger layout of other hidden content. We keep a local set
|
||||
// of hidden content we are laying out to handle recursive calls.
|
||||
nsTHashSet<nsIContent*> hiddenContentInForcedLayout;
|
||||
|
||||
MOZ_ASSERT(mHiddenContentInForcedLayout.IsEmpty());
|
||||
nsIFrame* topmostFrameWithContentHidden = nullptr;
|
||||
for (nsIFrame* cur = aFrame->GetInFlowParent(); cur;
|
||||
cur = cur->GetInFlowParent()) {
|
||||
if (cur->IsContentHidden()) {
|
||||
topmostFrameWithContentHidden = cur;
|
||||
mHiddenContentInForcedLayout.Insert(cur->GetContent());
|
||||
}
|
||||
}
|
||||
|
||||
if (mHiddenContentInForcedLayout.IsEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Queue and immediately flush a reflow for this node.
|
||||
MOZ_ASSERT(topmostFrameWithContentHidden);
|
||||
FrameNeedsReflow(topmostFrameWithContentHidden, IntrinsicDirty::Resize,
|
||||
NS_FRAME_IS_DIRTY);
|
||||
mDocument->FlushPendingNotifications(FlushType::Layout);
|
||||
|
||||
mHiddenContentInForcedLayout.Clear();
|
||||
}
|
||||
|
||||
bool PresShell::IsForcingLayoutForHiddenContent(const nsIFrame* aFrame) const {
|
||||
return mHiddenContentInForcedLayout.Contains(aFrame->GetContent());
|
||||
}
|
||||
|
|
|
@ -1739,6 +1739,18 @@ class PresShell final : public nsStubDocumentObserver,
|
|||
|
||||
bool GetZoomableByAPZ() const;
|
||||
|
||||
/**
|
||||
* If this frame has content hidden via `content-visibilty` that has a pending
|
||||
* reflow, force the content to reflow immediately.
|
||||
*/
|
||||
void EnsureReflowIfFrameHasHiddenContent(nsIFrame*);
|
||||
|
||||
/**
|
||||
* Whether or not this presshell is is forcing a reflow of hidden content in
|
||||
* this frame via EnsureReflowIfFrameHasHiddenContent().
|
||||
*/
|
||||
bool IsForcingLayoutForHiddenContent(const nsIFrame*) const;
|
||||
|
||||
private:
|
||||
~PresShell();
|
||||
|
||||
|
@ -2998,6 +3010,8 @@ class PresShell final : public nsStubDocumentObserver,
|
|||
nsTHashSet<nsIScrollableFrame*> mPendingScrollAnchorAdjustment;
|
||||
nsTHashSet<nsIScrollableFrame*> mPendingScrollResnap;
|
||||
|
||||
nsTHashSet<nsIContent*> mHiddenContentInForcedLayout;
|
||||
|
||||
nsCallbackEventRequest* mFirstCallbackEventRequest = nullptr;
|
||||
nsCallbackEventRequest* mLastCallbackEventRequest = nullptr;
|
||||
|
||||
|
|
|
@ -1774,7 +1774,8 @@ nsIFrame* nsLayoutUtils::GetPopupFrameForEventCoordinates(
|
|||
|
||||
nsIFrame* nsLayoutUtils::GetPopupFrameForPoint(
|
||||
nsPresContext* aRootPresContext, nsIWidget* aWidget,
|
||||
const mozilla::LayoutDeviceIntPoint& aPoint) {
|
||||
const mozilla::LayoutDeviceIntPoint& aPoint,
|
||||
GetPopupFrameForPointFlags aFlags /* = GetPopupFrameForPointFlags(0) */) {
|
||||
nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
|
||||
if (!pm) {
|
||||
return nullptr;
|
||||
|
@ -1783,11 +1784,19 @@ nsIFrame* nsLayoutUtils::GetPopupFrameForPoint(
|
|||
pm->GetVisiblePopups(popups);
|
||||
// Search from top to bottom
|
||||
for (nsIFrame* popup : popups) {
|
||||
if (popup->PresContext()->GetRootPresContext() == aRootPresContext &&
|
||||
popup->ScrollableOverflowRect().Contains(GetEventCoordinatesRelativeTo(
|
||||
aWidget, aPoint, RelativeTo{popup}))) {
|
||||
return popup;
|
||||
if (popup->PresContext()->GetRootPresContext() != aRootPresContext) {
|
||||
continue;
|
||||
}
|
||||
if (!popup->ScrollableOverflowRect().Contains(GetEventCoordinatesRelativeTo(
|
||||
aWidget, aPoint, RelativeTo{popup}))) {
|
||||
continue;
|
||||
}
|
||||
if (aFlags & GetPopupFrameForPointFlags::OnlyReturnFramesWithWidgets) {
|
||||
if (!popup->HasView() || !popup->GetView()->HasWidget()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return popup;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue