Merge autoland to mozilla-central. a=merge

This commit is contained in:
shindli 2018-02-23 11:39:45 +02:00
Родитель d068047dfa 0c2a975587
Коммит d611a1c062
30 изменённых файлов: 1787 добавлений и 272 удалений

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

@ -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>
* &#64;Setting.List(&#64;Setting(key = Setting.Key.USE_MULTIPROCESS,
* value = "false"))
* &#64;Test public void test() { ... }
* </pre>
* <p>
* Use multiple settings:
* <pre>
* &#64;Setting.List({&#64;Setting(key = Setting.Key.USE_MULTIPROCESS,
* value = "false"),
* &#64;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,