зеркало из https://github.com/mozilla/gecko-dev.git
Merge autoland to mozilla-central. a=merge
This commit is contained in:
Коммит
d611a1c062
|
@ -40,32 +40,53 @@ static const wchar_t* ConsumerStringMap[CONSUMERS_ENUM_LEN+1] = {
|
|||
L"\0"
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if module version is lesser than the given version.
|
||||
*/
|
||||
bool
|
||||
IsModuleVersionLessThan(HMODULE aModuleHandle, DWORD aMajor, DWORD aMinor)
|
||||
Compatibility::IsModuleVersionLessThan(HMODULE aModuleHandle,
|
||||
unsigned long long aVersion)
|
||||
{
|
||||
wchar_t fileName[MAX_PATH];
|
||||
::GetModuleFileNameW(aModuleHandle, fileName, MAX_PATH);
|
||||
// Get the full path to the dll.
|
||||
// We start with MAX_PATH, but the path can actually be longer.
|
||||
DWORD fnSize = MAX_PATH;
|
||||
UniquePtr<wchar_t[]> fileName;
|
||||
while (true) {
|
||||
fileName = MakeUnique<wchar_t[]>(fnSize);
|
||||
DWORD retLen = ::GetModuleFileNameW(aModuleHandle, fileName.get(), fnSize);
|
||||
MOZ_ASSERT(retLen != 0);
|
||||
if (retLen == 0) {
|
||||
return true;
|
||||
}
|
||||
if (retLen == fnSize &&
|
||||
::GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
|
||||
// The buffer was too short. Increase the size and try again.
|
||||
fnSize *= 2;
|
||||
}
|
||||
break; // Success!
|
||||
}
|
||||
|
||||
DWORD dummy = 0;
|
||||
DWORD length = ::GetFileVersionInfoSizeW(fileName, &dummy);
|
||||
// Get the version info from the file.
|
||||
DWORD length = ::GetFileVersionInfoSizeW(fileName.get(), nullptr);
|
||||
if (length == 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
LPBYTE versionInfo = new BYTE[length];
|
||||
::GetFileVersionInfoW(fileName, 0, length, versionInfo);
|
||||
auto versionInfo = MakeUnique<unsigned char[]>(length);
|
||||
if (!::GetFileVersionInfoW(fileName.get(), 0, length, versionInfo.get())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UINT uLen;
|
||||
VS_FIXEDFILEINFO* fixedFileInfo = nullptr;
|
||||
::VerQueryValueW(versionInfo, L"\\", (LPVOID*)&fixedFileInfo, &uLen);
|
||||
DWORD dwFileVersionMS = fixedFileInfo->dwFileVersionMS;
|
||||
DWORD dwFileVersionLS = fixedFileInfo->dwFileVersionLS;
|
||||
delete [] versionInfo;
|
||||
if (!::VerQueryValueW(versionInfo.get(), L"\\", (LPVOID*)&fixedFileInfo,
|
||||
&uLen)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
DWORD dwLeftMost = HIWORD(dwFileVersionMS);
|
||||
DWORD dwSecondRight = HIWORD(dwFileVersionLS);
|
||||
return (dwLeftMost < aMajor ||
|
||||
(dwLeftMost == aMajor && dwSecondRight < aMinor));
|
||||
// Combine into a 64 bit value for comparison.
|
||||
unsigned long long version =
|
||||
((unsigned long long)fixedFileInfo->dwFileVersionMS) << 32 |
|
||||
((unsigned long long)fixedFileInfo->dwFileVersionLS);
|
||||
|
||||
return version < aVersion;
|
||||
}
|
||||
|
||||
|
||||
|
@ -166,9 +187,11 @@ uint32_t Compatibility::sConsumers = Compatibility::UNKNOWN;
|
|||
Compatibility::InitConsumers()
|
||||
{
|
||||
HMODULE jawsHandle = ::GetModuleHandleW(L"jhook");
|
||||
if (jawsHandle)
|
||||
sConsumers |= (IsModuleVersionLessThan(jawsHandle, 19, 0)) ?
|
||||
OLDJAWS : JAWS;
|
||||
if (jawsHandle) {
|
||||
sConsumers |=
|
||||
IsModuleVersionLessThan(jawsHandle, MAKE_FILE_VERSION(19, 0, 0, 0)) ?
|
||||
OLDJAWS : JAWS;
|
||||
}
|
||||
|
||||
if (::GetModuleHandleW(L"gwm32inc"))
|
||||
sConsumers |= WE;
|
||||
|
|
|
@ -67,6 +67,16 @@ public:
|
|||
*/
|
||||
static bool HasKnownNonUiaConsumer();
|
||||
|
||||
/**
|
||||
* Return true if a module's version is lesser than the given version.
|
||||
* Generally, the version should be provided using the MAKE_FILE_VERSION
|
||||
* macro.
|
||||
* If the version information cannot be retrieved, true is returned; i.e.
|
||||
* no version information implies an earlier version.
|
||||
*/
|
||||
static bool IsModuleVersionLessThan(HMODULE aModuleHandle,
|
||||
unsigned long long aVersion);
|
||||
|
||||
private:
|
||||
Compatibility();
|
||||
Compatibility(const Compatibility&);
|
||||
|
@ -101,4 +111,10 @@ private:
|
|||
} // a11y namespace
|
||||
} // mozilla namespace
|
||||
|
||||
// Convert the 4 (decimal) components of a DLL version number into a
|
||||
// single unsigned long long, as needed by
|
||||
// mozilla::a11y::Compatibility::IsModuleVersionLessThan.
|
||||
#define MAKE_FILE_VERSION(a,b,c,d)\
|
||||
((a##ULL << 48) + (b##ULL << 32) + (c##ULL << 16) + d##ULL)
|
||||
|
||||
#endif
|
||||
|
|
|
@ -165,12 +165,30 @@ LazyInstantiator::GetClientPid(const DWORD aClientTid)
|
|||
return ::GetProcessIdOfThread(callingThread);
|
||||
}
|
||||
|
||||
#define ALL_VERSIONS ((unsigned long long)-1LL)
|
||||
|
||||
struct DllBlockInfo {
|
||||
// The name of the DLL.
|
||||
const wchar_t* mName;
|
||||
|
||||
// If mUntilVersion is ALL_VERSIONS, we'll block all versions of this dll.
|
||||
// Otherwise, we'll block all versions less than the given version, as queried
|
||||
// by GetFileVersionInfo and VS_FIXEDFILEINFO's dwFileVersionMS and
|
||||
// dwFileVersionLS fields.
|
||||
//
|
||||
// Note that the version is usually 4 components, which is A.B.C.D
|
||||
// encoded as 0x AAAA BBBB CCCC DDDD ULL (spaces added for clarity).
|
||||
unsigned long long mUntilVersion;
|
||||
};
|
||||
|
||||
/**
|
||||
* This is the blocklist for known "bad" DLLs that instantiate a11y.
|
||||
*/
|
||||
static const wchar_t* gBlockedInprocDlls[] = {
|
||||
L"dtvhooks.dll", // RealPlayer, bug 1418535
|
||||
L"dtvhooks64.dll" // RealPlayer, bug 1418535
|
||||
static const DllBlockInfo gBlockedInprocDlls[] = {
|
||||
// RealPlayer, bug 1418535, bug 1437417
|
||||
// Versions before 18.1.11.0 cause severe performance problems.
|
||||
{L"dtvhooks.dll", MAKE_FILE_VERSION(18, 1, 11, 0)},
|
||||
{L"dtvhooks64.dll", MAKE_FILE_VERSION(18, 1, 11, 0)}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -201,9 +219,17 @@ LazyInstantiator::IsBlockedInjection()
|
|||
|
||||
for (size_t index = 0, len = ArrayLength(gBlockedInprocDlls); index < len;
|
||||
++index) {
|
||||
if (::GetModuleHandleW(gBlockedInprocDlls[index])) {
|
||||
const DllBlockInfo& blockedDll = gBlockedInprocDlls[index];
|
||||
HMODULE module = ::GetModuleHandleW(blockedDll.mName);
|
||||
if (!module) {
|
||||
// This dll isn't loaded.
|
||||
continue;
|
||||
}
|
||||
if (blockedDll.mUntilVersion == ALL_VERSIONS) {
|
||||
return true;
|
||||
}
|
||||
return Compatibility::IsModuleVersionLessThan(module,
|
||||
blockedDll.mUntilVersion);
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
|
@ -1289,7 +1289,11 @@ pref("browser.newtabpage.directory.source", "https://tiles.services.mozilla.com/
|
|||
// Activity Stream prefs that control to which page to redirect
|
||||
pref("browser.newtabpage.activity-stream.prerender", true);
|
||||
#ifndef RELEASE_OR_BETA
|
||||
#ifdef MOZILLA_OFFICIAL
|
||||
pref("browser.newtabpage.activity-stream.debug", false);
|
||||
#else
|
||||
pref("browser.newtabpage.activity-stream.debug", true);
|
||||
#endif
|
||||
#endif
|
||||
|
||||
pref("browser.library.activity-stream.enabled", true);
|
||||
|
|
|
@ -523,7 +523,6 @@ skip-if = true # Bug 1409184 disabled because interactive find next is not autom
|
|||
[browser_visibleTabs.js]
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_visibleTabs_bookmarkAllPages.js]
|
||||
skip-if = true # Bug 1005420 - fails intermittently. also with e10s enabled: bizarre problem with hidden tab having _mouseenter called, via _setPositionalAttributes, and tab not being found resulting in 'candidate is undefined'
|
||||
# DO NOT ADD MORE TESTS HERE. USE A TOPICAL DIRECTORY INSTEAD.
|
||||
[browser_visibleTabs_tabPreview.js]
|
||||
skip-if = (os == "win" && !debug)
|
||||
|
|
|
@ -12,7 +12,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
|
|||
"@mozilla.org/browser/aboutnewtab-service;1",
|
||||
"nsIAboutNewTabService");
|
||||
|
||||
const IS_RELEASE_OR_BETA = AppConstants.RELEASE_OR_BETA;
|
||||
const {IS_RELEASE_OR_BETA, MOZILLA_OFFICIAL} = AppConstants;
|
||||
|
||||
const ACTIVITY_STREAM_PRERENDER_URL = "resource://activity-stream/prerendered/en-US/activity-stream-prerendered.html";
|
||||
const ACTIVITY_STREAM_PRERENDER_DEBUG_URL = "resource://activity-stream/prerendered/static/activity-stream-prerendered-debug.html";
|
||||
|
@ -134,6 +134,16 @@ add_task(function test_locale() {
|
|||
"The locale for testing should be en-US");
|
||||
});
|
||||
|
||||
add_task(async function test_debug_mode() {
|
||||
if (!IS_RELEASE_OR_BETA && !MOZILLA_OFFICIAL) { // Check if local build
|
||||
Assert.equal(aboutNewTabService.activityStreamDebug, true,
|
||||
"Debug mode is set for builds that are not official");
|
||||
} else {
|
||||
Assert.equal(aboutNewTabService.activityStreamDebug, false,
|
||||
"Debug mode is not set for any other builds");
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests reponse to updates to prefs
|
||||
*/
|
||||
|
@ -195,6 +205,9 @@ function setBoolPrefAndWaitForChange(pref, value, testMessage) {
|
|||
|
||||
|
||||
function setupASPrerendered() {
|
||||
// Don't run in debug mode regardless of build type
|
||||
Services.prefs.setBoolPref(ACTIVITY_STREAM_DEBUG_PREF, false);
|
||||
|
||||
if (Services.prefs.getBoolPref(ACTIVITY_STREAM_PRERENDER_PREF)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
|
|
@ -99,7 +99,7 @@ skip-if = (os == 'win' && ccov) # Bug 1423667
|
|||
skip-if = (os == 'win' && ccov) # Bug 1423667
|
||||
subsuite = clipboard
|
||||
[browser_sidebarpanels_click.js]
|
||||
skip-if = true # temporarily disabled for breaking the treeview - bug 658744
|
||||
skip-if = (os == 'win' && ccov) || (os == "mac" && debug) # Bug 1423667
|
||||
[browser_sort_in_library.js]
|
||||
skip-if = (os == 'win' && ccov) # Bug 1423667
|
||||
[browser_stayopenmenu.js]
|
||||
|
|
|
@ -50,9 +50,12 @@ buildscript {
|
|||
}
|
||||
}
|
||||
|
||||
ext.kotlin_version = '1.1.51'
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.0.1'
|
||||
classpath 'com.getkeepsafe.dexcount:dexcount-gradle-plugin:0.8.2'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -390,6 +390,7 @@ private:
|
|||
#define REL(machine, type) (EM_ ## machine | (R_ ## machine ## _ ## type << 8))
|
||||
switch (elf->getMachine() | (ELF32_R_TYPE(r->r_info) << 8)) {
|
||||
case REL(X86_64, PC32):
|
||||
case REL(X86_64, PLT32):
|
||||
case REL(386, PC32):
|
||||
case REL(386, GOTPC):
|
||||
case REL(ARM, GOTPC):
|
||||
|
|
|
@ -221,6 +221,29 @@ public:
|
|||
CSSPseudoElementType aPseudoType,
|
||||
nsStyleContext* aStyleContext);
|
||||
|
||||
// Update the mPropertiesWithImportantRules and
|
||||
// mPropertiesForAnimationsLevel members of the given EffectSet, and also
|
||||
// request any restyles required by changes to the cascade result.
|
||||
//
|
||||
// NOTE: This can be expensive so we should only call it if styles that apply
|
||||
// above the animation level of the cascade might have changed. For all
|
||||
// other cases we should call MaybeUpdateCascadeResults.
|
||||
//
|
||||
// This is typically reserved for internal callers but is public here since
|
||||
// when we detect changes to the cascade on the Servo side we can't call
|
||||
// MarkCascadeNeedsUpdate during the traversal so instead we call this as part
|
||||
// of a follow-up sequential task.
|
||||
//
|
||||
// As with MaybeUpdateCascadeResults, |aStyleContext| is only used
|
||||
// when |aBackendType| is StyleBackendType::Gecko. When |aBackendType| is
|
||||
// StyleBackendType::Servo, it is ignored.
|
||||
static void
|
||||
UpdateCascadeResults(StyleBackendType aBackendType,
|
||||
EffectSet& aEffectSet,
|
||||
dom::Element* aElement,
|
||||
CSSPseudoElementType aPseudoType,
|
||||
nsStyleContext* aStyleContext);
|
||||
|
||||
// Helper to fetch the corresponding element and pseudo-type from a frame.
|
||||
//
|
||||
// For frames corresponding to pseudo-elements, the returned element is the
|
||||
|
@ -293,24 +316,6 @@ private:
|
|||
CSSPseudoElementType aPseudoType,
|
||||
nsStyleContext* aStyleContext);
|
||||
|
||||
// Update the mPropertiesWithImportantRules and
|
||||
// mPropertiesForAnimationsLevel members of the given EffectSet, and also
|
||||
// request any restyles required by changes to the cascade result.
|
||||
//
|
||||
// This can be expensive so we should only call it if styles that apply
|
||||
// above the animation level of the cascade might have changed. For all
|
||||
// other cases we should call MaybeUpdateCascadeResults.
|
||||
//
|
||||
// As with MaybeUpdateCascadeResults, |aStyleContext| is only used
|
||||
// when |aBackendType| is StyleBackendType::Gecko. When |aBackendType| is
|
||||
// StyleBackendType::Servo, it is ignored.
|
||||
static void
|
||||
UpdateCascadeResults(StyleBackendType aBackendType,
|
||||
EffectSet& aEffectSet,
|
||||
dom::Element* aElement,
|
||||
CSSPseudoElementType aPseudoType,
|
||||
nsStyleContext* aStyleContext);
|
||||
|
||||
static nsPresContext* GetPresContext(dom::Element* aElement);
|
||||
|
||||
nsPresContext* mPresContext;
|
||||
|
|
|
@ -1280,22 +1280,6 @@ waitForAllPaints(() => {
|
|||
|
||||
document.body.appendChild(div);
|
||||
|
||||
markers = await observeStyling(1);
|
||||
if (isServo) {
|
||||
// In Servo, we explicitly set important_rules_change flag to the element
|
||||
// in compute_style() during the initial normal traversal right after
|
||||
// re-attaching which leads to invoking a SequentialTask for
|
||||
// CascadeResults which ends up calling RequestRestyle(Standard). As a
|
||||
// result, the animation is restyled during a second animation restyle in
|
||||
// the first frame.
|
||||
todo_is(markers.length, 1,
|
||||
'Bug 1388560 We should observe one restyle in the first frame ' +
|
||||
'right after re-attaching to the document');
|
||||
} else {
|
||||
is(markers.length, 1,
|
||||
'We should observe one restyle in the first frame right after ' +
|
||||
're-attaching to the document');
|
||||
}
|
||||
markers = await observeStyling(5);
|
||||
is(markers.length, 5,
|
||||
'Animation on re-attached to the document begins to update style');
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html class="reftest-wait">
|
||||
<html class="reftest-wait reftest-no-flush">
|
||||
<!--
|
||||
reftest-no-flush above is necessary to make invalidation changes happen.
|
||||
Without it, reftest harness takes the snapshot on the next frame after the
|
||||
frame that a paint for appending the element happened.
|
||||
-->
|
||||
<title>Transform animation whose target is not initially associated with any document creates a stacking context even if it has only 'transform:none' in its keyframe</title>
|
||||
<style>
|
||||
span {
|
||||
|
|
|
@ -671,14 +671,23 @@ Gecko_UpdateAnimations(RawGeckoElementBorrowed aElement,
|
|||
}
|
||||
|
||||
if (aTasks & UpdateAnimationsTasks::CascadeResults) {
|
||||
// This task will be scheduled if we detected any changes to !important
|
||||
// rules. We post a restyle here so that we can update the cascade
|
||||
// results in the pre-traversal of the next restyle.
|
||||
presContext->EffectCompositor()
|
||||
->RequestRestyle(const_cast<Element*>(aElement),
|
||||
pseudoType,
|
||||
EffectCompositor::RestyleType::Standard,
|
||||
EffectCompositor::CascadeLevel::Animations);
|
||||
EffectSet* effectSet = EffectSet::GetEffectSet(aElement, pseudoType);
|
||||
// CSS animations/transitions might have been destroyed as part of the above
|
||||
// steps so before updating cascade results, we check if there are still any
|
||||
// animations to update.
|
||||
if (effectSet) {
|
||||
// We call UpdateCascadeResults directly (intead of
|
||||
// MaybeUpdateCascadeResults) since we know for sure that the cascade has
|
||||
// changed, but we were unable to call MarkCascadeUpdated when we noticed
|
||||
// it since we avoid mutating state as part of the Servo parallel
|
||||
// traversal.
|
||||
presContext->EffectCompositor()
|
||||
->UpdateCascadeResults(StyleBackendType::Servo,
|
||||
*effectSet,
|
||||
const_cast<Element*>(aElement),
|
||||
pseudoType,
|
||||
nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
if (aTasks & UpdateAnimationsTasks::DisplayChangedFromNone) {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
buildDir "${topobjdir}/gradle/build/mobile/android/geckoview"
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
apply plugin: 'kotlin-android'
|
||||
|
||||
apply from: "${topsrcdir}/mobile/android/gradle/product_flavors.gradle"
|
||||
|
||||
|
@ -151,10 +152,12 @@ dependencies {
|
|||
implementation "com.android.support:support-v4:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
||||
implementation "com.android.support:palette-v7:${mozconfig.substs.ANDROID_SUPPORT_LIBRARY_VERSION}"
|
||||
|
||||
testImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
testImplementation 'junit:junit:4.12'
|
||||
testImplementation 'org.robolectric:robolectric:3.5.1'
|
||||
testImplementation 'org.mockito:mockito-core:1.10.19'
|
||||
|
||||
androidTestImplementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
androidTestImplementation 'com.android.support.test:runner:0.5'
|
||||
androidTestImplementation 'com.android.support.test:rules:0.5'
|
||||
androidTestImplementation 'com.android.support.test.espresso:espresso-core:2.2.2'
|
||||
|
@ -220,7 +223,7 @@ android.libraryVariants.all { variant ->
|
|||
}
|
||||
|
||||
apply plugin: 'maven'
|
||||
|
||||
|
||||
uploadArchives {
|
||||
repositories.mavenDeployer {
|
||||
pom.groupId = 'org.mozilla'
|
||||
|
@ -264,6 +267,18 @@ afterEvaluate {
|
|||
archives javadocJarOfficialWithGeckoBinariesNoMinApiRelease
|
||||
archives sourcesJarOfficialWithGeckoBinariesNoMinApiRelease
|
||||
}
|
||||
|
||||
// For now, ensure Kotlin is only used in tests.
|
||||
android.sourceSets.all { sourceSet ->
|
||||
if (sourceSet.name.startsWith('test') || sourceSet.name.startsWith('androidTest')) {
|
||||
return
|
||||
}
|
||||
(sourceSet.java.srcDirs + sourceSet.kotlin.srcDirs).each {
|
||||
if (!fileTree(it, { include '**/*.kt' }).empty) {
|
||||
throw new GradleException("Kotlin used in non-test directory ${it.path}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bug 1353055 - Strip 'vars' debugging information to agree with moz.build.
|
||||
|
|
|
@ -0,0 +1,483 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.geckoview.test
|
||||
|
||||
import org.mozilla.gecko.GeckoSession
|
||||
import org.mozilla.gecko.GeckoSessionSettings
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.AssertCalled
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.Setting
|
||||
import org.mozilla.geckoview.test.rule.GeckoSessionTestRule.TimeoutMillis
|
||||
import org.mozilla.geckoview.test.util.Callbacks
|
||||
|
||||
import android.support.test.filters.LargeTest
|
||||
import android.support.test.filters.MediumTest
|
||||
import android.support.test.runner.AndroidJUnit4
|
||||
import org.hamcrest.CoreMatchers.both
|
||||
import org.hamcrest.Matchers.greaterThan
|
||||
import org.hamcrest.Matchers.lessThan
|
||||
import org.junit.Assert.*
|
||||
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/**
|
||||
* Test for the GeckoSessionTestRule class, to ensure it properly sets up a session for
|
||||
* each test, and to ensure it can properly wait for and assert listener/delegate
|
||||
* callbacks.
|
||||
*/
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
@MediumTest
|
||||
class GeckoSessionTestRuleTest {
|
||||
companion object {
|
||||
const val HELLO_URI = GeckoSessionTestRule.APK_URI_PREFIX + "/assets/www/hello.html"
|
||||
}
|
||||
|
||||
@get:Rule val sessionRule = GeckoSessionTestRule()
|
||||
|
||||
@Test fun getSession() {
|
||||
assertNotNull("Can get session", sessionRule.session)
|
||||
assertTrue("Session is open", sessionRule.session.isOpen)
|
||||
}
|
||||
|
||||
@Setting.List(Setting(key = Setting.Key.USE_PRIVATE_MODE, value = "true"),
|
||||
Setting(key = Setting.Key.DISPLAY_MODE, value = "DISPLAY_MODE_MINIMAL_UI"))
|
||||
@Test fun settingsApplied() {
|
||||
assertEquals("USE_PRIVATE_MODE should be set", true,
|
||||
sessionRule.session.settings.getBoolean(GeckoSessionSettings.USE_PRIVATE_MODE))
|
||||
assertEquals("DISPLAY_MODE should be set",
|
||||
GeckoSessionSettings.DISPLAY_MODE_MINIMAL_UI,
|
||||
sessionRule.session.settings.getInt(GeckoSessionSettings.DISPLAY_MODE))
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
@TimeoutMillis(1000)
|
||||
@LargeTest
|
||||
fun noPendingCallbacks() {
|
||||
// Make sure we don't have unexpected pending callbacks at the start of a test.
|
||||
sessionRule.waitUntilCalled(object : Callbacks.All {})
|
||||
}
|
||||
|
||||
@Test fun waitForPageStop() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 1, counter)
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun waitForPageStop_throwOnChangedCallback() {
|
||||
sessionRule.session.progressListener = Callbacks.Default
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
}
|
||||
|
||||
@Test fun waitForPageStops() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
sessionRule.waitForPageStops(2)
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 2, counter)
|
||||
}
|
||||
|
||||
@Test fun waitUntilCalled_anyInterfaceMethod() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitUntilCalled(GeckoSession.ProgressListener::class.java)
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
|
||||
override fun onSecurityChange(session: GeckoSession,
|
||||
securityInfo: GeckoSession.ProgressListener.SecurityInformation) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 1, counter)
|
||||
}
|
||||
|
||||
@Test fun waitUntilCalled_specificInterfaceMethod() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitUntilCalled(GeckoSession.ProgressListener::class.java,
|
||||
"onPageStart", "onPageStop")
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 2, counter)
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun waitUntilCalled_throwOnNotGeckoSessionInterface() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitUntilCalled(CharSequence::class.java)
|
||||
}
|
||||
|
||||
@Test fun waitUntilCalled_anyObjectMethod() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.waitUntilCalled(object : Callbacks.ProgressListener {
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
|
||||
override fun onSecurityChange(session: GeckoSession,
|
||||
securityInfo: GeckoSession.ProgressListener.SecurityInformation) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 1, counter)
|
||||
}
|
||||
|
||||
@Test fun waitUntilCalled_specificObjectMethod() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.waitUntilCalled(object : Callbacks.ProgressListener {
|
||||
@AssertCalled
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
@AssertCalled
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 2, counter)
|
||||
}
|
||||
|
||||
@Test fun waitUntilCalled_multipleCount() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.waitUntilCalled(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(count = 2)
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
@AssertCalled(count = 2)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 4, counter)
|
||||
}
|
||||
|
||||
@Test fun waitUntilCalled_currentCall() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.waitUntilCalled(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(count = 2, order = intArrayOf(1, 2))
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
val info = sessionRule.currentCall
|
||||
assertNotNull("Method info should be valid", info)
|
||||
assertThat("Counter should be correct", info.counter,
|
||||
both(greaterThan(0)).and(lessThan(3)))
|
||||
assertEquals("Order should equal counter", info.counter, info.order)
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 2, counter)
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_anyMethod() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 1, counter)
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun forCallbacksDuringWait_throwOnAnyMethodNotCalled() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
sessionRule.forCallbacksDuringWait(GeckoSession.ScrollListener { _, _, _ -> })
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_specificMethod() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
@AssertCalled
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 2, counter)
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_specificMethodMultipleTimes() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
sessionRule.waitForPageStops(2)
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
@AssertCalled
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 4, counter)
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun forCallbacksDuringWait_throwOnSpecificMethodNotCalled() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
sessionRule.forCallbacksDuringWait(
|
||||
GeckoSession.ScrollListener @AssertCalled { _, _, _ -> })
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_specificCount() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
sessionRule.waitForPageStops(2)
|
||||
|
||||
var counter = 0
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(count = 2)
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
@AssertCalled(count = 2)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 4, counter)
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun forCallbacksDuringWait_throwOnWrongCount() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
sessionRule.waitForPageStops(2)
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_specificOrder() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(order = intArrayOf(1))
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
@AssertCalled(order = intArrayOf(2))
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun forCallbacksDuringWait_throwOnWrongOrder() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(order = intArrayOf(2))
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
@AssertCalled(order = intArrayOf(1))
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_multipleOrder() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
sessionRule.waitForPageStops(2)
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(order = intArrayOf(1, 3, 1))
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
@AssertCalled(order = intArrayOf(2, 4, 1))
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun forCallbacksDuringWait_throwOnWrongMultipleOrder() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
sessionRule.waitForPageStops(2)
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(order = intArrayOf(1, 2, 1))
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
@AssertCalled(order = intArrayOf(3, 4, 1))
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_notCalled() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
sessionRule.forCallbacksDuringWait(
|
||||
GeckoSession.ScrollListener @AssertCalled(false) { _, _, _ -> })
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun forCallbacksDuringWait_throwOnCallingNoCall() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(false)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_limitedToLastWait() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.session.reload()
|
||||
sessionRule.session.reload()
|
||||
sessionRule.session.reload()
|
||||
|
||||
// Wait for Gecko to finish all loads.
|
||||
Thread.sleep(100)
|
||||
|
||||
sessionRule.waitForPageStop() // Wait for loadUri.
|
||||
sessionRule.waitForPageStop() // Wait for first reload.
|
||||
|
||||
var counter = 0
|
||||
|
||||
// assert should only apply to callbacks within range (loadUri, first reload].
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
counter++
|
||||
}
|
||||
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
counter++
|
||||
}
|
||||
})
|
||||
|
||||
assertEquals("Callback count should be correct", 2, counter)
|
||||
}
|
||||
|
||||
@Test fun forCallbacksDuringWait_currentCall() {
|
||||
sessionRule.session.loadUri(HELLO_URI)
|
||||
sessionRule.waitForPageStop()
|
||||
|
||||
sessionRule.forCallbacksDuringWait(object : Callbacks.ProgressListener {
|
||||
@AssertCalled(count = 1)
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
val info = sessionRule.currentCall
|
||||
assertNotNull("Method info should be valid", info)
|
||||
assertEquals("Counter should be correct", 1, info.counter)
|
||||
assertEquals("Order should equal counter", 0, info.order)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@Test(expected = AssertionError::class)
|
||||
fun getCurrentCall_throwOnNoCurrentCall() {
|
||||
sessionRule.currentCall
|
||||
}
|
||||
}
|
|
@ -0,0 +1,810 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.geckoview.test.rule;
|
||||
|
||||
import org.mozilla.gecko.GeckoSession;
|
||||
import org.mozilla.gecko.GeckoSessionSettings;
|
||||
import org.mozilla.geckoview.test.util.Callbacks;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.Matchers.greaterThan;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertSame;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.runner.Description;
|
||||
import org.junit.runners.model.Statement;
|
||||
|
||||
import android.app.Instrumentation;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.MessageQueue;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.rule.UiThreadTestRule;
|
||||
|
||||
import java.lang.annotation.Annotation;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.ParameterizedType;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* TestRule that, for each test, sets up a GeckoSession, runs the test on the UI thread,
|
||||
* and tears down the GeckoSession at the end of the test. The rule also provides methods
|
||||
* for waiting on particular callbacks to be called, and methods for asserting that
|
||||
* callbacks are called in the proper order.
|
||||
*/
|
||||
public class GeckoSessionTestRule extends UiThreadTestRule {
|
||||
|
||||
private static final long DEFAULT_TIMEOUT_MILLIS = 10000;
|
||||
public static final String APK_URI_PREFIX = "resource://android";
|
||||
|
||||
/**
|
||||
* Specify the timeout for any of the wait methods, in milliseconds. Can be used
|
||||
* on classes or methods.
|
||||
*/
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface TimeoutMillis {
|
||||
long value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Specify a list of GeckoSession settings to be applied to the GeckoSession object
|
||||
* under test. Can be used on classes or methods. Note that the settings values must
|
||||
* be string literals regardless of the type of the settings.
|
||||
* <p>
|
||||
* Disable e10s for a particular test:
|
||||
* <pre>
|
||||
* @Setting.List(@Setting(key = Setting.Key.USE_MULTIPROCESS,
|
||||
* value = "false"))
|
||||
* @Test public void test() { ... }
|
||||
* </pre>
|
||||
* <p>
|
||||
* Use multiple settings:
|
||||
* <pre>
|
||||
* @Setting.List({@Setting(key = Setting.Key.USE_MULTIPROCESS,
|
||||
* value = "false"),
|
||||
* @Setting(key = Setting.Key.USE_TRACKING_PROTECTION,
|
||||
* value = "true")})
|
||||
* </pre>
|
||||
*/
|
||||
@Target(ElementType.ANNOTATION_TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface Setting {
|
||||
enum Key {
|
||||
CHROME_URI,
|
||||
DISPLAY_MODE,
|
||||
SCREEN_ID,
|
||||
USE_MULTIPROCESS,
|
||||
USE_PRIVATE_MODE,
|
||||
USE_REMOTE_DEBUGGER,
|
||||
USE_TRACKING_PROTECTION;
|
||||
|
||||
private final GeckoSessionSettings.Key<?> mKey;
|
||||
private final Class<?> mType;
|
||||
|
||||
Key() {
|
||||
final Field field;
|
||||
try {
|
||||
field = GeckoSessionSettings.class.getField(name());
|
||||
mKey = (GeckoSessionSettings.Key<?>) field.get(null);
|
||||
} catch (final NoSuchFieldException | IllegalAccessException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
final ParameterizedType genericType = (ParameterizedType) field.getGenericType();
|
||||
mType = (Class<?>) genericType.getActualTypeArguments()[0];
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void set(final GeckoSessionSettings settings, final String value) {
|
||||
if (boolean.class.equals(mType) || Boolean.class.equals(mType)) {
|
||||
settings.setBoolean((GeckoSessionSettings.Key<Boolean>) mKey,
|
||||
Boolean.valueOf(value));
|
||||
} else if (int.class.equals(mType) || Integer.class.equals(mType)) {
|
||||
try {
|
||||
settings.setInt((GeckoSessionSettings.Key<Integer>) mKey,
|
||||
(Integer) GeckoSessionSettings.class.getField(value)
|
||||
.get(null));
|
||||
return;
|
||||
} catch (final NoSuchFieldException | IllegalAccessException |
|
||||
ClassCastException e) {
|
||||
}
|
||||
settings.setInt((GeckoSessionSettings.Key<Integer>) mKey,
|
||||
Integer.valueOf(value));
|
||||
} else if (String.class.equals(mType)) {
|
||||
settings.setString((GeckoSessionSettings.Key<String>) mKey, value);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Unsupported type: " +
|
||||
mType.getSimpleName());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface List {
|
||||
Setting[] value();
|
||||
}
|
||||
|
||||
Key key();
|
||||
String value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that a method is called or not called, and if called, the order and number
|
||||
* of times it is called. The order number is a monotonically increasing integer; if
|
||||
* an called method's order number is less than the current order number, an exception
|
||||
* is raised for out-of-order call.
|
||||
* <p>
|
||||
* {@code @AssertCalled} asserts the method must be called at least once.
|
||||
* <p>
|
||||
* {@code @AssertCalled(false)} asserts the method must not be called.
|
||||
* <p>
|
||||
* {@code @AssertCalled(order = 2)} asserts the method must be called once and
|
||||
* after any other method with order number less than 2.
|
||||
* <p>
|
||||
* {@code @AssertCalled(order = {2, 4})} asserts order number 2 for first
|
||||
* call and order number 4 for any subsequent calls.
|
||||
* <p>
|
||||
* {@code @AssertCalled(count = 2)} asserts two calls total in any order
|
||||
* with respect to other calls.
|
||||
* <p>
|
||||
* {@code @AssertCalled(count = 2, order = 2)} asserts two calls, both with
|
||||
* order number 2.
|
||||
* <p>
|
||||
* {@code @AssertCalled(count = 2, order = {2, 4, 6})} asserts two calls
|
||||
* total: the first with order number 2 and the second with order number 4.
|
||||
*/
|
||||
@Target(ElementType.METHOD)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface AssertCalled {
|
||||
/**
|
||||
* @return True if the method must be called,
|
||||
* or false if the method must not be called.
|
||||
*/
|
||||
boolean value() default true;
|
||||
|
||||
/**
|
||||
* @return If called, the number of calls called, or 0 to allow any number > 0.
|
||||
*/
|
||||
int count() default 0;
|
||||
|
||||
/**
|
||||
* @return If called, the order number for each call, or 0 to allow arbitrary
|
||||
* order. If order's length is more than count, extra elements are not used;
|
||||
* if order's length is less than count, the last element is repeated.
|
||||
*/
|
||||
int[] order() default 0;
|
||||
}
|
||||
|
||||
public static class CallRequirement {
|
||||
public final boolean allowed;
|
||||
public final int count;
|
||||
public final int[] order;
|
||||
|
||||
public CallRequirement(final boolean allowed, final int count, final int[] order) {
|
||||
this.allowed = allowed;
|
||||
this.count = count;
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CallInfo {
|
||||
public final int counter;
|
||||
public final int order;
|
||||
|
||||
/* package */ CallInfo(final int counter, final int order) {
|
||||
this.counter = counter;
|
||||
this.order = order;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MethodCall {
|
||||
public final Method method;
|
||||
public final CallRequirement requirement;
|
||||
private int currentCount;
|
||||
|
||||
/* package */ MethodCall(final Method method) {
|
||||
this(method, (CallRequirement) null);
|
||||
}
|
||||
|
||||
/* package */ MethodCall(final Method method,
|
||||
final AssertCalled requirement) {
|
||||
this(method, requirement != null ? new CallRequirement(
|
||||
requirement.value(), requirement.count(), requirement.order()) : null);
|
||||
}
|
||||
|
||||
public MethodCall(final Method method,
|
||||
final CallRequirement requirement) {
|
||||
this.method = method;
|
||||
this.requirement = requirement;
|
||||
currentCount = 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
} else if (other instanceof MethodCall) {
|
||||
return methodsEqual(method, ((MethodCall) other).method);
|
||||
} else if (other instanceof Method) {
|
||||
return methodsEqual(method, (Method) other);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* package */ int getOrder() {
|
||||
if (requirement == null || currentCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
final int[] order = requirement.order;
|
||||
if (order == null || order.length == 0) {
|
||||
return 0;
|
||||
}
|
||||
return order[Math.min(currentCount - 1, order.length - 1)];
|
||||
}
|
||||
|
||||
/* package */ int getCount() {
|
||||
return (requirement == null) ? 0 :
|
||||
!requirement.allowed ? -1 : requirement.count;
|
||||
}
|
||||
|
||||
/* package */ void incrementCounter() {
|
||||
currentCount++;
|
||||
}
|
||||
|
||||
/* package */ boolean allowCalls() {
|
||||
return getCount() >= 0;
|
||||
}
|
||||
|
||||
/* package */ boolean allowUnlimitedCalls() {
|
||||
return getCount() == 0;
|
||||
}
|
||||
|
||||
/* package */ boolean allowMoreCalls() {
|
||||
final int count = getCount();
|
||||
return count == 0 || count > currentCount;
|
||||
}
|
||||
|
||||
/* package */ void assertAllowMoreCalls() {
|
||||
final int count = getCount();
|
||||
assertTrue(method.getName() + " should not be called", allowCalls());
|
||||
assertTrue(method.getName() + " should be limited to " + count +
|
||||
" call" + (count > 1 ? "s" : ""), allowMoreCalls());
|
||||
}
|
||||
|
||||
/* package */ void assertOrder(final int order) {
|
||||
final int newOrder = getOrder();
|
||||
if (newOrder != 0) {
|
||||
assertTrue(method.getName() + " order number " + newOrder +
|
||||
" does not match expected order number " + order,
|
||||
newOrder >= order);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ void assertMatchesCount() {
|
||||
if (requirement == null) {
|
||||
return;
|
||||
}
|
||||
final int count = getCount();
|
||||
if (count < 0) {
|
||||
assertEquals(method.getName() + " should not be called",
|
||||
0, currentCount);
|
||||
} else if (count == 0) {
|
||||
assertThat(method.getName() + " should be called", currentCount,
|
||||
is(greaterThan(0)));
|
||||
} else {
|
||||
assertEquals(method.getName() + " should be called " + count + " time" +
|
||||
(count == 1 ? "" : "s"), count, currentCount);
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ CallInfo getInfo() {
|
||||
return new CallInfo(currentCount, getOrder());
|
||||
}
|
||||
|
||||
// Similar to Method.equals, but treat the same method from an interface and an
|
||||
// overriding class as the same (e.g. CharSequence.length == String.length).
|
||||
private static boolean methodsEqual(final @NonNull Method m1, final @NonNull Method m2) {
|
||||
return (m1.getDeclaringClass().isAssignableFrom(m2.getDeclaringClass()) ||
|
||||
m2.getDeclaringClass().isAssignableFrom(m1.getDeclaringClass())) &&
|
||||
m1.getName().equals(m2.getName()) &&
|
||||
m1.getReturnType().equals(m2.getReturnType()) &&
|
||||
Arrays.equals(m1.getParameterTypes(), m2.getParameterTypes());
|
||||
}
|
||||
}
|
||||
|
||||
protected static class CallRecord {
|
||||
public final Method method;
|
||||
public final MethodCall methodCall;
|
||||
public final Object[] args;
|
||||
|
||||
public CallRecord(final Method method, final Object[] args) {
|
||||
this.method = method;
|
||||
this.methodCall = new MethodCall(method);
|
||||
this.args = args;
|
||||
}
|
||||
}
|
||||
|
||||
/* package */ static AssertCalled getAssertCalled(final Method method, final Object callback) {
|
||||
final AssertCalled annotation = method.getAnnotation(AssertCalled.class);
|
||||
if (annotation != null) {
|
||||
return annotation;
|
||||
}
|
||||
|
||||
// Some Kotlin lambdas have an invoke method that carries the annotation,
|
||||
// instead of the interface method carrying the annotation.
|
||||
try {
|
||||
return callback.getClass().getDeclaredMethod(
|
||||
"invoke", method.getParameterTypes()).getAnnotation(AssertCalled.class);
|
||||
} catch (final NoSuchMethodException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void addCallbackClasses(final List<Class<?>> list, final Class<?> ifce) {
|
||||
if (!Callbacks.class.equals(ifce.getDeclaringClass())) {
|
||||
list.add(ifce);
|
||||
return;
|
||||
}
|
||||
final Class<?>[] superIfces = ifce.getInterfaces();
|
||||
for (final Class<?> superIfce : superIfces) {
|
||||
addCallbackClasses(list, superIfce);
|
||||
}
|
||||
}
|
||||
|
||||
private static Class<?>[] getCallbackClasses() {
|
||||
final Class<?>[] ifces = Callbacks.class.getDeclaredClasses();
|
||||
final List<Class<?>> list = new ArrayList<>(ifces.length);
|
||||
|
||||
for (final Class<?> ifce : ifces) {
|
||||
addCallbackClasses(list, ifce);
|
||||
}
|
||||
|
||||
final HashSet<Class<?>> set = new HashSet<>(list);
|
||||
return set.toArray(new Class<?>[set.size()]);
|
||||
}
|
||||
|
||||
private static final List<Class<?>> CALLBACK_CLASSES = Arrays.asList(getCallbackClasses());
|
||||
|
||||
protected final Instrumentation mInstrumentation =
|
||||
InstrumentationRegistry.getInstrumentation();
|
||||
protected final GeckoSessionSettings mDefaultSettings;
|
||||
|
||||
protected GeckoSession mSession;
|
||||
protected Object mCallbackProxy;
|
||||
protected List<CallRecord> mCallRecords;
|
||||
protected int mLastWaitStart;
|
||||
protected int mLastWaitEnd;
|
||||
protected MethodCall mCurrentMethodCall;
|
||||
protected long mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
|
||||
|
||||
public GeckoSessionTestRule() {
|
||||
mDefaultSettings = new GeckoSessionSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the session set up for the current test.
|
||||
*
|
||||
* @return GeckoSession object.
|
||||
*/
|
||||
public @NonNull GeckoSession getSession() {
|
||||
return mSession;
|
||||
}
|
||||
|
||||
protected static Method getCallbackSetter(final @NonNull Class<?> cls)
|
||||
throws NoSuchMethodException {
|
||||
return GeckoSession.class.getMethod("set" + cls.getSimpleName(), cls);
|
||||
}
|
||||
|
||||
protected static Method getCallbackGetter(final @NonNull Class<?> cls)
|
||||
throws NoSuchMethodException {
|
||||
return GeckoSession.class.getMethod("get" + cls.getSimpleName());
|
||||
}
|
||||
|
||||
protected void applyAnnotations(final Collection<Annotation> annotations,
|
||||
final GeckoSessionSettings settings) {
|
||||
for (final Annotation annotation : annotations) {
|
||||
if (TimeoutMillis.class.equals(annotation.annotationType())) {
|
||||
mTimeoutMillis = Math.max(((TimeoutMillis) annotation).value(), 100);
|
||||
} else if (Setting.List.class.equals(annotation.annotationType())) {
|
||||
for (final Setting setting : ((Setting.List) annotation).value()) {
|
||||
setting.key().set(settings, setting.value());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void prepareSession(final Description description) throws Throwable {
|
||||
final GeckoSessionSettings settings = new GeckoSessionSettings(mDefaultSettings);
|
||||
|
||||
applyAnnotations(Arrays.asList(description.getTestClass().getAnnotations()), settings);
|
||||
applyAnnotations(description.getAnnotations(), settings);
|
||||
|
||||
final List<CallRecord> records = new ArrayList<>();
|
||||
mCallRecords = records;
|
||||
mLastWaitStart = 0;
|
||||
mLastWaitEnd = 0;
|
||||
|
||||
final InvocationHandler recorder = new InvocationHandler() {
|
||||
@Override
|
||||
public Object invoke(final Object proxy, final Method method,
|
||||
final Object[] args) {
|
||||
assertEquals("Callbacks must be on UI thread",
|
||||
Looper.getMainLooper(), Looper.myLooper());
|
||||
|
||||
records.add(new CallRecord(method, args));
|
||||
|
||||
try {
|
||||
return method.invoke(Callbacks.Default.INSTANCE, args);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
final Class<?>[] classes = CALLBACK_CLASSES.toArray(new Class<?>[CALLBACK_CLASSES.size()]);
|
||||
mCallbackProxy = Proxy.newProxyInstance(GeckoSession.class.getClassLoader(),
|
||||
classes, recorder);
|
||||
|
||||
mSession = new GeckoSession(settings);
|
||||
|
||||
for (final Class<?> cls : CALLBACK_CLASSES) {
|
||||
if (cls != null) {
|
||||
getCallbackSetter(cls).invoke(mSession, mCallbackProxy);
|
||||
}
|
||||
}
|
||||
|
||||
mSession.openWindow(mInstrumentation.getTargetContext());
|
||||
|
||||
if (settings.getBoolean(GeckoSessionSettings.USE_MULTIPROCESS)) {
|
||||
// Under e10s, we receive an initial about:blank load; don't expose that to the test.
|
||||
waitForPageStop();
|
||||
}
|
||||
}
|
||||
|
||||
protected void cleanupSession() throws Throwable {
|
||||
if (mSession.isOpen()) {
|
||||
mSession.closeWindow();
|
||||
}
|
||||
mSession = null;
|
||||
mCallbackProxy = null;
|
||||
mCallRecords = null;
|
||||
mLastWaitStart = 0;
|
||||
mLastWaitEnd = 0;
|
||||
mTimeoutMillis = DEFAULT_TIMEOUT_MILLIS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Statement apply(final Statement base, final Description description) {
|
||||
return super.apply(new Statement() {
|
||||
@Override
|
||||
public void evaluate() throws Throwable {
|
||||
try {
|
||||
prepareSession(description);
|
||||
base.evaluate();
|
||||
} finally {
|
||||
cleanupSession();
|
||||
}
|
||||
}
|
||||
}, description);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldRunOnUiThread(final Description description) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loop the current thread until the message queue is idle. If loop is already idle and
|
||||
* timeout is not specified, return immediately. If loop is already idle and timeout is
|
||||
* specified, wait for a message to arrive first; an exception is thrown if timeout
|
||||
* expires during the wait.
|
||||
*
|
||||
* @param timeout Wait timeout in milliseconds or 0 to not wait.
|
||||
*/
|
||||
protected static void loopUntilIdle(final long timeout) {
|
||||
// Adapted from GeckoThread.pumpMessageLoop.
|
||||
final Looper looper = Looper.myLooper();
|
||||
assertNotNull("Looper must exist", looper);
|
||||
|
||||
final MessageQueue queue = looper.getQueue();
|
||||
final Handler handler = new Handler(looper);
|
||||
final MessageQueue.IdleHandler idleHandler = new MessageQueue.IdleHandler() {
|
||||
@Override
|
||||
public boolean queueIdle() {
|
||||
final Message msg = Message.obtain(handler);
|
||||
msg.obj = handler;
|
||||
handler.sendMessageAtFrontOfQueue(msg);
|
||||
return false; // Remove this idle handler.
|
||||
}
|
||||
};
|
||||
final Runnable timeoutRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
fail("Timed out after " + timeout + "ms");
|
||||
}
|
||||
};
|
||||
|
||||
if (timeout > 0) {
|
||||
handler.postDelayed(timeoutRunnable, timeout);
|
||||
} else {
|
||||
queue.addIdleHandler(idleHandler);
|
||||
}
|
||||
|
||||
final Method getNextMessage;
|
||||
try {
|
||||
getNextMessage = queue.getClass().getDeclaredMethod("next");
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
getNextMessage.setAccessible(true);
|
||||
|
||||
while (true) {
|
||||
final Message msg;
|
||||
try {
|
||||
msg = (Message) getNextMessage.invoke(queue);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
if (msg.getTarget() == handler && msg.obj == handler) {
|
||||
// Our idle signal.
|
||||
break;
|
||||
} else if (msg.getTarget() == null) {
|
||||
looper.quit();
|
||||
break;
|
||||
}
|
||||
msg.getTarget().dispatchMessage(msg);
|
||||
|
||||
if (timeout > 0) {
|
||||
handler.removeCallbacks(timeoutRunnable);
|
||||
queue.addIdleHandler(idleHandler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until a page load has finished. The session must have started a page load since
|
||||
* the last wait, or this method will wait indefinitely.
|
||||
*/
|
||||
public void waitForPageStop() {
|
||||
waitForPageStops(/* count */ 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until a page load has finished. The session must have started a page load since
|
||||
* the last wait, or this method will wait indefinitely.
|
||||
*
|
||||
* @param count Number of page loads to wait for.
|
||||
*/
|
||||
public void waitForPageStops(final int count) {
|
||||
final Method onPageStop;
|
||||
try {
|
||||
onPageStop = GeckoSession.ProgressListener.class.getMethod(
|
||||
"onPageStop", GeckoSession.class, boolean.class);
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
final List<MethodCall> methodCalls = new ArrayList<>(1);
|
||||
methodCalls.add(new MethodCall(onPageStop,
|
||||
new CallRequirement(/* allowed */ true, count, null)));
|
||||
|
||||
waitUntilCalled(GeckoSession.ProgressListener.class, methodCalls);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the specified methods have been called on the specified callback
|
||||
* interface. If no methods are specified, wait until any method has been called.
|
||||
*
|
||||
* @param callback Target callback interface; must be an interface under GeckoSession.
|
||||
* @param methods List of methods to wait on; use empty or null or wait on any method.
|
||||
*/
|
||||
public void waitUntilCalled(final @NonNull Class<?> callback,
|
||||
final @Nullable String... methods) {
|
||||
assertTrue("Class should be a GeckoSession interface",
|
||||
CALLBACK_CLASSES.contains(callback));
|
||||
|
||||
final int length = (methods != null) ? methods.length : 0;
|
||||
final Pattern[] patterns = new Pattern[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
patterns[i] = Pattern.compile(methods[i]);
|
||||
}
|
||||
|
||||
final List<MethodCall> waitMethods = new ArrayList<>();
|
||||
|
||||
for (final Method method : callback.getDeclaredMethods()) {
|
||||
for (final Pattern pattern : patterns) {
|
||||
if (pattern.matcher(method.getName()).matches()) {
|
||||
waitMethods.add(new MethodCall(method));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitUntilCalled(callback, waitMethods);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until the specified methods have been called on the specified object,
|
||||
* as specified by any {@link AssertCalled @AssertCalled} annotations. If no
|
||||
* {@link AssertCalled @AssertCalled} annotations are found, wait until any method
|
||||
* has been called. Only methods belonging to a GeckoSession callback are supported.
|
||||
*
|
||||
* @param callback Target callback object; must implement an interface under GeckoSession.
|
||||
*/
|
||||
public void waitUntilCalled(final @NonNull Object callback) {
|
||||
if (callback instanceof Class<?>) {
|
||||
waitUntilCalled((Class<?>) callback, (String[]) null);
|
||||
return;
|
||||
}
|
||||
|
||||
final List<MethodCall> methodCalls = new ArrayList<>();
|
||||
for (final Class<?> ifce : CALLBACK_CLASSES) {
|
||||
if (!ifce.isInstance(callback)) {
|
||||
continue;
|
||||
}
|
||||
for (final Method method : ifce.getMethods()) {
|
||||
final Method callbackMethod;
|
||||
try {
|
||||
callbackMethod = callback.getClass().getMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
final AssertCalled ac = getAssertCalled(callbackMethod, callback);
|
||||
if (ac != null && ac.value()) {
|
||||
methodCalls.add(new MethodCall(callbackMethod, ac));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
waitUntilCalled(callback.getClass(), methodCalls);
|
||||
forCallbacksDuringWait(callback);
|
||||
}
|
||||
|
||||
protected void waitUntilCalled(final @NonNull Class<?> listener,
|
||||
final @NonNull List<MethodCall> methodCalls) {
|
||||
// Make sure all handlers are set though #delegateUntilTestEnd or #delegateDuringNextWait,
|
||||
// instead of through GeckoSession directly, so that we can still record calls even with
|
||||
// custom handlers set.
|
||||
for (final Class<?> ifce : CALLBACK_CLASSES) {
|
||||
try {
|
||||
assertSame("Callbacks should be set through" +
|
||||
" GeckoSessionTestRule delegate methods",
|
||||
mCallbackProxy, getCallbackGetter(ifce).invoke(mSession));
|
||||
} catch (final NoSuchMethodException | IllegalAccessException |
|
||||
InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
}
|
||||
}
|
||||
|
||||
boolean calledAny = false;
|
||||
int index = mLastWaitStart = mLastWaitEnd;
|
||||
|
||||
while (!calledAny || !methodCalls.isEmpty()) {
|
||||
while (index >= mCallRecords.size()) {
|
||||
loopUntilIdle(mTimeoutMillis);
|
||||
}
|
||||
|
||||
final MethodCall recorded = mCallRecords.get(index).methodCall;
|
||||
calledAny |= recorded.method.getDeclaringClass().isAssignableFrom(listener);
|
||||
index++;
|
||||
|
||||
final int i = methodCalls.indexOf(recorded);
|
||||
if (i < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final MethodCall methodCall = methodCalls.get(i);
|
||||
methodCall.incrementCounter();
|
||||
if (methodCall.allowUnlimitedCalls() || !methodCall.allowMoreCalls()) {
|
||||
methodCalls.remove(i);
|
||||
}
|
||||
}
|
||||
|
||||
mLastWaitEnd = index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Playback callbacks that were made during the previous wait. For any methods
|
||||
* annotated with {@link AssertCalled @AssertCalled}, assert that the callbacks
|
||||
* satisfy the specified requirements. If no {@link AssertCalled @AssertCalled}
|
||||
* annotations are found, assert any method has been called. Only methods belonging
|
||||
* to a GeckoSession callback are supported.
|
||||
*
|
||||
* @param callback Target callback object; must implement one or more interfaces
|
||||
* under GeckoSession.
|
||||
*/
|
||||
public void forCallbacksDuringWait(final @NonNull Object callback) {
|
||||
final Method[] declaredMethods = callback.getClass().getDeclaredMethods();
|
||||
final List<MethodCall> methodCalls = new ArrayList<>(declaredMethods.length);
|
||||
for (final Class<?> ifce : CALLBACK_CLASSES) {
|
||||
if (!ifce.isInstance(callback)) {
|
||||
continue;
|
||||
}
|
||||
for (final Method method : ifce.getMethods()) {
|
||||
final Method callbackMethod;
|
||||
try {
|
||||
callbackMethod = callback.getClass().getMethod(method.getName(),
|
||||
method.getParameterTypes());
|
||||
} catch (final NoSuchMethodException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
methodCalls.add(new MethodCall(
|
||||
callbackMethod, getAssertCalled(callbackMethod, callback)));
|
||||
}
|
||||
}
|
||||
|
||||
int order = 0;
|
||||
boolean calledAny = false;
|
||||
|
||||
for (int index = mLastWaitStart; index < mLastWaitEnd; index++) {
|
||||
final CallRecord record = mCallRecords.get(index);
|
||||
if (!record.method.getDeclaringClass().isInstance(callback)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int i = methodCalls.indexOf(record.methodCall);
|
||||
assertTrue(record.method.getName() + " should be found", i >= 0);
|
||||
|
||||
final MethodCall methodCall = methodCalls.get(i);
|
||||
methodCall.assertAllowMoreCalls();
|
||||
methodCall.incrementCounter();
|
||||
methodCall.assertOrder(order);
|
||||
order = Math.max(methodCall.getOrder(), order);
|
||||
|
||||
try {
|
||||
mCurrentMethodCall = methodCall;
|
||||
record.method.invoke(callback, record.args);
|
||||
} catch (final IllegalAccessException | InvocationTargetException e) {
|
||||
throw new RuntimeException(e.getCause() != null ? e.getCause() : e);
|
||||
} finally {
|
||||
mCurrentMethodCall = null;
|
||||
}
|
||||
calledAny = true;
|
||||
}
|
||||
|
||||
for (final MethodCall methodCall : methodCalls) {
|
||||
methodCall.assertMatchesCount();
|
||||
if (methodCall.requirement != null) {
|
||||
calledAny = true;
|
||||
}
|
||||
}
|
||||
|
||||
assertTrue("Should have called one of " +
|
||||
Arrays.toString(callback.getClass().getInterfaces()), calledAny);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get information about the current call. Only valid during a {@link #forCallbacksDuringWait}
|
||||
* callback.
|
||||
*
|
||||
* @return Call information
|
||||
*/
|
||||
public @NonNull CallInfo getCurrentCall() {
|
||||
assertNotNull("Should be in a method call", mCurrentMethodCall);
|
||||
return mCurrentMethodCall.getInfo();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,121 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
package org.mozilla.geckoview.test.util
|
||||
|
||||
import org.mozilla.gecko.GeckoSession
|
||||
import org.mozilla.gecko.util.GeckoBundle
|
||||
|
||||
class Callbacks private constructor() {
|
||||
object Default : All {
|
||||
}
|
||||
|
||||
interface All : ContentListener, NavigationListener, PermissionDelegate, ProgressListener,
|
||||
PromptDelegate, ScrollListener, TrackingProtectionDelegate {
|
||||
}
|
||||
|
||||
interface ContentListener : GeckoSession.ContentListener {
|
||||
override fun onTitleChange(session: GeckoSession, title: String) {
|
||||
}
|
||||
|
||||
override fun onFocusRequest(session: GeckoSession) {
|
||||
}
|
||||
|
||||
override fun onCloseRequest(session: GeckoSession) {
|
||||
}
|
||||
|
||||
override fun onFullScreen(session: GeckoSession, fullScreen: Boolean) {
|
||||
}
|
||||
|
||||
override fun onContextMenu(session: GeckoSession, screenX: Int, screenY: Int, uri: String, elementSrc: String) {
|
||||
}
|
||||
}
|
||||
|
||||
interface NavigationListener : GeckoSession.NavigationListener {
|
||||
override fun onLocationChange(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
override fun onCanGoBack(session: GeckoSession, canGoBack: Boolean) {
|
||||
}
|
||||
|
||||
override fun onCanGoForward(session: GeckoSession, canGoForward: Boolean) {
|
||||
}
|
||||
|
||||
override fun onLoadUri(session: GeckoSession, uri: String, where: GeckoSession.NavigationListener.TargetWindow): Boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
override fun onNewSession(session: GeckoSession, uri: String, response: GeckoSession.Response<GeckoSession>) {
|
||||
response.respond(null)
|
||||
}
|
||||
}
|
||||
|
||||
interface PermissionDelegate : GeckoSession.PermissionDelegate {
|
||||
override fun requestAndroidPermissions(session: GeckoSession, permissions: Array<out String>, callback: GeckoSession.PermissionDelegate.Callback) {
|
||||
callback.reject()
|
||||
}
|
||||
|
||||
override fun requestContentPermission(session: GeckoSession, uri: String, type: String, access: String, callback: GeckoSession.PermissionDelegate.Callback) {
|
||||
callback.reject()
|
||||
}
|
||||
|
||||
override fun requestMediaPermission(session: GeckoSession, uri: String, video: Array<out GeckoSession.PermissionDelegate.MediaSource>, audio: Array<out GeckoSession.PermissionDelegate.MediaSource>, callback: GeckoSession.PermissionDelegate.MediaCallback) {
|
||||
callback.reject()
|
||||
}
|
||||
}
|
||||
|
||||
interface ProgressListener : GeckoSession.ProgressListener {
|
||||
override fun onPageStart(session: GeckoSession, url: String) {
|
||||
}
|
||||
|
||||
override fun onPageStop(session: GeckoSession, success: Boolean) {
|
||||
}
|
||||
|
||||
override fun onSecurityChange(session: GeckoSession, securityInfo: GeckoSession.ProgressListener.SecurityInformation) {
|
||||
}
|
||||
}
|
||||
|
||||
interface PromptDelegate : GeckoSession.PromptDelegate {
|
||||
override fun alert(session: GeckoSession, title: String, msg: String, callback: GeckoSession.PromptDelegate.AlertCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForButton(session: GeckoSession, title: String, msg: String, btnMsg: Array<out String>, callback: GeckoSession.PromptDelegate.ButtonCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForText(session: GeckoSession, title: String, msg: String, value: String, callback: GeckoSession.PromptDelegate.TextCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForAuth(session: GeckoSession, title: String, msg: String, options: GeckoSession.PromptDelegate.AuthenticationOptions, callback: GeckoSession.PromptDelegate.AuthCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForChoice(session: GeckoSession, title: String, msg: String, type: Int, choices: Array<out GeckoSession.PromptDelegate.Choice>, callback: GeckoSession.PromptDelegate.ChoiceCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForColor(session: GeckoSession, title: String, value: String, callback: GeckoSession.PromptDelegate.TextCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForDateTime(session: GeckoSession, title: String, type: Int, value: String, min: String, max: String, callback: GeckoSession.PromptDelegate.TextCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
|
||||
override fun promptForFile(session: GeckoSession, title: String, type: Int, mimeTypes: Array<out String>, callback: GeckoSession.PromptDelegate.FileCallback) {
|
||||
callback.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
interface ScrollListener : GeckoSession.ScrollListener {
|
||||
override fun onScrollChanged(session: GeckoSession, scrollX: Int, scrollY: Int) {
|
||||
}
|
||||
}
|
||||
|
||||
interface TrackingProtectionDelegate : GeckoSession.TrackingProtectionDelegate {
|
||||
override fun onTrackerBlocked(session: GeckoSession, uri: String, categories: Int) {
|
||||
}
|
||||
}
|
||||
}
|
|
@ -459,12 +459,7 @@ public class GeckoSession extends LayerSession
|
|||
}
|
||||
|
||||
public GeckoSession(final GeckoSessionSettings settings) {
|
||||
if (settings == null) {
|
||||
mSettings = new GeckoSessionSettings(this);
|
||||
} else {
|
||||
mSettings = new GeckoSessionSettings(settings, this);
|
||||
}
|
||||
|
||||
mSettings = new GeckoSessionSettings(settings, this);
|
||||
mListener.registerListeners();
|
||||
}
|
||||
|
||||
|
@ -788,6 +783,10 @@ public class GeckoSession extends LayerSession
|
|||
mScrollHandler.setListener(listener, this);
|
||||
}
|
||||
|
||||
public ScrollListener getScrollListener() {
|
||||
return mScrollHandler.getListener();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the tracking protection callback handler.
|
||||
* This will replace the current handler.
|
||||
|
|
|
@ -10,6 +10,8 @@ import org.mozilla.gecko.util.GeckoBundle;
|
|||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.Log;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
@ -25,16 +27,17 @@ public final class GeckoSessionSettings implements Parcelable {
|
|||
public static final int DISPLAY_MODE_STANDALONE = 2;
|
||||
public static final int DISPLAY_MODE_FULLSCREEN = 3;
|
||||
|
||||
private static class Key<T> {
|
||||
public final String name;
|
||||
public final boolean initOnly;
|
||||
public final Collection<T> values;
|
||||
public static class Key<T> {
|
||||
/* package */ final String name;
|
||||
/* package */ final boolean initOnly;
|
||||
/* package */ final Collection<T> values;
|
||||
|
||||
public Key(final String name) {
|
||||
/* package */ Key(final String name) {
|
||||
this(name, /* initOnly */ false, /* values */ null);
|
||||
}
|
||||
|
||||
public Key(final String name, final boolean initOnly, final Collection<T> values) {
|
||||
/* package */ Key(final String name, final boolean initOnly,
|
||||
final Collection<T> values) {
|
||||
this.name = name;
|
||||
this.initOnly = initOnly;
|
||||
this.values = values;
|
||||
|
@ -88,13 +91,23 @@ public final class GeckoSessionSettings implements Parcelable {
|
|||
private final GeckoBundle mBundle;
|
||||
|
||||
public GeckoSessionSettings() {
|
||||
this(null);
|
||||
this(null, null);
|
||||
}
|
||||
|
||||
/* package */ GeckoSessionSettings(final GeckoSession session) {
|
||||
mSession = session;
|
||||
mBundle = new GeckoBundle();
|
||||
public GeckoSessionSettings(final @NonNull GeckoSessionSettings settings) {
|
||||
this(settings, null);
|
||||
}
|
||||
|
||||
/* package */ GeckoSessionSettings(final @Nullable GeckoSessionSettings settings,
|
||||
final @Nullable GeckoSession session) {
|
||||
mSession = session;
|
||||
|
||||
if (settings != null) {
|
||||
mBundle = new GeckoBundle(settings.mBundle);
|
||||
return;
|
||||
}
|
||||
|
||||
mBundle = new GeckoBundle();
|
||||
mBundle.putString(CHROME_URI.name, null);
|
||||
mBundle.putInt(SCREEN_ID.name, 0);
|
||||
mBundle.putBoolean(USE_TRACKING_PROTECTION.name, false);
|
||||
|
@ -104,12 +117,6 @@ public final class GeckoSessionSettings implements Parcelable {
|
|||
mBundle.putBoolean(USE_REMOTE_DEBUGGER.name, false);
|
||||
}
|
||||
|
||||
/* package */ GeckoSessionSettings(final GeckoSessionSettings settings,
|
||||
final GeckoSession session) {
|
||||
mSession = session;
|
||||
mBundle = new GeckoBundle(settings.mBundle);
|
||||
}
|
||||
|
||||
public void setBoolean(final Key<Boolean> key, final boolean value) {
|
||||
synchronized (mBundle) {
|
||||
if (valueChangedLocked(key, value)) {
|
||||
|
|
|
@ -651,7 +651,7 @@ impl HTMLFormElement {
|
|||
child.downcast::<HTMLSelectElement>().unwrap().reset();
|
||||
}
|
||||
NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLTextAreaElement)) => {
|
||||
child.downcast::<HTMLTextAreaElement>().unwrap().reset(true);
|
||||
child.downcast::<HTMLTextAreaElement>().unwrap().reset();
|
||||
}
|
||||
NodeTypeId::Element(ElementTypeId::HTMLElement(HTMLElementTypeId::HTMLOutputElement)) => {
|
||||
// Unimplemented
|
||||
|
|
|
@ -550,8 +550,43 @@ impl HTMLInputElementMethods for HTMLInputElement {
|
|||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-input-value
|
||||
fn SetValue(&self, value: DOMString) -> ErrorResult {
|
||||
self.update_text_contents(value, true)
|
||||
fn SetValue(&self, mut value: DOMString) -> ErrorResult {
|
||||
match self.value_mode() {
|
||||
ValueMode::Value => {
|
||||
// Step 3.
|
||||
self.value_dirty.set(true);
|
||||
|
||||
// Step 4.
|
||||
self.sanitize_value(&mut value);
|
||||
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
|
||||
// Step 5.
|
||||
if *textinput.single_line_content() != value {
|
||||
// Steps 1-2
|
||||
textinput.set_content(value);
|
||||
|
||||
// Step 5.
|
||||
textinput.clear_selection_to_limit(Direction::Forward);
|
||||
}
|
||||
}
|
||||
ValueMode::Default |
|
||||
ValueMode::DefaultOn => {
|
||||
self.upcast::<Element>().set_string_attribute(&local_name!("value"), value);
|
||||
}
|
||||
ValueMode::Filename => {
|
||||
if value.is_empty() {
|
||||
let window = window_from_node(self);
|
||||
let fl = FileList::new(&window, vec![]);
|
||||
self.filelist.set(Some(&fl));
|
||||
} else {
|
||||
return Err(Error::InvalidState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-input-defaultvalue
|
||||
|
@ -906,9 +941,7 @@ impl HTMLInputElement {
|
|||
InputType::Image => (),
|
||||
_ => ()
|
||||
}
|
||||
|
||||
self.update_text_contents(self.DefaultValue(), true)
|
||||
.expect("Failed to reset input value to default.");
|
||||
self.textinput.borrow_mut().set_content(self.DefaultValue());
|
||||
self.value_dirty.set(false);
|
||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||
}
|
||||
|
@ -1056,44 +1089,6 @@ impl HTMLInputElement {
|
|||
fn selection(&self) -> TextControlSelection<Self> {
|
||||
TextControlSelection::new(&self, &self.textinput)
|
||||
}
|
||||
|
||||
fn update_text_contents(&self, mut value: DOMString, update_text_cursor: bool) -> ErrorResult {
|
||||
match self.value_mode() {
|
||||
ValueMode::Value => {
|
||||
// Step 3.
|
||||
self.value_dirty.set(true);
|
||||
|
||||
// Step 4.
|
||||
self.sanitize_value(&mut value);
|
||||
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
|
||||
if *textinput.single_line_content() != value {
|
||||
// Steps 1-2
|
||||
textinput.set_content(value, update_text_cursor);
|
||||
|
||||
// Step 5.
|
||||
textinput.clear_selection_to_limit(Direction::Forward, update_text_cursor);
|
||||
}
|
||||
}
|
||||
ValueMode::Default |
|
||||
ValueMode::DefaultOn => {
|
||||
self.upcast::<Element>().set_string_attribute(&local_name!("value"), value);
|
||||
}
|
||||
ValueMode::Filename => {
|
||||
if value.is_empty() {
|
||||
let window = window_from_node(self);
|
||||
let fl = FileList::new(&window, vec![]);
|
||||
self.filelist.set(Some(&fl));
|
||||
} else {
|
||||
return Err(Error::InvalidState);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualMethods for HTMLInputElement {
|
||||
|
@ -1203,11 +1198,11 @@ impl VirtualMethods for HTMLInputElement {
|
|||
let mut textinput = self.textinput.borrow_mut();
|
||||
let mut value = textinput.single_line_content().clone();
|
||||
self.sanitize_value(&mut value);
|
||||
textinput.set_content(value, true);
|
||||
textinput.set_content(value);
|
||||
|
||||
// Steps 7-9
|
||||
if !previously_selectable && self.selection_api_applies() {
|
||||
textinput.clear_selection_to_limit(Direction::Backward, true);
|
||||
textinput.clear_selection_to_limit(Direction::Backward);
|
||||
}
|
||||
},
|
||||
AttributeMutation::Removed => {
|
||||
|
@ -1231,7 +1226,7 @@ impl VirtualMethods for HTMLInputElement {
|
|||
let mut value = value.map_or(DOMString::new(), DOMString::from);
|
||||
|
||||
self.sanitize_value(&mut value);
|
||||
self.textinput.borrow_mut().set_content(value, true);
|
||||
self.textinput.borrow_mut().set_content(value);
|
||||
self.update_placeholder_shown_state();
|
||||
},
|
||||
&local_name!("name") if self.input_type() == InputType::Radio => {
|
||||
|
|
|
@ -233,7 +233,7 @@ impl HTMLTextAreaElementMethods for HTMLTextAreaElement {
|
|||
// if the element's dirty value flag is false, then the element's
|
||||
// raw value must be set to the value of the element's textContent IDL attribute
|
||||
if !self.value_dirty.get() {
|
||||
self.reset(false);
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,7 +244,23 @@ impl HTMLTextAreaElementMethods for HTMLTextAreaElement {
|
|||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-textarea-value
|
||||
fn SetValue(&self, value: DOMString) {
|
||||
self.update_text_contents(value, true);
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
|
||||
// Step 1
|
||||
let old_value = textinput.get_content();
|
||||
|
||||
// Step 2
|
||||
textinput.set_content(value);
|
||||
|
||||
// Step 3
|
||||
self.value_dirty.set(true);
|
||||
|
||||
if old_value != textinput.get_content() {
|
||||
// Step 4
|
||||
textinput.clear_selection_to_limit(Direction::Forward);
|
||||
}
|
||||
|
||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#dom-lfe-labels
|
||||
|
@ -306,9 +322,10 @@ impl HTMLTextAreaElementMethods for HTMLTextAreaElement {
|
|||
|
||||
|
||||
impl HTMLTextAreaElement {
|
||||
pub fn reset(&self, update_text_cursor: bool) {
|
||||
pub fn reset(&self) {
|
||||
// https://html.spec.whatwg.org/multipage/#the-textarea-element:concept-form-reset-control
|
||||
self.update_text_contents(self.DefaultValue(), update_text_cursor);
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
textinput.set_content(self.DefaultValue());
|
||||
self.value_dirty.set(false);
|
||||
}
|
||||
|
||||
|
@ -316,27 +333,6 @@ impl HTMLTextAreaElement {
|
|||
fn selection(&self) -> TextControlSelection<Self> {
|
||||
TextControlSelection::new(&self, &self.textinput)
|
||||
}
|
||||
|
||||
// Helper function to check if text_cursor is to be updated or not
|
||||
fn update_text_contents(&self, value: DOMString, update_text_cursor: bool) {
|
||||
let mut textinput = self.textinput.borrow_mut();
|
||||
|
||||
// Step 1
|
||||
let old_value = textinput.get_content();
|
||||
|
||||
// Step 2
|
||||
textinput.set_content(value, update_text_cursor);
|
||||
|
||||
// Step 3
|
||||
self.value_dirty.set(true);
|
||||
|
||||
if old_value != textinput.get_content() {
|
||||
// Step 4
|
||||
textinput.clear_selection_to_limit(Direction::Forward, update_text_cursor);
|
||||
}
|
||||
|
||||
self.upcast::<Node>().dirty(NodeDamage::OtherNodeDamage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -429,7 +425,7 @@ impl VirtualMethods for HTMLTextAreaElement {
|
|||
s.children_changed(mutation);
|
||||
}
|
||||
if !self.value_dirty.get() {
|
||||
self.reset(false);
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -480,7 +476,7 @@ impl VirtualMethods for HTMLTextAreaElement {
|
|||
self.super_type().unwrap().pop();
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/#the-textarea-element:stack-of-open-elements
|
||||
self.reset(false);
|
||||
self.reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -188,7 +188,7 @@ impl<T: ClipboardProvider> TextInput<T> {
|
|||
min_length: min_length,
|
||||
selection_direction: selection_direction,
|
||||
};
|
||||
i.set_content(initial, false);
|
||||
i.set_content(initial);
|
||||
i
|
||||
}
|
||||
|
||||
|
@ -568,9 +568,9 @@ impl<T: ClipboardProvider> TextInput<T> {
|
|||
}
|
||||
|
||||
/// Remove the current selection and set the edit point to the end of the content.
|
||||
pub fn clear_selection_to_limit(&mut self, direction: Direction, update_text_cursor: bool) {
|
||||
pub fn clear_selection_to_limit(&mut self, direction: Direction) {
|
||||
self.clear_selection();
|
||||
self.adjust_horizontal_to_limit(direction, Selection::NotSelected, update_text_cursor);
|
||||
self.adjust_horizontal_to_limit(direction, Selection::NotSelected);
|
||||
}
|
||||
|
||||
pub fn adjust_horizontal_by_word(&mut self, direction: Direction, select: Selection) {
|
||||
|
@ -662,20 +662,18 @@ impl<T: ClipboardProvider> TextInput<T> {
|
|||
self.perform_horizontal_adjustment(shift, select);
|
||||
}
|
||||
|
||||
pub fn adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection, update_text_cursor: bool) {
|
||||
pub fn adjust_horizontal_to_limit(&mut self, direction: Direction, select: Selection) {
|
||||
if self.adjust_selection_for_horizontal_change(direction, select) {
|
||||
return
|
||||
}
|
||||
if update_text_cursor {
|
||||
match direction {
|
||||
Direction::Backward => {
|
||||
self.edit_point.line = 0;
|
||||
self.edit_point.index = 0;
|
||||
},
|
||||
Direction::Forward => {
|
||||
self.edit_point.line = &self.lines.len() - 1;
|
||||
self.edit_point.index = (&self.lines[&self.lines.len() - 1]).len();
|
||||
}
|
||||
match direction {
|
||||
Direction::Backward => {
|
||||
self.edit_point.line = 0;
|
||||
self.edit_point.index = 0;
|
||||
},
|
||||
Direction::Forward => {
|
||||
self.edit_point.line = &self.lines.len() - 1;
|
||||
self.edit_point.index = (&self.lines[&self.lines.len() - 1]).len();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -765,12 +763,12 @@ impl<T: ClipboardProvider> TextInput<T> {
|
|||
},
|
||||
#[cfg(target_os = "macos")]
|
||||
(None, Key::Up) if mods.contains(KeyModifiers::SUPER) => {
|
||||
self.adjust_horizontal_to_limit(Direction::Backward, maybe_select, true);
|
||||
self.adjust_horizontal_to_limit(Direction::Backward, maybe_select);
|
||||
KeyReaction::RedrawSelection
|
||||
},
|
||||
#[cfg(target_os = "macos")]
|
||||
(None, Key::Down) if mods.contains(KeyModifiers::SUPER) => {
|
||||
self.adjust_horizontal_to_limit(Direction::Forward, maybe_select, true);
|
||||
self.adjust_horizontal_to_limit(Direction::Forward, maybe_select);
|
||||
KeyReaction::RedrawSelection
|
||||
},
|
||||
(None, Key::Left) if mods.contains(KeyModifiers::ALT) => {
|
||||
|
@ -871,7 +869,7 @@ impl<T: ClipboardProvider> TextInput<T> {
|
|||
|
||||
/// Set the current contents of the text input. If this is control supports multiple lines,
|
||||
/// any \n encountered will be stripped and force a new logical line.
|
||||
pub fn set_content(&mut self, content: DOMString, update_text_cursor: bool) {
|
||||
pub fn set_content(&mut self, content: DOMString) {
|
||||
self.lines = if self.multiline {
|
||||
// https://html.spec.whatwg.org/multipage/#textarea-line-break-normalisation-transformation
|
||||
content.replace("\r\n", "\n")
|
||||
|
@ -882,14 +880,11 @@ impl<T: ClipboardProvider> TextInput<T> {
|
|||
vec!(content)
|
||||
};
|
||||
|
||||
if update_text_cursor {
|
||||
self.edit_point = self.edit_point.constrain_to(&self.lines);
|
||||
}
|
||||
self.edit_point = self.edit_point.constrain_to(&self.lines);
|
||||
|
||||
if let Some(origin) = self.selection_origin {
|
||||
self.selection_origin = Some(origin.constrain_to(&self.lines));
|
||||
}
|
||||
|
||||
self.assert_ok_selection();
|
||||
}
|
||||
|
||||
|
|
|
@ -30,10 +30,15 @@ python components/style/properties/build.py servo html regular
|
|||
cd components/script
|
||||
cmake .
|
||||
cmake --build . --target supported-apis
|
||||
echo "Copying apis.html."
|
||||
cp apis.html ../../target/doc/servo/
|
||||
echo "Copied apis.html."
|
||||
cd ../..
|
||||
|
||||
echo "Starting ghp-import."
|
||||
ghp-import -n target/doc
|
||||
echo "Finished ghp-import."
|
||||
git push -qf \
|
||||
"https://${TOKEN}@github.com/servo/doc.servo.org.git" gh-pages \
|
||||
&>/dev/null
|
||||
echo "Finished git push."
|
||||
|
|
|
@ -27,7 +27,7 @@ fn test_set_content_ignores_max_length() {
|
|||
Lines::Single, DOMString::from(""), DummyClipboardContext::new(""), Some(1), None, SelectionDirection::None
|
||||
);
|
||||
|
||||
textinput.set_content(DOMString::from("mozilla rocks"), true);
|
||||
textinput.set_content(DOMString::from("mozilla rocks"));
|
||||
assert_eq!(textinput.get_content(), DOMString::from("mozilla rocks"));
|
||||
}
|
||||
|
||||
|
@ -489,15 +489,16 @@ fn test_textinput_set_content() {
|
|||
let mut textinput = text_input(Lines::Multiple, "abc\nde\nf");
|
||||
assert_eq!(textinput.get_content(), "abc\nde\nf");
|
||||
|
||||
textinput.set_content(DOMString::from("abc\nf"), true);
|
||||
textinput.set_content(DOMString::from("abc\nf"));
|
||||
assert_eq!(textinput.get_content(), "abc\nf");
|
||||
|
||||
assert_eq!(textinput.edit_point().line, 0);
|
||||
assert_eq!(textinput.edit_point().index, 0);
|
||||
|
||||
textinput.adjust_horizontal(3, Selection::Selected);
|
||||
assert_eq!(textinput.edit_point().line, 0);
|
||||
assert_eq!(textinput.edit_point().index, 3);
|
||||
textinput.set_content(DOMString::from("de"), true);
|
||||
textinput.set_content(DOMString::from("de"));
|
||||
assert_eq!(textinput.get_content(), "de");
|
||||
assert_eq!(textinput.edit_point().line, 0);
|
||||
assert_eq!(textinput.edit_point().index, 2);
|
||||
|
|
|
@ -3758,7 +3758,9 @@
|
|||
"n_buckets": 50
|
||||
},
|
||||
"PREDICTOR_PREDICT_ATTEMPTS": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3766,7 +3768,9 @@
|
|||
"description": "Number of times nsINetworkPredictor::Predict is called and attempts to predict"
|
||||
},
|
||||
"PREDICTOR_LEARN_ATTEMPTS": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3774,7 +3778,9 @@
|
|||
"description": "Number of times nsINetworkPredictor::Learn is called and attempts to learn"
|
||||
},
|
||||
"PREDICTOR_PREDICT_FULL_QUEUE": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 60000,
|
||||
|
@ -3782,7 +3788,9 @@
|
|||
"description": "Number of times nsINetworkPredictor::Predict doesn't continue because the queue is full"
|
||||
},
|
||||
"PREDICTOR_LEARN_FULL_QUEUE": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 60000,
|
||||
|
@ -3790,7 +3798,9 @@
|
|||
"description": "Number of times nsINetworkPredictor::Learn doesn't continue because the queue is full"
|
||||
},
|
||||
"PREDICTOR_WAIT_TIME": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 3000,
|
||||
|
@ -3798,7 +3808,8 @@
|
|||
"description": "Amount of time a predictor event waits in the queue (ms)"
|
||||
},
|
||||
"PREDICTOR_PREDICT_WORK_TIME": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 3000,
|
||||
|
@ -3807,7 +3818,9 @@
|
|||
"alert_emails": ["necko@mozilla.com"]
|
||||
},
|
||||
"PREDICTOR_LEARN_WORK_TIME": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 3000,
|
||||
|
@ -3815,7 +3828,9 @@
|
|||
"description": "Amount of time spent doing the work for learn (ms)"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PREDICTIONS": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3823,9 +3838,9 @@
|
|||
"description": "How many actual predictions (preresolves, preconnects, ...) happen"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PREFETCHES": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"expires_in_version": "never",
|
||||
"alert_emails": [],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [1016628],
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3833,9 +3848,9 @@
|
|||
"description": "How many actual prefetches happen"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PREFETCHES_USED": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"expires_in_version": "never",
|
||||
"alert_emails": [],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [1016628],
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3843,9 +3858,9 @@
|
|||
"description": "How many prefetches are actually used by a channel"
|
||||
},
|
||||
"PREDICTOR_PREFETCH_TIME": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"expires_in_version": "never",
|
||||
"alert_emails": [],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [1016628],
|
||||
"kind": "exponential",
|
||||
"high": 3000,
|
||||
|
@ -3853,15 +3868,19 @@
|
|||
"description": "How long it takes from OnStartRequest to OnStopRequest for a prefetch"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PRECONNECTS": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"expires_in_version": "never",
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
"n_buckets": 50,
|
||||
"description": "How many actual preconnects happen"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_CREATED": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3869,7 +3888,9 @@
|
|||
"description": "How many preconnects actually created a speculative socket"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_USED": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3877,7 +3898,9 @@
|
|||
"description": "How many preconnects actually created a used speculative socket"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_UNUSED": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3885,7 +3908,9 @@
|
|||
"description": "How many preconnects needlessly created a speculative socket"
|
||||
},
|
||||
"PREDICTOR_TOTAL_PRERESOLVES": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3893,7 +3918,9 @@
|
|||
"description": "How many actual preresolves happen"
|
||||
},
|
||||
"PREDICTOR_PREDICTIONS_CALCULATED": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 1000000,
|
||||
|
@ -3901,7 +3928,9 @@
|
|||
"description": "How many prediction calculations are performed"
|
||||
},
|
||||
"PREDICTOR_GLOBAL_DEGRADATION": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "linear",
|
||||
"high": 100,
|
||||
|
@ -3909,7 +3938,9 @@
|
|||
"description": "The global degradation calculated"
|
||||
},
|
||||
"PREDICTOR_SUBRESOURCE_DEGRADATION": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "linear",
|
||||
"high": 100,
|
||||
|
@ -3917,7 +3948,9 @@
|
|||
"description": "The degradation calculated for a subresource"
|
||||
},
|
||||
"PREDICTOR_BASE_CONFIDENCE": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "linear",
|
||||
"high": 100,
|
||||
|
@ -3925,7 +3958,9 @@
|
|||
"description": "The base confidence calculated for a subresource"
|
||||
},
|
||||
"PREDICTOR_CONFIDENCE": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "linear",
|
||||
"high": 100,
|
||||
|
@ -3933,7 +3968,8 @@
|
|||
"description": "The final confidence calculated for a subresource"
|
||||
},
|
||||
"PREDICTOR_PREDICT_TIME_TO_ACTION": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 3000,
|
||||
|
@ -3942,7 +3978,9 @@
|
|||
"alert_emails": ["necko@mozilla.com"]
|
||||
},
|
||||
"PREDICTOR_PREDICT_TIME_TO_INACTION": {
|
||||
"record_in_processes": ["main", "content"],
|
||||
"record_in_processes": ["main"],
|
||||
"alert_emails": ["hurley@mozilla.com"],
|
||||
"bug_numbers": [881804],
|
||||
"expires_in_version": "never",
|
||||
"kind": "exponential",
|
||||
"high": 3000,
|
||||
|
|
|
@ -445,24 +445,6 @@
|
|||
"PRCONNECT_BLOCKING_TIME_NORMAL",
|
||||
"PRCONNECT_BLOCKING_TIME_OFFLINE",
|
||||
"PRCONNECT_BLOCKING_TIME_SHUTDOWN",
|
||||
"PREDICTOR_BASE_CONFIDENCE",
|
||||
"PREDICTOR_CONFIDENCE",
|
||||
"PREDICTOR_GLOBAL_DEGRADATION",
|
||||
"PREDICTOR_LEARN_ATTEMPTS",
|
||||
"PREDICTOR_LEARN_FULL_QUEUE",
|
||||
"PREDICTOR_LEARN_WORK_TIME",
|
||||
"PREDICTOR_PREDICTIONS_CALCULATED",
|
||||
"PREDICTOR_PREDICT_ATTEMPTS",
|
||||
"PREDICTOR_PREDICT_FULL_QUEUE",
|
||||
"PREDICTOR_PREDICT_TIME_TO_INACTION",
|
||||
"PREDICTOR_SUBRESOURCE_DEGRADATION",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_CREATED",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_UNUSED",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_USED",
|
||||
"PREDICTOR_TOTAL_PREDICTIONS",
|
||||
"PREDICTOR_TOTAL_PRERESOLVES",
|
||||
"PREDICTOR_WAIT_TIME",
|
||||
"PROCESS_CRASH_SUBMIT_ATTEMPT",
|
||||
"PROCESS_CRASH_SUBMIT_SUCCESS",
|
||||
"PWMGR_BLOCKLIST_NUM_SITES",
|
||||
|
@ -1131,26 +1113,6 @@
|
|||
"PRCONNECT_BLOCKING_TIME_NORMAL",
|
||||
"PRCONNECT_BLOCKING_TIME_OFFLINE",
|
||||
"PRCONNECT_BLOCKING_TIME_SHUTDOWN",
|
||||
"PREDICTOR_BASE_CONFIDENCE",
|
||||
"PREDICTOR_CONFIDENCE",
|
||||
"PREDICTOR_GLOBAL_DEGRADATION",
|
||||
"PREDICTOR_LEARN_ATTEMPTS",
|
||||
"PREDICTOR_LEARN_FULL_QUEUE",
|
||||
"PREDICTOR_LEARN_WORK_TIME",
|
||||
"PREDICTOR_PREDICTIONS_CALCULATED",
|
||||
"PREDICTOR_PREDICT_ATTEMPTS",
|
||||
"PREDICTOR_PREDICT_FULL_QUEUE",
|
||||
"PREDICTOR_PREDICT_TIME_TO_ACTION",
|
||||
"PREDICTOR_PREDICT_TIME_TO_INACTION",
|
||||
"PREDICTOR_PREDICT_WORK_TIME",
|
||||
"PREDICTOR_SUBRESOURCE_DEGRADATION",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_CREATED",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_UNUSED",
|
||||
"PREDICTOR_TOTAL_PRECONNECTS_USED",
|
||||
"PREDICTOR_TOTAL_PREDICTIONS",
|
||||
"PREDICTOR_TOTAL_PRERESOLVES",
|
||||
"PREDICTOR_WAIT_TIME",
|
||||
"PROCESS_CRASH_SUBMIT_ATTEMPT",
|
||||
"PROCESS_CRASH_SUBMIT_SUCCESS",
|
||||
"PWMGR_BLOCKLIST_NUM_SITES",
|
||||
|
|
|
@ -2416,6 +2416,13 @@ TSFTextStore::FlushPendingActions()
|
|||
selectionSet.mLength =
|
||||
static_cast<uint32_t>(action.mSelectionLength);
|
||||
selectionSet.mReversed = action.mSelectionReversed;
|
||||
DispatchEvent(selectionSet);
|
||||
if (!selectionSet.mSucceeded) {
|
||||
MOZ_LOG(sTextStoreLog, LogLevel::Error,
|
||||
("0x%p TSFTextStore::FlushPendingActions() "
|
||||
"FAILED due to eSetSelection failure", this));
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -433,11 +433,6 @@ struct CoTaskMemFreePolicy
|
|||
|
||||
SetThreadDpiAwarenessContextProc WinUtils::sSetThreadDpiAwarenessContext = NULL;
|
||||
EnableNonClientDpiScalingProc WinUtils::sEnableNonClientDpiScaling = NULL;
|
||||
#ifdef ACCESSIBILITY
|
||||
typedef NTSTATUS (NTAPI* NtTestAlertPtr)(VOID);
|
||||
static NtTestAlertPtr sNtTestAlert = nullptr;
|
||||
#endif
|
||||
|
||||
|
||||
/* static */
|
||||
void
|
||||
|
@ -461,12 +456,6 @@ WinUtils::Initialize()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ACCESSIBILITY
|
||||
sNtTestAlert = reinterpret_cast<NtTestAlertPtr>(
|
||||
::GetProcAddress(::GetModuleHandleW(L"ntdll.dll"), "NtTestAlert"));
|
||||
MOZ_ASSERT(sNtTestAlert);
|
||||
#endif
|
||||
}
|
||||
|
||||
// static
|
||||
|
@ -683,10 +672,6 @@ WinUtils::MonitorFromRect(const gfx::Rect& rect)
|
|||
}
|
||||
|
||||
#ifdef ACCESSIBILITY
|
||||
#ifndef STATUS_SUCCESS
|
||||
#define STATUS_SUCCESS ((NTSTATUS)0x00000000L)
|
||||
#endif
|
||||
|
||||
/* static */
|
||||
a11y::Accessible*
|
||||
WinUtils::GetRootAccessibleForHWND(HWND aHwnd)
|
||||
|
|
|
@ -6502,7 +6502,15 @@ LRESULT nsWindow::ProcessKeyUpMessage(const MSG &aMsg, bool *aEventDispatched)
|
|||
{
|
||||
ModifierKeyState modKeyState;
|
||||
NativeKey nativeKey(this, aMsg, modKeyState);
|
||||
return static_cast<LRESULT>(nativeKey.HandleKeyUpMessage(aEventDispatched));
|
||||
bool result = nativeKey.HandleKeyUpMessage(aEventDispatched);
|
||||
if (aMsg.wParam == VK_F10) {
|
||||
// Bug 1382199: Windows default behavior will trigger the System menu bar
|
||||
// when F10 is released. Among other things, this causes the System menu bar
|
||||
// to appear when a web page overrides the contextmenu event. We *never*
|
||||
// want this default behavior, so eat this key (never pass it to Windows).
|
||||
return true;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
LRESULT nsWindow::ProcessKeyDownMessage(const MSG &aMsg,
|
||||
|
|
Загрузка…
Ссылка в новой задаче